Skip to content

feat(baton): add the ticket/DAG subagent control plane plugin (0.3.2) - #167

Open
Lumos-789 wants to merge 1 commit into
zai-org:mainfrom
Lumos-789:feat/baton
Open

Lumos-789 wants to merge 1 commit into
zai-org:mainfrom
Lumos-789:feat/baton

Conversation

@Lumos-789

@Lumos-789 Lumos-789 commented Nov 4, 2026 •

Copy link
Copy Markdown

Plugin type (required) / 插件类型(必填)

  • UI Plugin / 是 UI 插件 — provides MCP Apps interactive pages / 提供 MCP Apps 交互页面
  • Standard plugin / 普通插件(非 UI) — does not provide MCP Apps interactive pages / 不提供 MCP Apps 交互页面
  • Not applicable / 不适用 — repository-only changes; no plugin content changed / 仅仓库级改动,不涉及插件内容

Affected plugin name(s) / 涉及的插件名称:baton

What / 改动内容

EN — Adds baton, a deterministic ticket/DAG/subagent control plane over OpenSpec changes: a local Python CLI computes the task frontier and returns session actions; the main session dispatches each task packet to a subagent in an isolated git worktree, and only a sealed receipt with green verification counts as done.

  • session_control.py drives dispatch, bind and reconcile; receipt.py verifies and seals; ticket_board.py renders a static progress board (no process, no port).
  • The sealed receipt digests its whole body (receiptHash), so an edited exit code, changed-file list or commit SHA is rejected before anything is merged.
  • Integration is serialized under a repository-scoped lock. A preserved merge conflict is attributed to the branch that actually caused it; a conflict owned by another assignment leaves the attempt untouched for a later retry instead of poisoning every pending receipt.
  • An expired lease retires the attempt and lets a retry take over, abandon is the operator escape hatch, and a dirty integration worktree is reported as a named blocked action instead of wedging the run.
  • status is a pure read, and task checkboxes are written back to tasks.md in both directions with line endings preserved.

中文 — 新增 baton:OpenSpec 变更上的确定性 ticket/DAG/子代理控制面。本地 Python CLI 计算任务前沿并返回会话动作,主会话把任务包派发给隔离 worktree 中的子代理,只有密封回执(整包哈希)加验证全绿才算完成;集成串行加锁,冲突归因到真正引入它的分支。

Visible behavior and testing / 可见行为与测试

  • python3 -m pytest tests -q → 179 passed (21 s); tests/regression.sh is the same suite plus the live-CLI and docs stages.
  • The whole cycle was run from the packaged payload directory (not the repo checkout): dispatch → seal → reconcile → complete, with tasks.md written back to [x] — so the bundled scripts are what was exercised.
  • Integration edge cases were reproduced against a real git repo before fixing: a preserved conflict from assignment A no longer marks B blocked-review; a dirty integration worktree produces a named blocked action instead of raising on every actions call; a hard merge failure lands in blocked-review instead of sitting in integrating until the 2 h lease expires.
  • Tamper check: editing a sealed receipt's exit code or commit SHA fails verification (tests/test_receipt.py).

Not covered by this round / 本轮未覆盖:

  • A full multi-agent dispatch (several subagents running concurrently against one change) was not run end to end; capacity, locking and recovery are covered by tests instead.

Version and marketplace registration / 版本与市场注册

  • New plugin directory external_plugins/baton/ (33 files, 254 KiB), manifest and marketplace.json entry both at 0.3.2.
  • Category developer-tools; icon added at the repo-root path the entry uses (assets/baton/icon.png, 512×512).
  • description_i18n carries en and zh-CN, identical in both files. README.md / README_CN.md / CHANGELOG.md are all in the payload — the previous package file list omitted the Chinese README and the changelog, which is fixed in the source repository (14438b0).

Dependencies, network, permissions, side effects / 依赖、网络、权限与副作用

  • Python 3.10+ and git (worktrees, merges, the integration lock).
  • Writes only inside the target project: .dsh/baton/queue.db, per-assignment runs/<id>/{packet,verification,receipt}.json, the live plan.json projection, and isolated git worktrees/branches.
  • No network access, no credentials, no daemons and no background loops: every state change is a CLI call the main session makes.

Licensing / provenance / 许可与来源

  • MIT (LICENSE ships in the plugin directory); code written for this submission.
  • No third-party code, assets or services.

Self-check / 自查清单

  • Plugin name is unique kebab-case; new external plugin directory is external_plugins/baton/; existing fixes retain ownership / 插件名唯一且 kebab-case,外部新增插件位于 external_plugins/baton/,已有插件修复保留原归属
  • Usage category is set independently of publisher ownership / 用途分类 category 已填写,与维护归属分开:developer-tools
  • Plugin type and affected names are declared above / 已在上方声明插件类型与涉及的插件名称
  • version bumped in BOTH plugin.json and marketplace.json (artifacts are immutable) / plugin.json 与 marketplace.json 两处版本号已同步升级:0.3.2
  • README.md (EN) and README_CN.md (中文) both updated and equivalent / 中英文 README 均已更新且语义一致
  • description_i18n has both en and zh-CN / description_i18n 包含 en 与 zh-CN
  • No secrets, machine-specific paths, or private dependencies / 不含密钥、机器专属路径或私有依赖
  • Model/API/network dependencies documented / 模型、API、网络依赖已写明(见上方依赖小节与两个 README)
  • python3 scripts/validate.py passes on this branch, and the plugin artifact was built through the same build_zip() code path CI uses (size and sha256 in the comment below). The full python3 scripts/build_dist.py needs the repo's UI-plugin pnpm build and fails identically on upstream main (excalidraw: incomplete build), so CI runs it here — see the note above. / 本地 validate.py 通过,本插件产物用 CI 同一条 build_zip() 路径打出(体积与 sha256 见下方评论);完整 build_dist.py 依赖仓库自带 UI 插件的 pnpm build,在 main 上同样失败,交由 CI 执行

@Lumos-789
Lumos-789 force-pushed the feat/baton branch 2 times, most recently from b82f409 to 0a57b72 Compare November 4, 2026 12:22
@Lumos-789

Copy link
Copy Markdown
Author

0.3.0 — verification (2026-11-04)

Environment: ZCode CLI 0.17.1, macOS 27.0.1 arm64, Python 3.14.4, git 2.x.

Installed from a local marketplace → baton@0.3.0; the skill resolves from the plugin cache (baton/0.3.0/skills/baton/SKILL.md), not from a repo checkout.

Ran the deterministic controller from that installed copy against a scratch OpenSpec project (openspec/changes/demo/tasks.md with 1.1 and 1.2 unchecked):

Step Result
session_control.py <project> demo status {ok:true, revision:1, missingRequiredTickets:["1.1","1.2"]}
session_control.py <project> demo actions --session-id … --capacity 2 two dispatch actions, each with an isolated worktree path and a Task Packet on disk

Repo-side: ./tests/regression.sh → all 5 stages green (byte-compile, pytest 155 passed, live CLI smoke, docs consistency: 27 referenced paths across 4 docs), plugin_acceptance.py PASS baton@0.3.0.

Note on scope: 0.3.0 removes the generic resource queue (queue.py + coordination/) from the package — it was a second product that the controller never imported, and it now lives outside this bundle. The shipped scripts/ are the controller, the plan store, receipts and the ticket board; the skill documents run / status only.

Not covered: a full dispatch → subagent → sealed-receipt round (needs a hosted DSH session), and Windows.

@Lumos-789

Copy link
Copy Markdown
Author

Addendum — also run inside a real ZCode agent session

Not only the CLI from the cache: a headless ZCode session (sess_9b36001d-377c-4a27-87ab-c4355511b114) with the skill forced by its qualified name. The agent located scripts/session_control.py itself, ran status (→ revision=1, missing 1.1, 1.2) and actions --capacity 1 (→ one dispatch, ASG-demo-101-r1-a1), and correctly reported that actions claims the ticket as a side effect, so an unconsumed call needs recover before re-dispatch.

@panda920

panda920 commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

✅ Internal validation passed. A ZCode maintainer will review the change next; after acceptance, release checks run separately and this PR is closed automatically.

@Lumos-789

Copy link
Copy Markdown
Author

Third round — review & simplification (2026-11-05)

No new features. This pass asked whether the documentation still matches the code, and found one thing that would have broken a first-time user:

The skill was teaching a command that does not exist. SKILL.md documented run <project> <change-id> as the canonical verb, said "no subcommand defaults to run", and listed session_id as defaulting to baton-run. The real CLI has six verbs (actions, bind, reconcile, status, cleanup, abandon), add_subparsers(required=True), and --session-id is required=True. An agent following the skill would have failed on its first call. SKILL.md now tables the six verbs with their argument contracts (351 → 229 lines), and the duplicated material it used to carry — the loop diagram, the tasks.md sample, the status response schema, a walkthrough — is a pointer to README.md / docs/ticket-format.md instead.

Dead code removed (1283 lines of Python, 8440 → 7157). Everything was pre-verified as zero-call:

  • the local resource queue that moved out to a separate project in 0.3.0 left six public methods, nine helpers and two tables behind (queue_tasks, resource_leases) — the live attempt_resource_leases path is untouched;
  • the pre-0.3.0 worker API in worktree.py (prepare_worktree, remove_worktree, list_worktrees, …) was superseded by the assignment worktrees;
  • model.py still carried eight symbols from the retired agent-profile model;
  • topo_sort, plan_history, record_assignment, heartbeat_execution_attempt and record_result_envelope had no callers.

Leases are now documented. They are fixed at 7200 s with no renewal entry point, so a ticket longer than two hours always comes back as lease-expired and is re-dispatched. That is a real constraint a user needs before starting, and it was written down nowhere.

Verification: 156 tests pass and ./tests/regression.sh is 5/5 green (I re-ran both). The count went 172 → 156 because 16 tests exercised only the deleted code — I checked each of them rather than taking the number on trust.

Payload: re-synced, 28 files, verified blob-by-blob against Lumos-789/baton@aa3db75. tests/check_docs.py now also covers README_CN.md, docs/architecture.md and docs/ticket-format.md, so this class of drift fails CI next time.

Not verified this round: the full dispatch loop over a real change (unchanged code path, but the payload's CLI was only smoke-tested for --help and status).

@panda920

panda920 commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

✅ Internal validation passed. A ZCode maintainer will review the change next; after acceptance, release checks run separately and this PR is closed automatically.

@Lumos-789

Copy link
Copy Markdown
Author

Repushed to 0.3.2: head b540961 → abfbb18, payload 31 files, verify_pr_payload.py baton:167 → MATCH.

This round is a correctness pass over the integration path plus the leftovers of the 0.3.0 queue extraction. Every fix below was reproduced against a real git repo first and now has a regression test.

Three defects in the merge/integration path

  • A preserved conflict was attributed to the wrong assignment. git merge refuses to start while a conflicted merge is preserved, and git diff --diff-filter=U lists the earlier merge's files. The old code read that as "this branch conflicts", so one real conflict marked every other pending receipt blocked-review and emitted a second resolve_merge for a conflict that branch never touched — i.e. two solvers pointed at one shared worktree. integrate_assignment now returns a reason: conflict (only when MERGE_HEAD is this branch's own tip), merge-in-progress (another assignment owns the worktree — nothing attempted, attempt left untouched and retried later) or worktree-dirty.
  • A dirty integration worktree wedged the run. An uncommitted tracked change there made every actions call raise WorktreeError while the attempt sat in integrating until its two-hour lease expired. It is now a named blocked action (integration-worktree-dirty) listing the files and the abandon escape; the attempt keeps its lease and resumes once the worktree is clean.
  • A merge failure without conflict files stranded the attempt. It is now parked in blocked-review with failure_code=integration-failed: … and surfaced as a blocked action.

Two behaviour corrections

  • status is genuinely read-only. It used to create .dsh/baton/, the database and revision 1, and rewrite plan.json on every call (a mistyped change-id even left a runtime directory behind). PlanStore.snapshot() reads the existing store and writes nothing; revision: 0 now means "no plan revision recorded yet".
  • ticket_board uncheck no longer rewrites a CRLF file. It is the documented reverse of baton's own [x] write-back, but it read/wrote through universal-newline text I/O and wrote non-atomically — one flip turned a Windows checkout into a whole-file diff. The flip moved into _ticket.uncheck_task, the exact mirror of check_task.

Dead code removed (the 0.3.0 queue extraction's leftovers): the resource-lease subsystem (attempt_resource_leases table, claim-time insert loop, four deletes) and the assignments table — neither had a producer, because the tasks.md parser never fills resources, priority, agent_profile, authority, successor or source_amendment. PlanTicket is down to what the parser actually produces, and the pre-0.3.1 "adopt --verify from existing packets" path is gone. plan_store.py split 1262 → 434 lines (store_base / store_schema / attempt_store, dependency one-way).

Docs corrected: BATON_PYTHON was documented in README, README_CN and the skill but read by no code — removed. The PR body no longer claims "a failed probe rolls back" (that sentence was copied from galadon; baton has no probe).

New drift tripwire: tests/test_doc_cli_contract.py parses every CLI invocation in README / README_CN / SKILL.md / architecture.md and checks it against the real argparse tree — verb must be registered, each flag must belong to that verb. The stage-5 docs gate only checked that paths exist, which is how the 0.3.0 skill could teach a nonexistent run subcommand. Verified to fail on exactly that bug; session_control.build_parser() was extracted so the test reads the real parser rather than a copy.

Evidence

Check Result
pytest 179 passed (was 156)
./tests/regression.sh 5/5 stages green
plugin_acceptance.py baton PASS baton@0.3.2 (exit 0)
simplify_gate.py baton / public_release_gate.py repo baton / marketplace_check.py PASS / PASS / OK zcode=6 codex=4
Real CLI loop (4-ticket, 3-section change, capacity 1–3) completes; tasks.md written back, assignment worktrees cleaned, main untouched
Conflict attribution (2 parallel assignments, 1 real conflict) exactly one resolve_merge, the clean assignment stays leased with no failure code, both succeed after the single conflict is resolved

Migration notes (also in CHANGELOG): a packet prepared by 0.3.1 carries the removed resources field, so its task-packet hash no longer matches — an attempt mid-flight across this upgrade is reported as receipt-invalid: task packet hash mismatch; abandon it (or let the next actions retry) and it re-dispatches with a fresh packet. plan.json no longer carries an assignments array, and status reports revision: 0 until a run records one.

Source: https://github.com/Lumos-789/baton/tree/6190d61

@Lumos-789 Lumos-789 changed the title feat: add baton (ticket/DAG subagent control plane) feat(baton): add the ticket/DAG subagent control plane plugin (0.3.2) Nov 5, 2026
@Lumos-789

Copy link
Copy Markdown
Author

Re-pushed this branch as 97419739 and fixed two classes of defect that were ours, not the repository's. Nothing about the plugin's behaviour changed.

1. The PR now follows .github/PULL_REQUEST_TEMPLATE.md. The body had drifted into hand-written prose and lost the two parts a reviewer checks first: the Plugin type declaration and the Self-check list. Both are back, the five sections CONTRIBUTING asks for are filled in (problem / visible behaviour and testing / version and marketplace registration / dependencies and side effects / licensing), and the title carries the (<plugin-name>) scope the template requires for the squash commit on main.

2. The entry named the repo-root assets/baton/icon.png, but that file was never uploaded with the branch — the catalogue would have shown a broken icon. It is committed now (512×512) and the entry uses the same absolute CDN form as every other icon-bearing entry in marketplace.json.

3. The package file list no longer drops documentation. package.json's files list carried README.md but neither README_CN.md nor CHANGELOG.md, so the payload shipped an English-only plugin. Both are in the list now (fixed in the source repository, commit 14438b0) and the payload carries 33 files instead of 31.

Re-verified against the branch itself (not a local checkout):

check result
python3 scripts/validate.py @ 97419739 OK: 53 plugin(s) validated
plugin artifact via build_zip() — the same code path CI runs 100972 B, sha256 932b820fec201b6969e5e277c035ebdc47ed635e5dba4d67fee6e3ee33d74e81
assets/baton/icon.png present, 512×512
README.md + README_CN.md in the payload both present

Request: the earlier validate status was bound to abfbb188, so it is stale after this push. Could you re-run the external validation on 97419739? The full build_dist.py still needs the repository's UI-plugin pnpm build and fails identically on main (excalidraw: incomplete build; run pnpm build first), which is why I am relying on CI for that step.

中文摘要:本轮只补元数据与载荷完整性 —— 恢复了模板要求的插件类型声明与自查清单、标题加上 (插件名) scope(模板说明该标题会成为 main 上的 squash commit)、把条目引用的根级图标真正提交并以 CDN 绝对地址引用。分支上 validate.py 通过,插件产物按 CI 同一代码路径打包成功(体积与 sha256 见上表)。旧的 validate 状态绑定在 abfbb188 上已失效,请在 97419739 上重跑一次校验。

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants