dsh-git-workflow
September 12, 2026 · View on GitHub
Git workflow tools for DeepSeek Harness: a real Cordis host tool plugin that
builds validated git argv arrays, runs them through an injectable command seam,
and parses the status/diff/log text a caller pastes in.
git_branch_planturns an intent (list, current, create, switch, merge, rebase, delete, publish) into a validated argv array. It plans by default and only spawns git when the caller passesrun=true.git_conflicts_parseturns any shape ofgit status,git ls-files -u,git diff --diff-filter=U, or a conflicted patch into a per-file conflict report with resolution commands.git_log_formatturnsgit logoutput into structured commits plus a rendered changelog.- Nothing is pushed, reset, or cleaned up behind your back: history rewrites and deletions of protected branches are refused, and every command is shown before it can be run.
Installation
npx -y @deepseek-ai/dsh plugin --profile web add @qingshanjiluo/dsh-git-workflow
The bundle ships cordis.patch.yml, which inserts the plugin into the profile's
layer stack with the defaults below.
Configuration
| Field | Type | Default | Meaning |
|---|---|---|---|
gitPath | string | "git" | Executable the planner puts at argv[0] (git, or an absolute path). |
defaultBranch | string | "main" | Trunk assumed when a plan supplies no base. |
timeoutMs | number | 15000 | Per-command timeout handed to the command seam. |
protectedBranches | array | ["main","master","develop","release/*"] | Never deleted, never rebased, never force-pushed. * matches inside a name. |
branchPrefixes | array | ["feature/","hotfix/","fix/","chore/","docs/","test/","refactor/"] | Naming convention; a create outside them gets an advisory note, not a refusal. |
maxCommits | number | 200 | Cap on the commits git_log_format returns and renders. |
maxFiles | number | 200 | Cap on the files one conflict report lists. |
runCommand | function | defaultRunCommand | Seam (argv, timeoutMs) => { exitCode, stdout, stderr }. Defaults to spawnSync from node:child_process (no shell, 16 MB buffer cap); override it in tests or on git-less hosts. Not settable from YAML — it keeps its default there. |
now | function | () => Date.now() | Injectable clock used for commit ages, so rendered output is reproducible. Also not settable from YAML. |
Tools
Every parameter is declared required, so "not applicable" is an explicit empty
string, false, or 0 rather than a missing field — the model always sees the
full call.
| Tool | Arguments (all required) | Returns |
|---|---|---|
git_branch_plan | action (list | current | create | switch | merge | rebase | delete | publish), branch (name, or a --list glob pattern, or ""), base (start point for create, upstream for rebase; "" = defaultBranch), remote ("origin"), force (allow -C, -D, --force-with-lease), run (execute through the seam) | { ok, action, argv, command, errors, notes, run_requested, exitCode, stdout, stderr, message }. argv is the validated command array ([] when refused) and command its copy-pasteable form; run=false never touches the seam, so the default answer is a plan, not a side effect. |
git_conflicts_parse | text (raw git output, verbatim), source (auto or a forced parser: porcelain-v1, porcelain-v2, ls-files-u, status-long, diff-patch, diff-names) | { ok, format, operation, branch, inProgress, allResolved, conflicted[{path,state,stages,blocks,hint}], paths, count, markers, oursLabel, theirsLabel, stageCommand, continueCommand, abortCommand, notes, message }. state is one of both-modified, both-added, both-deleted, added-by-us, added-by-them, deleted-by-us, deleted-by-them, unmerged; operation is the merge / rebase / cherry-pick / revert / bisect / am in progress, and its --continue / --abort commands come back with it. |
git_log_format | text (raw git log output), delimiter ("" auto-detects \x1f, \x1e, tab, |), columns (field order; empty infers by field count), style (markdown | table | plain | jsonl), groupBy (none | type | author | day), limit (0 = all up to maxCommits) | { ok, format, delimiter, commits[{hash,shortHash,author,email,date,age,subject,type,scope,breaking}], count, returned, truncated, authors, sections[{label,count,entries}], rendered, notes, message }. rendered is ready to paste into a release note; type grouping uses Conventional Commits and flags ! subjects under ## BREAKING CHANGES. |
Input shapes the parsers accept, all captured from real git output:
git status --porcelain—UU path,AA path,DU path, and friends.git status --porcelain=v2—u UU N... <modes> <oids> path, plus# branch.*headers.git status(long form) —both modified: path,deleted by us: path.git ls-files -u— stage rows grouped per path, so a1+3pair is read as "deleted by us" and2+3as "both added".git diff/git diff --ccpatches — conflict markers are counted per file, with the<<<<<<<and>>>>>>>labels reported asoursLabel/theirsLabel.git diff --name-only --diff-filter=U— a bare path list.
Branch names are validated with git's own check-ref-format rules before any
argv is built: no whitespace or control characters, no ~ ^ : ? * [ ] \, no ..
or @{, no leading -, no leading or trailing /, no //, no leading ., no
trailing . or .lock. Anything that survives validation is passed as a literal
argv element — never through a shell — so quoting is not an attack surface.
Development
npm install --no-audit --no-fund
npx tsc --noEmit # types
npm run build # lib/index.js + lib/index.d.ts
npx vitest run # behaviour tests (fake seam, pinned clock, no git needed)
node scripts/load-smoke.mjs # loads the built artifact, registers the tools, drives them
Both tests and the smoke script replace runCommand and now, so the suite
never spawns a process, touches the network, or needs a repository.
License
MIT