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_plan turns 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 passes run=true.
  • git_conflicts_parse turns any shape of git 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_format turns git log output 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

FieldTypeDefaultMeaning
gitPathstring"git"Executable the planner puts at argv[0] (git, or an absolute path).
defaultBranchstring"main"Trunk assumed when a plan supplies no base.
timeoutMsnumber15000Per-command timeout handed to the command seam.
protectedBranchesarray["main","master","develop","release/*"]Never deleted, never rebased, never force-pushed. * matches inside a name.
branchPrefixesarray["feature/","hotfix/","fix/","chore/","docs/","test/","refactor/"]Naming convention; a create outside them gets an advisory note, not a refusal.
maxCommitsnumber200Cap on the commits git_log_format returns and renders.
maxFilesnumber200Cap on the files one conflict report lists.
runCommandfunctiondefaultRunCommandSeam (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.
nowfunction() => 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.

ToolArguments (all required)Returns
git_branch_planaction (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_parsetext (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_formattext (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 --porcelainUU path, AA path, DU path, and friends.
  • git status --porcelain=v2u 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 a 1+3 pair is read as "deleted by us" and 2+3 as "both added".
  • git diff / git diff --cc patches — conflict markers are counted per file, with the <<<<<<< and >>>>>>> labels reported as oursLabel / 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