Runs 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 worktree per assignment, and returns exact actions. The session delegates each action with the DSH subagent tool and reconciles the completion notice against a hash-bound receipt produced by re-running verification commands on a clean, advanced HEAD, so free-text "done" never counts. Runtime scope changes are plain tasks.md edits. Requires Python 3.10+ on PATH and a git project that uses OpenSpec.
Install
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:Lumos-789/baton
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
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
Links
More in this category
loopx-project/loopx#dsh-loopx-plugin★ 6537
LoopX, a provider-neutral, local-first state kernel and control plane for long-horizon agents: keeps Goal, Todo, gate, evidence, quota, recovery, and handoff state above DeepSeek Harness, while the plugin bootstraps the CLI and skills, admits bounded same-session continuation, and adds a loopback GoalBar for the exact bound loop.
Q00/ouroboros#integrations/dsh-plugin★ 6308
Config-only bundle that mounts Ouroboros through the DSH MCP client, exposing 36 interview, Seed, execution, evaluation, and evolution workflow tools in DSH.
chuspeeism/dashi-taskboard#deepseek-harness★ 3434
Embeds the active installed Codex Taskboard runtime in the DeepSeek Harness sidebar, using its launcher runtime descriptor instead of a fixed port.
NanmiCoder/dsh-agent-teams★ 2647
AgentTeams multi-agent teams.
EthanYoQ/AI-Novel-Writer#dsh-ai-novel-writer★ 2265
Installs a dedicated AI novel-writing preset and workbench: revisioned local project assets, a compact side drawer, and native approval-gated single-file changes.
Miaotofu01/Study-Mate★ 1307
Your AI learning companion: helping you plan your studies, understand concepts, and build projects to master a subject.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.