dsh-agent-playbook
Verifieddsh-agent-playbook Β· v0.6.0 Β· MIT
DeepSeek Harness plugin for Agent-Playbook: multi-agent claims, isolated git worktrees, and enforced acceptance checks. "Done" is an exit code, not a claim.
Install
dsh plugin add dsh-agent-playbook Confirm the layer applied with dsh --profile default --dump-config β see the install guide.
Source
Tags
Creators
Readme
dsh-agent-playbook
A DeepSeek Harness plugin for Agent-Playbook.
The engine's whole thesis is one sentence: "done" is an exit code, not a claim. A task's
acceptance_checks are shell commands, and recording it done re-runs them and refuses if any
fail. This plugin brings that loop into the harness β with multi-agent attribution and isolated git
worktrees.
Why a plugin, and what it deliberately does not do
| Owns | System |
|---|---|
| Truth β backlog, claims, journals, acceptance checks, worktrees | Agent-Playbook (the engine) |
| Convenience β agent identity, context injection, model-facing tools | this plugin (the harness) |
The plugin never re-implements a gate. Every action translates into a pb invocation. If
something cannot be expressed as a pb call, the engine does not support it yet β and inventing a
second opinion about "done" is exactly the failure this design refuses.
It also shells out rather than importing the engine. That is not laziness: every engine command
reports refusal with process.exit(), so an in-process call would terminate the harness instead
of returning an error. As a subprocess, the exit code remains the contract.
What it gives you
1. Multi-agent writes stay attributable
Fan-out is the point, and it is where naive setups lose data. The plugin stamps every engine call with a real identity:
| Variable | Meaning |
|---|---|
PB_AGENT_ID |
who is writing (defaults to the harness session id) |
PB_SESSION_ID |
which harness session it came from |
PB_PARENT_AGENT_ID |
the spawning agent, for sub-agents |
PB_AGENT_CHAIN |
the full delegation path (root,sub,grand) |
PB_CLAIM_TOKEN |
proof of entitlement to the task this agent holds |
PB_RUNTIME |
dsh, so records show which host wrote them |
So a sub-agent writes as itself and is recorded as itself, while the engine can still verify it
was entitled to touch its ancestor's claim. A refused write is flagged ownership: unproven β never
silently dropped, never silently allowed.
Under the hood the engine serializes every state write through one lock and an atomic replace, and
stamps each journal row with a monotonic seq. "Who wrote first, who wrote last, and on whose
behalf" is a recorded fact, not an inference from colliding timestamps.
2. Isolated worktrees, gated merges
worker actions drive a real git worktree per task:
create β open a slot (atomic: one live slot per task, no orphan worktrees)
status β ahead / behind / uncommitted / head
exec β run a command INSIDE the worktree
verify β run the task's acceptance_checks INSIDE the worktree
merge β gated by merge-ready; refuses unless the branch actually carries verified work
remove β tear the slot down
The merge gate reads the branch, not just the journal: a worktree that is missing, dirty, or has zero commits ahead of its base cannot be merged, and a verification that has gone stale is reported rather than trusted.
3. The constitution survives compaction
Before each agent step the plugin stages a short context block on the agent's inbox:
[agent-playbook]
playbook: my-project v0.3.6
loop: loop-7 β finish the worktree lifecycle
north star: Make "done" mean a verified exit code, not a claim.
backlog: 2 todo Β· 1 in_progress Β· 0 blocked Β· 4 done
in progress: T1 (alice)
task in hand: [T1] Do the thing
done means these exit 0:
$ npm test
$ node scripts/pb.mjs validate
Rule: "done" is an exit code, not a claim. Record with `pb record` β it re-runs the checks and refuses on failure.
The North Star, the active loop, the task in hand and its checks are re-injected every step, so a
long session or a compaction cannot lose the plot. When the backlog has nothing actionable the block
is suppressed (quietWhenIdle) rather than burning tokens.
4. The playbook's own skills are loadable
The project's skills/<id>/SKILL.md files are registered as harness skills, so its procedures load
the same way the harness's built-in ones do β including mode-local skills, because the ENGINE's own
resolution decides which are active (nothing is re-implemented in the plugin).
They are namespaced playbook-<id>: a harness skill is invoked by slot (/name) and a playbook
skill by path, so a bare id could shadow an unrelated harness skill. The catalog is discovered live
on each list, so a skill added to skills/index.yaml appears without a restart; bodies load on
demand rather than at discovery.
A loaded body is prefixed with its routing, because a playbook skill that says "follow the process" is useless without knowing which process file the engine's skills-first routing keys on:
# Playbook skill: run-task
This skill belongs to the workspace playbook. Operate the project through it:
- skill file: skills/run-task/SKILL.md
- canonical process: processes/run-task.yaml β follow it step by step
- record work with the `playbook` tool; "done" is an acceptance check exiting 0, not a claim
Install
The plugin carries the engine, so this is one install β no separate engine checkout and no version to keep in step by hand.
dsh plugin --profile <profile> add dsh-agent-playbook
That is the whole install. It installs the package AND enables it: dsh plugin forwards to
pnpm inside the profile directory, then reconciles dsh.profile.bundles β a dependency whose
package declares dsh.bundle.patch is appended to the layer stack automatically. There is no
bundle list to edit by hand.
Verified end to end β after that one command,
$DSH_HOME/profiles/<profile>/package.json contains both
"dependencies": { "dsh-agent-playbook": "^0.5.1" },
"dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "β¦", "dsh-agent-playbook"] } }
and dsh --profile <profile> --dump-config composes - id: agent-playbook.
Prerequisite: pnpm
dsh plugin is a pnpm forwarder, so pnpm must be on PATH:
npm install -g pnpm # or, if you have corepack: corepack enable pnpm
Without it the command fails with dsh: pnpm not found on PATH (exit 127).
What does NOT work: plain npm install
npm install dsh-agent-playbook does not enable the plugin. It installs a package into
whatever directory you happen to be in, and the harness only reads
$DSH_HOME/profiles/<name>/package.json β so the plugin lands where nothing looks for it and
nothing appears to happen. That is the confusing "installed but DSH wants more steps" state.
If you would rather manage the dependency yourself, the manual equivalent is to install it
into the profile directory and add it to the bundle list β the package declares
dsh.bundle.patch, so the name alone is the mount:
{ "dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "dsh-agent-playbook"] } } }
Two things that look like problems but are not
pnpm peers checkreports five missing peers. It inspects only the profile's ownnode_modules. The harness resolves its packages through$DSH_HOME/profiles/node_modulesβ a directory of junctions the launcher maintains, one level above the profile β so@deepseek-ai/cordis,dsh-agent,dsh-llm,dsh-toolsandschemasteryall resolve at runtime.- pnpm edits
pnpm-workspace.yaml. Installing a package published minutes earlier makes pnpm record aminimumReleaseAgeExcludeentry. That is its supply-chain guard recording a deliberate exception, not an error.
After installing
- Reload the session. The profile is composed at boot (
patchReloadislivefor the web profile andstartupfor headless), so a profile that gained a package wants a restart. - Give the workspace a playbook. The plugin is deliberately dormant until it finds one.
Then scaffold one for the workspace you want to operate, from the agent itself:
playbook action=init
That writes .agents-playbook/ (engine + master + runtime files) into the session
workspace, hydrates it with the engine's own init, and validates the result β all from
the bundled engine. The workspace copy then takes over, so the playbook is
self-hosting: it can scaffold further playbooks, and it does not depend on the
plugin being installed to keep working.
If you already have a playbook (or want it somewhere else), point the plugin at it instead:
- insert:
- id: agent-playbook
name: 'dsh-agent-playbook'
config:
playbookPath: '' # '' β discover: nested locations, then ancestors
playbookDir: '.agents-playbook' # where action=init scaffolds
injectContext: true
quietWhenIdle: true
maxContextChars: 4000
commandTimeoutMs: 120000
There is no engine install requirement to load the plugin: it looks for a playbook in
the well-known nested locations (.agents-playbook, .agent-playbook, .playbook,
agent-playbook) and then walks up from the session workspace. The plural
.agents-playbook is canonical; the singular .agent-playbook is a supported legacy alias
for projects scaffolded before the spelling settled. It stays dormant when there is none β
except that action=init always works, because that is what the bundled engine is for.
Which workspace it uses. The session's own directory, resolved from the harness workspace
registry β never the server's process cwd. When no workspace can be resolved the plugin
refuses (unknown workspace) rather than guessing, because guessing is how a playbook gets
scaffolded into an unrelated project.
Which engine runs, and where
Two separate things, deliberately:
| meaning | |
|---|---|
| which engine | the workspace's own copy if it has one, else the bundled one |
| which playbook | always a real workspace playbook β never the plugin's vendored copy |
A workspace playbook therefore wins over the bundle, and the bundle is never used as a target. That matters because the plugin now contains a playbook-shaped tree: a naive ancestor walk would "discover" it and operate on the wrong project.
Configuration
| Field | Default | Meaning |
|---|---|---|
playbookPath |
'' |
Absolute path, or '' to discover (nested locations, then ancestors). |
playbookDir |
.agents-playbook |
Where action=init scaffolds a playbook. |
injectContext |
true |
Stage the constitution on the agent's inbox each step. |
quietWhenIdle |
true |
Stay silent when nothing is todo or in_progress. |
maxContextChars |
4000 |
Character cap on the staged block. |
commandTimeoutMs |
120000 |
Per-pb-invocation timeout. |
stopGate |
true |
Hold a turn open (once per claim) when it ends with an unresolved claim. |
The Stop gate
Everything else in this plugin asks the agent to verify. The gate is the one place the harness refuses: when a turn is about to end while this agent still holds a claim it never resolved, the turn is held open and the model is told which task to resolve and how.
It is built on agent/turn-stopping, which fires as a turn would otherwise close; a handler
that calls agent.steer() keeps it open. That contract is pinned by
scripts/test-dsh-stop-gate.mjs and was confirmed in a live session β the handler steered at
fire 1 and the loop fired again for the same turn.
It is deliberately timid, because the harness documents the opposite failure: a handler
that blocks unconditionally force-continues every step. So a given claim is reminded about
at most once, the session caps its total, and the message says so. One extra step is a
nudge; an unbounded one is a runaway. Set stopGate: false to make the loop advisory again.
The playbook tool
| Action | What it does |
|---|---|
status |
Orient: backlog counts, active loop, guardrail state, recent records. |
anchor |
Re-inject the constitution (North Star + current cycle). |
next |
Show the next claimable task and its checks, without claiming. |
claim |
Claim it. Returns the claim token β pass it to a sub-agent so it can record on your behalf. |
task |
One task's projection: status, holder, checks, gate quality, worker. |
check |
Run that task's acceptance_checks on demand. |
record |
Record an outcome. status=done re-runs the checks and refuses on failure. |
worker |
Worktree lifecycle: create / status / exec / verify / merge / remove. |
init |
Scaffold + hydrate a playbook from the bundled engine (works with none present). |
unlock |
Report and (with force) clear a leaked lock. |
repair |
Check or rebuild the state projection from the append-only journal. |
Delegating to a sub-agent
1. playbook action=claim β task + claim token
2. spawn the sub-agent with:
PB_AGENT_ID=<sub>
PB_PARENT_AGENT_ID=<you>
PB_AGENT_CHAIN=<you>,<sub>
PB_CLAIM_TOKEN=<token>
3. the sub-agent works in `worker create`/`verify` and records as ITSELF, proven
4. playbook action=worker workerAction=merge β only after the gate opens
Development
npm run build:plugin # bundle the engine into dsh-plugin/engine/ (generated, gitignored)
npm run pack:plugin # build + `npm pack --dry-run` + manifest checks
node scripts/test-dsh-plugin-core.mjs # host-independent logic (no harness needed)
DSH_PACKAGES=<path to @deepseek-ai> node scripts/test-dsh-plugin-index.mjs
core.mjs holds everything host-independent (identity, discovery, subprocess results,
context rendering) so it is testable with plain node. index.js is the thin Cordis
layer. The index suite loads the real module, registers the tool against a stub ctx,
and executes it against a real playbook on disk; the bundle suite proves the shipped
engine is complete, clean of this repository's state, and can bootstrap a workspace. Both
skip cleanly when their prerequisites are absent, rather than failing for something the
checkout cannot fix.
License
MIT