Skip to content

dsh-agent-playbook

Verified

dsh-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 check reports five missing peers. It inspects only the profile's own node_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-tools and schemastery all resolve at runtime.
  • pnpm edits pnpm-workspace.yaml. Installing a package published minutes earlier makes pnpm record a minimumReleaseAgeExclude entry. That is its supply-chain guard recording a deliberate exception, not an error.

After installing

  1. Reload the session. The profile is composed at boot (patchReload is live for the web profile and startup for headless), so a profile that gained a package wants a restart.
  2. 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