baton
Manifest validRuns an approved OpenSpec change to completion while the model stays a mechanical forwarder: a bundled local MCP server computes the ticket DAG frontier, claims tasks atomically, prepares one git work
Baton
The sole ticket / DAG / session-subagent control plane over OpenSpec changes. A local MCP server exposes deterministic session actions; the main session delegates each assignment with the DSH
subagenttool, binds the child id, and reconciles the completion notice against a sealed receipt.
The problem it solves
OpenSpec is strong on spec-driven decomposition (proposal -> tasks -> archive)
but does not execute. Baton turns an approved change into finished work,
deterministically: the main session never decides ticket state itself. It
forwards the deterministic actions returned by the Baton MCP server to DSH
subagents verbatim, then reconciles each completion notice against a sealed
receipt (the assignment's verification commands run green in an isolated
worktree). Free-text subagent output is ignored.
A sealed receipt is hash-bound evidence produced locally by receipt.py
(task-packet hash + clean, advanced HEAD + exit-0 verification results). It is
integrity evidence, not a cryptographic proof against a hostile subagent.
The change's tasks and the execution share the same granularity (one assignment ~ one task), and three seams are bridged deterministically:
| Seam | Baton's answer |
|---|---|
| Dependency (section-order DAG -> what to run next) | SessionController.actions() computes the frontier fresh every call and atomically claims distinct tasks |
| Concurrency (<=5 parallel tasks) | one isolated git worktree per assignment; integration serialized by a repository-scoped lock |
| State (subagent done -> task resolved) | a sealed receipt is the only success evidence; the controller integrates the branch, finalizes the attempt, and flips the task's [x] in tasks.md |
Baton is the conductor. SessionController is the sole deterministic
scheduler; the main session is a mechanical forwarder; OpenSpec is the plan
face.
Install (DSH)
Baton is a DSH bundle: one package mounts the baton skill and starts the local
MCP server. It requires DSH 0.2.1-rc.2 or newer, a profile that provides the
subagent tool (the shipped dsh-base does), and Python 3.10+ on PATH (or
BATON_PYTHON pointing at it).
# From a checkout
dsh plugin add /path/to/baton
# Once published
dsh plugin add dsh-openspec-baton
Verify the bundle (self-contained, needs only python3 + git; drives the
real MCP server over stdio and exits 0 when healthy):
python3 tests/mcp_smoke.py
# MCP SMOKE PASS: handshake, canonical tool registry, actions dispatch,
# status snapshot, error paths — all green over live stdio
The package name is dsh-openspec-baton to avoid colliding with the unrelated
dsh-baton plugin. The repository and brand remain baton, and the MCP server
keeps the baton namespace (mcp__baton__*).
Install (ZCode, local dev only)
The same repository also carries a ZCode plugin form
(.zcode-plugin/plugin.json, not published to any public marketplace): it
mounts the identical skills/baton/SKILL.md and starts the same stdio server
(python3 + mcp-servers/baton-server.py), so the mcp__baton__* tools work
in a ZCode session the same way. Python 3.10+ must be reachable as python3.
Quick start
# In your business project, write an OpenSpec change (the project's normal
# openspec/AGENTS.md workflow) and get it approved:
# openspec/changes/<change-id>/proposal.md + tasks.md (unchecked N.M tasks)
tasks.md is the ticket source. Sections are phases, tasks carry N.M ids,
and a task is blocked by every task in earlier sections (tasks within one
section run in parallel). Minimal shape:
# Tasks: add-login
## 1. Backend
- [x] 1.1 token endpoint
- [ ] 1.2 session persistence
## 2. Frontend
- [ ] 2.1 login form
[x] = resolved, [ ] = ready (no draft state); the exact parsing rules are
in docs/ticket-format.md.
Then ask the session:
run baton on <project_path> <change-id>
The skill drives actions -> subagent -> bind_agent -> completion notice
-> reconcile until the batch returns complete.
Architecture
main session (mechanical forwarder - no model-driven state decisions)
|- mcp__baton__actions(project_path, feature, session_id, capacity<=5)
| -> SessionController.actions(): recover/reconcile sealed receipts,
| compute frontier, atomically claim, prepare isolated worktrees +
| immutable Task Packets, return dispatch/recover/resolve_merge/
| wait/blocked/complete actions
|- subagent(dispatch.prompt, cwd=dispatch.cwd) -> child id
|- mcp__baton__bind_agent(assignment_id, claim_token, agent_id)
|- runtime sends the subagent completion notice
|- mcp__baton__reconcile(assignment_id, claim_token, agent_terminal, ...)
-> sealed receipt -> succeeded + integrated; otherwise unknown + recover
SessionController (the sole deterministic scheduler)
|- durable plan store (.dsh/baton/queue.db): revisions, attempts, leases,
| sealed receipts, completion facts
|- isolated assignment worktrees + a shared integration worktree
|- serial git integration; merge conflicts preserved (never auto-aborted)
`- latest-plan completion (all required tickets verified + integrated)
Capabilities
Session-subagent execution (sole path):
- Deterministic scheduling - claim, lease, worktree, receipt verification, integration, completion are all scripted; the model never decides state.
- Isolated worktrees - one branch per assignment; capacity <= 5 sliding window; integration serialized by a repository-scoped lock.
- Sealed receipts -
scripts/receipt.py verify/sealis the only accepted success evidence; a completion notice without a receipt flips the attempt tounknown.
Control plane:
- OpenSpec-native cognitive phase - "use baton to finish X" means the
session runs the project's OpenSpec workflow (align -> write the change ->
user approves), then drives execution to
complete. No self-built Grill/Brief/approval state machine. - Live plan = edit tasks.md - add tasks or sections at runtime; the next
actions()call records a new plan revision and the frontier is recomputed. In-flight packets stay immutable.
Observability: scripts/ticket_board.py produces a pure static HTML
Kanban board over a project's OpenSpec changes (zero process, zero port,
colored feature tags, open via file://):
python3 scripts/ticket_board.py build-all /path/to/project
# → .dsh/baton/board-all.html (every change on one board)
python3 scripts/ticket_board.py build /path/to/project <change-id>
# → .dsh/baton/<change-id>/board.html
# <change-id> is optional for build/scan when the project has exactly one
# change — the common day-one case — and the omission fails loud (naming the
# dir or the candidates) on zero or several.
Clicking a card shows the copy-paste check / uncheck commands (runnable
from any directory); a flip rewrites any built board in place — just reload
the page.
Standalone coordination kernel: scripts/queue.py runs without the MCP
server and without a session — wrap any shell command in a resource gate,
submit background jobs, freeze resources, and audit everything. State lives
in one SQLite store (default ~/.baton/coordination/queue.db, --db to
override):
| Verb | What it does |
|---|---|
exec --resource R [--resource R2 …] -- <cmd> | claim the resources (exclusive), wait for the lease, run <cmd>, exit with its exit code; identical owner+command+resource submissions collapse onto one job |
submit <spec.json> | submit a multi-stage background job from a JSON spec (shape mirrors Job.from_dict); prints the persisted job id |
status (--resource R / --job J / --status S) | one-line-per-job summary, one resource's leases + waiters, or full job detail + events |
cancel <job> [--force] | cancel a queued job; running jobs need --force (signals the owner process group and releases the lease) |
freeze R / unfreeze R | stop granting new leases on a resource / reverse it |
declare R [--capacity N] [--mode exclusive|capacity|shared] | annotate a resource's capacity and claim mode up front instead of relying on the lazy capacity=1 default |
gate --resource R [--drain-check CMD] -- <cmd> | freeze the resources, wait until every in-flight lease is gone and the optional check exits 0, run the disruptive command, then unfreeze in a finally; on drain timeout exits 124 without running the command |
events [--job J] [--type T] [--limit N] | dump the audit log as JSON |
reap [--dry-run] [--lease-ttl S] | scan heartbeat-expired leases and route stale jobs per the guardian contract |
The headline is the drain barrier: a disruptive command (migration, reindex) stops racing other users of the resource — new claimants queue while in-flight leases finish, and the barrier always unfreezes, even when the command fails:
python3 scripts/queue.py gate --resource db:prod --reason "schema migration" \
-- ./migrate.sh
Bundle contents
| Path | Purpose |
|---|---|
index.js | mounts the bundled skills/ provider and the stdio MCP server |
cordis.patch.yml | DSH bundle insert row (dsh-openspec-baton) |
.zcode-plugin/plugin.json | ZCode plugin manifest (local dev installs; same skill + MCP server) |
skills/baton/SKILL.md | the baton skill (actions -> subagent -> reconcile) |
mcp-servers/baton-server.py | local stdio MCP server (mcp__baton__*) |
scripts/ | deterministic controller, plan store, receipts, queue CLI |
History
Baton v1 drove an external ralph.sh loop. v2 brought the loop into Python.
v3 replaced the subprocess model with subagents but still used a shared PRD +
passes flags. v4 retired the PRD/passes protocol and made
SessionController the sole deterministic scheduler. v5 (current) adopts
OpenSpec as the single plan face - the change's tasks.md is the ticket source
(section-order DAG, [x] write-back), and the self-built cognitive/amendment
engines are retired.
Requirements
- DSH
0.2.1-rc.2or newer, withsubagentavailable (dsh-base) - Python 3.10+ (
python3on macOS/Linux,pythonon Windows, orBATON_PYTHON) + pytest - A git target project (the controller prepares isolated worktrees)
- The target project uses OpenSpec (
openspec/changes/<id>/tasks.md)
Regression
The standard regression entry - full pytest suite (incl. temp-git-fixture e2e), a live MCP stdio smoke test of the server, and a docs/file consistency check:
./tests/regression.sh
Documentation
- docs/architecture.md - why the deterministic control plane is the core IP
- docs/ticket-format.md - the OpenSpec change -> ticket mapping spec
License
MIT
Comments
Loading…
Similar plugins
by Kaiji-Z
An RTS-style orchestration dashboard where commands replace chats: a resident commander agent decomposes each order into task briefs with acceptance criteria, typed subagent troops (recon/engineer/med
★ 3
↓ 420/wk
MIT
TypeScript
Sep 30, 2026
dsh plugin --profile web add dsh-plugin-stardeckby dd2673
Codex-style Git workbench: workspace-scoped changes, diff, stage, commit, guarded branch operations, graph history, fetch, fast-forward pull, and upstream push through official plugin APIs.
★ 1
Apache-2.0
TypeScript
Aug 16, 2026
dsh plugin --profile web add dsh-wending-git-workbenchby bainianlaoyao
Codex-shaped coding tools and agent presets for GPT models on DeepSeek Harness: exec_command, write_stdin, apply_patch and view_image, plus OpenAI Chat Completions and Responses API routes, bridged to
★ 0
JavaScript
Sep 13, 2026
dsh plugin --profile web add dsh-codex-modeAutomatically orchestrates complex DeepSeek Harness requests. It detects when delegation is useful, builds a dependency-aware plan, assigns bounded researcher, architect, backend, frontend, tester, an
★ 0
dsh plugin --profile web add dsh-task-orchestratorby RaditPasya
Git panel for DeepSeek Harness — branch dropdown, commit graph, and previews in the left rail, plus an always-on footer dock that follows your session
★ 0
MIT
TypeScript
Oct 7, 2026
dsh plugin --profile web add dsh-web-git-sidebarTurns a one-line DSH draft into a complete task brief (role / goal / constraints / output format). The engine is a byte-verified port of linshenkx/prompt-optimizer (three modes, 10 templates identical
★ 0
dsh plugin --profile web add @benrong/dsh-prompt-polish