dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
One clean git and GitHub surface for DSH: status, per-task worktrees, commit, push, sync, pull requests, and enforcement that stops raw git in bash from leaving dirty trees and unpushed commits behind.
One consistent way for DSH to use git and GitHub, and the automation that makes "nothing was left uncommitted" true without a model remembering it.
A third-party bundle for DeepSeek Harness. MIT licensed — see LICENSE. Not an official DeepSeek package.
# run in the profile directory — the install target, not a global prefix
pnpm add github:stansz/dsh-git-core # the shared library, first
pnpm add github:stansz/dsh-git
Every setting has a working default, so it mounts and does something useful with no configuration at all. They are listed under Settings, and Changing a setting shows how to override one.
| Package | Repository | What it is |
|---|---|---|
dsh-git-core | github.com/stansz/dsh-git-core | The shared library: one place a git command is run, one state store, one recorder of the files a session changes. It registers no tool, no command and no listener. |
dsh-git (this one) | github.com/stansz/dsh-git | The git tool, /git, the guard that redirects raw git in the shell, and the automatic commit and push at the turn boundary. |
dsh-git-worktrees | github.com/stansz/dsh-git-worktrees | One git worktree per task: the worktree tool (start, list, remove) and the guard that refuses a file edit landing in a repository sitting on a protected branch. |
Install the core first. dsh-git-core is a library, not a plugin: it registers
nothing, and its bundle patch is a deliberately empty []. Installing it puts it in
the profile's node_modules, where the other two bundles find it through
lib/core-loader.mjs — a link: install does not hoist dependencies, so a bare
import cannot resolve.
Core plus dsh-git is the minimum for the everyday flow: the tool, the guard and
the automatic commit and push. dsh-git-worktrees is a separate, optional bundle with
its own settings (worktreeRoot, autoWorktree, worktrees); none of those exist on
this bundle's row. docs/install.md has the full order, the loader's
probe list, and the portability rules.
Raw git through bash was inconsistent: sessions left work uncommitted and
unpushed, and nothing reported it. This bundle contributes four things, and the
last two are the ones that fix it:
| Contribution | What it is |
|---|---|
ctx.tools.register() | the git tool: clone, init, status, save, publish, sync, pr, merge, finish, prune |
ctx.commands.register() | /git, printing the same state to a human |
ctx.on('tools/pre-execute') | a guard that redirects the everyday flow in bash (and pwsh) at the tool that does it consistently, and refuses the commands that discard work |
ctx.on('agent/turn-stopping'), ctx.on('session/event'), ctx.on('tools/pre-execute') | the automatic sync: at a turn boundary, or a few seconds after a turn's edits stop when no boundary arrives, commit the paths this session recorded and push what is safe to push |
The last two are deliberately not opt-in behaviours an agent has to remember. An operation that must be remembered is the original bug.
One tool, ten actions. The model sees the full description of each; this is the shape of them.
clone — clone a repository from a URL. path is where it lands, branch
checks out a branch at clone time. Returns the state of the clone.init — create a new empty repository. path is where it lands, branch
names the initial branch.status — the state of a repository, or of every repository under a directory:
branch, ahead/behind, changed paths, worktrees, and the open pull request when asked
(pull: true). Reads only.save — commit the work this session did here. With no paths, it commits
exactly the files this session changed in this repository and reports the rest, so
nothing of another session's is swept in. paths names files explicitly, message
overrides the generated subject.publish — push the current branch, setting the upstream on the first push and
refusing a protected branch unless allowProtected is true. forceWithLease: true
replaces this branch's own remote ref after a rebase — git refuses it if the remote
moved since the last fetch.sync — fetch and bring the base in. With nothing else it fast-forwards when
this branch has no commits of its own, and reports the divergence when it has.
rebase: true replays this branch onto the base; merge: true merges the base in.
A conflict aborts whichever ran, changes nothing, and names the paths.pr — open a pull request, or return the one already open for this branch. Never
opens a second one.merge — merge the pull request by number, or the one open for the current branch,
then delete the remote branch it came from when that ref still points at the merged commit.
It fetches origin after a successful merge (baseFetched in the result), so a following
prune can prove what landed without an intervening sync; a failed fetch adds a note.finish — the closing move for a unit of work: save anything dirty, publish, open or
reuse the pull request, and report.prune — dry-run then remove what this tool left behind: task branches that no
worktree holds and that carry no commit of their own, plus branches a merged pull request
proves disposable. apply: true removes; a branch holding work is reported rather than
deleted, unless you name it with branches./git prints the same state to a human.
Raw git in the shell is inspected before it runs, for the agent's tool calls only —
a tools/pre-execute listener on bash and pwsh — so your own terminal is never
touched. Three outcomes:
git add, commit, push, pull, fetch
and rebase are refused, each naming the action that does the same job: save,
publish, sync (and sync with rebase: true for a replay).reset --hard/--merge/--keep, clean, a force push,
branch -D, checkout -- <paths>, restore, stash drop, tag -f, the history
rewrites, and the object-deleting gc --prune=now and git prune. Each says what is
lost; git prune -n/--dry-run removes nothing and runs.config, remote, tag,
branch, checkout, merge, clone, worktree, reads and the rest — are not
the daily flow, and denying them is how a guard becomes a wall (git config credential.helper is the repair path for a machine that cannot push). A dry run
(fetch --dry-run, push --dry-run, clean -n) sends nothing and runs.Detection is best-effort and not a security boundary: it reads the command line's
tokens and expands nothing. guardMode: warn allows everything, destructive commands
included, and reports nothing. The full refusal list, the override and wrapper cases,
and the dry-run edge cases are in docs/guard.md.
The outcome this bundle is named for is delivered here, and it does not depend on which commands a session used to write the files.
When a turn boundary arrives — agent/turn-stopping, or a session/event carrying
turn/end — the sync runs. When neither does, every tool call arms a settle timer:
four seconds of quiet and the sync runs on its own, and a session in an unbroken
burst of tool calls is still synced at least once a minute.
In each repository this session recorded work in, it commits only the paths this
session recorded, intersected with what is actually dirty. Anything else dirty is
left where it is and named in the report instead of being swept into this session's
commit. The commit subject says what the commit contains —
dsh: turn 3 — a.mjs, b.mjs, or dsh: a.mjs when there was no turn number — and
the body carries Assisted-by: DSH.
The commit is pushed when the branch is safe to push: one under branchPrefix, or
the branch in use when nothing is protected. A protected branch is never pushed
automatically, and a branch whose work is already in the base is not pushed either.
A skip, a refused push or a failure is written into the model's context and wakes it,
so a sync that declined to commit is not silent — unless leaving the tree alone is what
the settings asked for.
Detail, including the exact notices: docs/turn-sync.md.
This file is the overview. The detail lives beside it:
| File | What is in it |
|---|---|
docs/turn-sync.md | the automatic half: what is committed and pushed, when it declines, how git hooks interact with it, and how to watch it |
docs/guard.md | the guard's policy: what raw git in bash is refused or allowed, and why |
docs/install.md | a fresh machine: three packages, the order they install in, and link: versus GitHub installs |
Every setting has a working default; these are the ones worth knowing.
| Field | Default | Meaning |
|---|---|---|
stateRoot | $DSH_HOME/state, else ~/.dsh/state | Where session state is recorded: the repositories this session touched, and the push backoff |
branchPrefix | dsh/ | Task branch namespace; these are what automatic push may push, and what prune may remove |
baseBranch | (empty) | Empty resolves origin/HEAD, then main |
protectedBranches | [] | The plugin's own list of branch names, matched literally against the checked-out branch. Never pushed automatically. An explicit [] lets a publish to main through without allowProtected |
guardMode | redirect | off | warn | redirect. redirect is the policy above; warn classifies and then allows everything, destructive commands included, and reports nothing |
guardAsk | ask | The one command the guard cannot classify — a wrapper whose inner text peeked a work-loss command. ask and deny both refuse it; allow accepts it |
guardTools | [bash, pwsh] | Which tools the guard inspects |
plainMode | false | Shorthand for the tool without the automation: turns turnCommit and turnPush off and protects nothing. A named key beside it wins |
turnCommit | true | Commit each turn's recorded paths |
turnCommitMessage | (empty) | Empty generates dsh: turn <N> — <paths>, or dsh: <paths> with no turn number. {turn} and {paths} are substituted |
turnPush | true | Push each turn when the branch is safe |
turnSyncScope | session | session touches only repositories this session recorded work in; workspace also scans for any dirty repository |
turnCommitMaxFiles | 200 | Skip above this many changed paths and report the repository instead |
turnSyncTimeoutMs | 20000 | Budget for one end-of-turn sync |
githubHost | github.com | Set for GitHub Enterprise |
prDraft | false | Open pull requests as drafts by default |
timeoutMs | 30000 | Per-git-command deadline |
The worktree settings — worktreeRoot, autoWorktree, worktrees — belong to the
dsh-git-worktrees bundle and are not fields of this one.
Override this bundle's row in the profile's patch layer,
~/.dsh/profiles/<name>/cordis.patch.yml:
- id: dsh-git
name: 'dsh-git'
config:
protectedBranches: [main, release]
Only the fields you name are applied; every other field keeps its default. The row id
is dsh-git. Restart the Harness to load it — a plugin's row config is read when the
plugin mounts.
A row that carries a setting the schema no longer declares can fail at mount. The two
that used to live here are worktreeRoot and worktrees; they belong to
dsh-git-worktrees now, so remove them from this bundle's row rather than leaving
them behind. docs/install.md
has the exact symptom and fix.
There is no Configure page: the one that existed wrote a single toggle into this hand-maintained file, with values nobody asked for, and its effect arrived at the next start anyway. The patch layer above is how a setting is changed.
Nothing is stored here. git runs with HOME and PATH preserved, so the
machine's own credential helper resolves credentials — on this machine that is
osxkeychain from the system gitconfig. GitHub API calls use
GH_TOKEN → GITHUB_TOKEN → gh auth token --hostname <host> — the first two need
nothing installed — read lazily, never logged, never written to disk, and redacted from
every error. Neither half is interactive: prompting is off (GIT_TERMINAL_PROMPT=0, no
askpass), so nothing can block on a question with nobody to answer it — and a credential
embedded in a remote URL is dropped wherever a remote is reported, so status shows the
host and the path, never the userinfo.
The two halves share nothing: a machine can have a working git credential and no GitHub
token, or the reverse. Reading a public repository needs no credential at all, so a new
machine looks healthy until its first push, and GH_TOKEN never helps a push. A push
or a fetch that fails for want of a credential is reported with the fix: a fetch
states it as next:, while for a push the note the turn ends with and the record
status prints both name a credential helper, an SSH remote, or — when the GitHub CLI
is present — gh auth setup-git. A failure is not remembered: the chain is asked
again on the next call, so gh auth login in a terminal is picked up without
restarting the Harness.
docs/install.md is the
checklist for a machine that has never logged in.
GitHub is talked to over REST rather than through gh, because structured
status codes beat matching error prose — and because
gh pr merge --delete-branch does
local branch cleanup in the current directory's repository, matching the branch
by name only, which can delete an unrelated checkout's unpushed commits.
node --test from the repository root: no install step, no network, no GitHub
token. The tests build real repositories with the real git binary, drive the
actions, and check the repository actual state afterwards; the GitHub API is a
local stand-in that performs a real merge in a bare origin, so the pull-request
workflow is exercised against real git refs. What each file pins, and what the
fakes are allowed to fake, is in docs/testing.md.
git on PATH. gh is optional and only used as a token source; without it, set
GH_TOKEN. Node 18+ for global fetch. The one package dependency is
dsh-git-core, resolved at runtime through lib/core-loader.mjs;
@deepseek-ai/schemastery is declared but loaded through createRequire probes, so
a profile that cannot resolve it still mounts with every setting at its default.
On the Harness side it declares tools, commands and sessions in inject, so
a Harness without those leaves the row pending rather than half-mounting it. It
reaches for agents and workspaceChanges defensively — a build without them
still works, with a less precise idea of which files a turn changed — and registers on
tools/pre-execute, agent/turn-stopping and session/event.