@staix/agent-hub 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +2 -2
  3. package/docs/agent-notes/README.md +5 -0
  4. package/docs/agent-notes/adapters.md +20 -5
  5. package/docs/agent-notes/benchmarks.md +1 -0
  6. package/docs/agent-notes/daemon.md +5 -3
  7. package/docs/agent-notes/local-worker.md +1 -0
  8. package/docs/agent-notes/tasks.md +9 -3
  9. package/docs/agent-notes/tests.md +6 -3
  10. package/docs/dogfood.md +21 -4
  11. package/docs/memory.md +1 -1
  12. package/docs/operations.md +150 -34
  13. package/docs/profiles.md +36 -0
  14. package/docs/quickstart.md +14 -4
  15. package/docs/roles.md +1 -0
  16. package/docs/sdlc.md +39 -13
  17. package/docs/security.md +41 -9
  18. package/docs/smoke.md +51 -3
  19. package/docs/specs/2026-09-19-agent-hub-design.md +56 -10
  20. package/docs/specs/2026-09-20-upgrade-recovery-design.md +3 -1
  21. package/docs/verification/2026-10-25-0.17.0.md +74 -0
  22. package/docs/verification/2026-10-25-0.18.0.md +76 -0
  23. package/docs/verification/2026-10-25-codex-0164-reviewer-tool-call.md +52 -0
  24. package/docs/verification/2026-10-25-codex-review-live-verdict.md +35 -0
  25. package/docs/verification/2026-10-25-kimi-231-reviewer-compat.md +48 -0
  26. package/docs/verification/2026-10-25-kimi-usage-ac11.md +48 -0
  27. package/docs/verification/2026-10-25-qwen-dual-output-hub-e2e.md +23 -0
  28. package/docs/verification/2026-10-25-qwen-dual-output-t0.md +20 -0
  29. package/docs/verification/2026-10-25-remote-docker-transport.md +46 -0
  30. package/docs/verification/2026-10-25-reviewer-no-verdict-diagnostics.md +35 -0
  31. package/docs/verification/profiles/YYYY-MM-DD-general.md +46 -0
  32. package/docs/verification/profiles/YYYY-MM-DD-research.md +61 -0
  33. package/docs/verification/profiles/YYYY-MM-DD-writing.md +46 -0
  34. package/docs/verified.json +28 -19
  35. package/package.json +2 -2
  36. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  37. package/plugins/agent-hub/server.js +1115 -192
  38. package/src/adapters/codex-review-isolation.ts +10 -1
  39. package/src/adapters/compat.ts +7 -2
  40. package/src/adapters/conformance/enrollment.ts +18 -4
  41. package/src/adapters/conformance/factories.ts +4 -1
  42. package/src/adapters/conformance/kit.ts +11 -0
  43. package/src/adapters/definitions/builtin-metadata.ts +1 -1
  44. package/src/adapters/definitions/native-cli-entry-qwen.ts +32 -4
  45. package/src/adapters/definitions/native-cli-preview-qwen.ts +9 -1
  46. package/src/adapters/definitions/native-factories.ts +8 -0
  47. package/src/adapters/definitions/native-recovery.ts +8 -0
  48. package/src/adapters/definitions/qwen.ts +7 -4
  49. package/src/adapters/local-worker.ts +5 -3
  50. package/src/adapters/qwen-dual.ts +578 -0
  51. package/src/adapters/registry-types.ts +5 -1
  52. package/src/adapters/registry.ts +9 -1
  53. package/src/adapters/reviewer-mcp.ts +12 -6
  54. package/src/adapters/reviewer-print.ts +100 -17
  55. package/src/cli/console-state.ts +9 -2
  56. package/src/cli/help.ts +29 -28
  57. package/src/cli/identity.ts +87 -6
  58. package/src/cli/main.ts +190 -69
  59. package/src/cli/output.ts +7 -2
  60. package/src/cli/sdlc-output.ts +242 -0
  61. package/src/cli/task-output.ts +212 -0
  62. package/src/cli/upgrade-output.ts +57 -0
  63. package/src/cli/upgrade.ts +4 -3
  64. package/src/hub/board.ts +17 -0
  65. package/src/hub/check-runner.ts +252 -22
  66. package/src/hub/conductor.ts +5 -0
  67. package/src/hub/config-apply.ts +16 -7
  68. package/src/hub/config-edit-host.ts +1 -1
  69. package/src/hub/config-tier.ts +1 -1
  70. package/src/hub/daemon.ts +487 -94
  71. package/src/hub/dogfood-profile.ts +248 -0
  72. package/src/hub/dogfood-read.ts +77 -14
  73. package/src/hub/dogfood.ts +5 -1
  74. package/src/hub/events.ts +1 -1
  75. package/src/hub/host-executable.ts +2 -1
  76. package/src/hub/hub-tools.ts +2 -2
  77. package/src/hub/memory-plan-guard.ts +8 -33
  78. package/src/hub/operational-authority.ts +60 -0
  79. package/src/hub/operational-view.ts +141 -0
  80. package/src/hub/orphan-process.ts +69 -0
  81. package/src/hub/permission-mode.ts +1 -1
  82. package/src/hub/read-only-shell-proof.ts +80 -0
  83. package/src/hub/read-only-shell.ts +162 -0
  84. package/src/hub/reviewer-pool-config.ts +3 -2
  85. package/src/hub/reviewer-pool.ts +3 -0
  86. package/src/hub/routing.ts +23 -6
  87. package/src/hub/sdlc-merge.ts +299 -0
  88. package/src/hub/sdlc-mirror.ts +129 -0
  89. package/src/hub/sdlc.ts +58 -12
  90. package/src/hub/settings.ts +11 -5
  91. package/src/hub/snapshots.ts +60 -0
  92. package/src/hub/task-repository.ts +31 -5
  93. package/src/hub/tasks.ts +266 -28
  94. package/src/local/tools.ts +17 -4
  95. package/src/memory/brief.ts +45 -5
  96. package/src/ui/index.html +2 -1
package/docs/sdlc.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Authority and evidence
4
4
 
5
- The issue remains the specification, Tasks remains the sole task mutation boundary, and GitHub remains authoritative for PR heads, required checks, merge commits and releases. The hub stores artifact identities and bounded structured readbacks, never issue/PR bodies. Off is the default. Advise exposes blockers without changing task transitions. Enforce blocks only the review/approval transitions listed below; it never merges, releases, edits GitHub or gives a peer additional capabilities.
5
+ The issue remains the specification, Tasks remains the sole task mutation boundary, and GitHub remains authoritative for PR heads, required checks, merge commits and releases. The hub stores artifact identities and bounded structured readbacks, never issue/PR bodies. Off is the default. Advise exposes blockers without changing task transitions. Enforce blocks only the review/approval transitions listed below; it never releases, edits GitHub or gives a peer additional capabilities. The one GitHub write is the guarded merge of an approved task (#786, below), off by default.
6
6
 
7
7
  A reviewed head is a full lowercase 40-character SHA observed through a successful readback, not an implementer-supplied statement. A check is evidence only for that same head. Unknown/missing required checks, unavailable GitHub, pending reviews or unresolved threads remain blockers in enforce and visible unknowns in advise. A newer head invalidates the derived review and approval readiness. Historical task approval is retained as history, not presented as approval of the new commit. Revalidate after asynchronous readbacks immediately before any board write.
8
8
 
@@ -10,17 +10,17 @@ A reviewed head is a full lowercase 40-character SHA observed through a successf
10
10
 
11
11
  The built-in lifecycle follows the [AI-Native SDLC Playbook](https://claude.com/resources/articles/the-ai-native-sdlc-playbook). Stage ids describe the hub's artifacts and handoffs; the playbook labels group them into Plan, Design, Build, Test, Deploy and Maintain.
12
12
 
13
- | Stage id | Playbook stage | Acting role | Artifact or exit gate | Current enforcement |
14
- | --- | --- | --- | --- | --- |
15
- | intent | Plan | person/planner | intent or backlog identity | person spec acceptance required at build entry in enforce |
16
- | spec | Design | person/planner | issue identity and scope | spec acceptance/body drift checked at build entry |
17
- | plan | Build | planner/owner | plan, branch, existing predecessor gates | current planner review required at build admission; literal-path conformance gates approval in enforce |
18
- | build | Build | owner | PR identity/current head and local check on that head | entering review, when enabled |
19
- | test | Test | verifier/reviewer | required checks and local gate on the reviewed head | approval, when enabled; bug test integrity below, evals pending S5 |
20
- | review | Deploy | assigned reviewer | current head-bound verdict, no pending change request/open thread; AC checklist under Compliance | approval, when enabled; open Important findings block in enforce |
21
- | merge | Deploy | person | current-head readiness, ordered touching PRs | shown; no mutation |
22
- | release | Deploy | person | merged commit, version/tag, docs freshness/live receipt checklist | person authorization and optional tag hook below |
23
- | maintain | Maintain | person/planner | follow-up intent and operational evidence | shown; metrics and control bands from S7 (`ahub sdlc metrics`) |
13
+ | Stage id | Playbook stage | Acting role | Artifact or exit gate | Current enforcement | Class (#784) |
14
+ | --- | --- | --- | --- | --- | --- |
15
+ | intent | Plan | person/planner | intent or backlog identity | person spec acceptance required at build entry in enforce | person-only (acceptance) |
16
+ | spec | Design | person/planner | issue identity and scope | spec acceptance/body drift checked at build entry | person-only (acceptance) |
17
+ | plan | Build | planner/owner | plan, branch, existing predecessor gates | current planner review required at build admission; literal-path conformance gates approval in enforce | agent-executable |
18
+ | build | Build | owner | PR identity/current head and local check on that head | entering review, when enabled | agent-executable |
19
+ | test | Test | verifier/reviewer | required checks and local gate on the reviewed head | approval, when enabled; bug test integrity below, evals pending S5 | agent-executable |
20
+ | review | Deploy | assigned reviewer | current head-bound verdict, no pending change request/open thread; AC checklist under Compliance | approval, when enabled; open Important findings block in enforce | agent-executable |
21
+ | merge | Deploy | person | current-head readiness, ordered touching PRs | shown; the guarded merge below runs only when `auto_merge` is `on` (default off) | agent-executable where the project's AGENTS.md delegates it to the approving reviewer (this repository does, #784); the hub still shows the person as actor |
22
+ | release | Deploy | person | merged commit, version/tag, docs freshness/live receipt checklist | person authorization and optional tag hook below | agent-evidence / person-verdict (consent) |
23
+ | maintain | Maintain | person/planner | follow-up intent and operational evidence | shown; metrics and control bands from S7 (`ahub sdlc metrics`) | agent-executable |
24
24
 
25
25
  Stored `implement` and `verify` stage ids read as `build` and `test`. A write that names the old id stores the id the configuration lists, so new enabled-lifecycle receipts use the canonical ids. The default remains off, preserving existing off-mode output, receipt and schema shapes. Status, explain, console and dashboard name the playbook stage beside enabled lifecycle ids.
26
26
 
@@ -54,7 +54,7 @@ A reviewer records the head it actually reviewed. If GitHub no longer reports th
54
54
 
55
55
  ## Readbacks and presentation
56
56
 
57
- Use structured gh JSON/GraphQL read-only calls with validated repository and numeric artifact identities. No shell interpolation, PR/issue body fetching, token logging or unbounded comment storage. Failure yields unknown. Required checks come from repository rules or explicitly configured names; an empty/unknown set is not silently green. Review-thread status is counted, not copied. Cancellation and task-owner generation fencing prevent a late read from approving reassigned work.
57
+ Use structured gh JSON/GraphQL read-only calls (the guarded merge below is the one exception) with validated repository and numeric artifact identities. No shell interpolation, PR/issue body fetching, token logging or unbounded comment storage. Failure yields unknown. Required checks come from repository rules or explicitly configured names; an empty/unknown set is not silently green. Review-thread status is counted, not copied. Cancellation and task-owner generation fencing prevent a late read from approving reassigned work.
58
58
 
59
59
  ahub sdlc status and explain expose mode, stage, gate, observed/reviewed/local-check heads, next actor and next action. Console/dashboard derive the same result. Merge ordering compares exact PR path sets, reports intersections and never guesses an automatic landing order where dependencies are unknown. Release readiness lists merged work since the tag, stale/nonancestor documentation stamps and open native/live receipts; each unknown remains visible.
60
60
 
@@ -164,6 +164,30 @@ The package-owned scripts/check-docs.mjs accepts --json with the same normal/--r
164
164
 
165
165
  The release reader additionally requests --committed: manifest, README and package metadata come from the captured HEAD, and dirty documentation/evidence files block freshness. A working-tree restamp cannot certify a commit that still contains stale stamps. The ordinary checker continues to support working-tree review before those stamps are committed.
166
166
 
167
+ # Guarded merge (#786)
168
+
169
+ `.agenthub/sdlc.toml` takes `auto_merge = "off" | "dry-run" | "on"` (default `off`) and optional `[repos."owner/name"]` tables with `required_checks` and `auto_merge`. A table's mode overrides the top-level one for that repository; its `required_checks` come first, then the top-level list, so a repository without branch protection is gated by what this file names (an empty set refuses: `required checks unknown`). Unknown keys are refused with a field path. The file is read once at start.
170
+
171
+ Two triggers, neither blocking: an approving `hub_review` at the PR's current head (the check runs after the verdict is saved and reports through a notice; a failing or throwing check never changes the verdict), and `ahub sdlc merge <task> [--dry-run]` (the person or the one configured conductor; a daemon control op that always replies). Right before acting the hub re-reads the PR (`readSdlcPr`) and the queue (`readMergeQueue`) and refuses, naming every failing gate: task not approved, head changed since the approved head, a required check failing, pending or missing, a pending change request, pending or unknown review requests, an unresolved or unknown thread, an open Important finding, a draft, a PR not first among its touching PRs or a dependency that is not approved, a release PR (`release PR: person consent required`), a private task, or unreadable evidence. Unknown is never a pass. The decision judges every gate on three readings (the PR, the queue's own read, then the PR once more, last) and refuses what any of them fails, so a check, review request, thread or draft that changes between reads at the same head is caught; `--match-head-commit` pins only the head. The board-side inputs are read again, synchronously, in the same stretch as the `gh pr merge` call (nothing is awaited between the read and the call) and compared with what the decision judged: the task's state, repo, PR and reviewed head, its privacy, every dependency's state and the dependency edges, open Important findings on the head, the release stage, the merge mode and required checks in force, and the asker's authority and the hub's availability. Any difference refuses and runs nothing. The complete local queue inputs (all task links, dependency edges, receipts, plans, findings, privacy and SDLC configuration) must also match their pre-read fingerprint in a synchronous check after the fresh gates and argv validation, immediately before dispatch. The caller must still be the person, the hub approval trigger or the currently configured conductor at that final check. Any changed projection refuses, conservatively including an unrelated task projection change; a new invocation reads the queue again.
172
+
173
+ `on` runs exactly `gh pr merge <pr> --repo <repo> --squash --match-head-commit <head>` (a closed argv shape checked right before the call) and reads the PR back: only GitHub reporting `MERGED` is a merge, whatever the exit code. Never `--admin`, never a ref update used to perform the merge, and no `--auto` while the repository does not allow auto-merge (a person's repository setting). `dry-run` reports the same decision and the argv it would run and runs nothing; `off` reads nothing. Logs and notices name the task number and gates only. The verdict mirror is described in the next section.
174
+
175
+ ## Merge result and cleanup (#786 T2)
176
+
177
+ Post-run reads make at most three attempts within a shared six-second budget. Only a GitHub MERGED observation with its merge commit permits cleanup; the CLI exit is recorded separately, including a nonzero exit when GitHub confirms the merge. Non-merged, missing or unreadable evidence records the bounded result and leaves branches alone. Off and dry-run create no merge record or cleanup effects.
178
+
179
+ Tasks stores host-only `sdlc.merge` evidence bound to the repository, PR and head. Lifecycle tools cannot submit this field, and artifact changes invalidate it. A change during an awaited operation leaves the previous attempt in history without overwriting the replacement artifact. Durable cleanup and owner-notice claims precede those effects. Merged or uncertain attempts are not automatically repeated after restart; a positively observed not-merged result can be retried by an explicit command. A crash before the first post-run record remains an untracked attempt requiring inspection.
180
+
181
+ Remote cleanup reads the merged PR head, repository default, legacy protection and active rulesets. It skips missing, fork, default, protected, moved or unsupported refs. Its only ref write is a single GraphQL compare-and-swap deletion: `beforeOid` is the merged PR head and `afterOid` is zero. A moved head refuses deletion; unknown outcomes stay explicit. The hub sends the owner a public-view notice to remove only its task worktree and local branch when safe. The hub runs no local branch or worktree deletion.
182
+
183
+ ## Verdict mirror (#786)
184
+
185
+ Each `hub_review` verdict is mirrored as ONE `COMMENT` review on the head it was given for, so a person reading the PR sees the hub's verdict. One GitHub account authors and merges every PR and GitHub refuses `APPROVE` and `REQUEST_CHANGES` on your own PR, so a comment is the only review the hub can leave; it is a record, never a gate, and nothing reads it back as approval. The call is exactly `gh api repos/<repo>/pulls/<pr>/reviews -f event=COMMENT -f commit_id=<head> -f body=<text>`, a closed shape checked right before it runs; the hub makes no other review write. The first line of the body is `agent-hub verdict: approved|changes_requested (task #N, reviewer <peer>)`.
186
+
187
+ It follows the same `auto_merge` setting as the guarded merge, per repository: `off` posts and reads nothing, `dry-run` reports the call it would make in a notice, `on` posts. It runs after the verdict is saved, is never awaited by the verdict and never fails it (a failure is a notice naming the task number). A verdict at a head the PR is no longer at posts nothing, a closed PR gets nothing (the head and state are read once more after the review list, last, so a push or a close during that read is caught; GitHub offers no compare-and-set on a review, so a push landing between that read and the post is not, and the comment still names its commit), and unknown evidence (an unreadable PR or review list, or a full page of 100 reviews) posts nothing. At most one mirror exists per (task, head, verdict): the hub reads the PR's reviews for its own headline at that head before posting, so a retried verdict or a restart does not post twice. For an approval the comment is posted before the guarded merge starts reading.
188
+
189
+ The body after the headline is the note's public view: a private task, a note the review screen withheld, a note given in a private turn and a note matching a private pattern are mirrored as the headline alone. The note is bounded, control characters are stripped, and a line that looks like a verdict headline or an agent-hub message header is neutralised so a note cannot forge a second mirror.
190
+
167
191
  # Read-only merge queue contract (#257)
168
192
 
169
193
  Queue reads fetch PR numbers and bounded head/check/review/thread/file-count metadata, never remote titles or bodies. Up to16 open PRs and8 external dependency reads share a20second budget. Every linked local receipt must satisfy the existing approval floor on that current head; draft/unknown status, missing file coverage and unlinked/private work remain blocked. Same-target path intersections return counts without names. Task after edges provide the only dependency order; open predecessor PRs block current readiness, unresolved touching order asks the person, cycles/truncation remain incomplete. A board approval never substitutes for a merged-commit receipt. No queue operation changes dependencies, owns work, merges or writes GitHub.
@@ -251,6 +275,8 @@ The planner can provide scope with `hub_plan_review`; the person can run `ahub s
251
275
 
252
276
  A review request is premature if its task head does not match the PR head, configured required checks are not green, or a pending bot thread is unanswered. A resolved thread is settled; an unresolved one needs a user reply after its last bot comment. The hub reads identity, timestamps, authorship type and resolution only, never comment bodies. Without explicit required checks the native floor is both OS checks plus `seeded guards`. Advise reports the gap; enforce refuses before the requesting done/cohort writes. If only quiet task-history events arrive during an advise readback, the evidence is discarded and review continues with unknown readiness, without another external read. A reviewer change is allowed because the reviewer is re-derived before the write; it is not an authority or readiness receipt. Mode, owner, privacy, receipt, authority and other history drift still refuse; enforce remains unchanged. A private turn may finish its own PII task in advise with a generic unknown-readiness notice and no remote metadata read; the private-turn refusal for ordinary work remains. Delayed checks revalidate before completing; a refused completion sends a private-aware notice to call `hub_task_done` again only while the task is still in progress with the owner that started the check. Otherwise only the console notice is recorded. Background roster/quota role changes remain fact, but enforce holds a new review envelope without an admission readback; refresh it through a manual reviewer assignment after CI and replies are ready. The brief and verdict coverage belong to #520, separate from this admission.
253
277
 
278
+ **Evidence in the review request (#824).** A fresh reviewer cannot run tests or read `gh`, so the request carries the hub's own reading of the admission it already made, for every reviewer: `Hub-verified at <head>: required checks <names> passed at this exact head; no bot review thread is waiting for a reply` (plus `via identical-tree run <n>` when the hub reused one), or in advise mode `Hub-verified at <head>: not ready: <blockers>`. No admission (lifecycle off) and a PII task get no block, and no new `gh` call is made for it: it renders what `reviewRequestAdmission` read. `ReviewReadiness.checks` names the required checks that were judged. When the admission is ready and the hub ran no check of its own, the checklist's check line says CI passed at this exact head and that test execution or CI evidence is not an unmet item (name a specific missing test or behaviour instead); otherwise the old wording stays. The text comes only from the hub's reads and recorded `check passed` events, never from the builder's summary or a `hub_sdlc_record` claim, and a review-only reviewer still gets no builder summary (#223 R2).
279
+
254
280
  CI controllers are serialized per pull request (`check-pr-<number>`) and for main pushes and ordinary dispatches (`check-main`), for the entire workflow and without canceling an admitted controller (#609); macOS timing dispatches use their own macos-timing group. Within a PR `queue: single` lets a newer pending run replace the older pending one; main keeps `queue: max`, which preserves up to 100 waiting controllers ([GitHub concurrency documentation](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency)). Different PRs run side by side: a repository-wide lock serialized every PR (17-18 min alone, 40-54 min with 4-5 PRs in flight on 2026-10-20). Two groups admitting the same tree at the same instant can run it twice; that duplicate costs one run and never a false green. Required job names and read-only token permissions are unchanged. Before PR or push gates, `scripts/ci-reuse.mjs` reuses successful full proof for the identical Git tree, including its workflow. On reuse the two OS matrix jobs still expand and report their exact required names, while the seeded aggregate reports its exact name. They run only a `Reuse verified identical-tree full gate` marker step naming the original proof; full setup and gates are skipped. Skipped jobs and successful reuse-reporting jobs never count as fresh full proof. A running identical-tree gate refuses duplicate admission; unknown active coverage refuses safely. Only a 404, a timeout on the individual read limit, or malformed successful proof is dropped: a failed successful-run list drops that proof phase, while a failed commit or jobs read drops only that candidate and continues the bounded search. Other failures (5xx, 403, 429, response size bounds, authentication and unclassified errors) remain unknown. Exhausting the overall deadline or request budget stays unknown even when a request reports a timeout. Without verified proof, a complete empty active scan requires a fresh gate. An unreadable, truncated or unidentifiable active inventory, or exhaustion of the shared request budget or deadline, stays unknown and refuses admission. The release reader (`--require`) still refuses without verified identical-tree full proof. Metadata reads are bounded to 100 recent successes/active runs and 240 calls within 60 seconds. Pending serialized controllers are not admitted gates. Run focused tests and `scripts/check.sh --changed` locally; CI carries the full-suite gate; a known flake warrants only one rerun of its failed job with recorded evidence.
255
281
 
256
282
  `hub_sdlc_costs` and the person's `ahub sdlc costs [owner/repo]` report linked merged PRs without changing the board. A review round is a board done-to-verdict handoff, not each reviewer in a multi-reviewer round. An observed handoff still counts when its declared scope was none. The report counts distinct check-workflow run ids, extra attempts (`run_attempt - 1`), full re-reviews, delta rounds and summed review intervals, with GitHub review counts alongside them. Unspecified scope and unavailable evidence stay unknown/incomplete. Only current public links are projected; a private counterpart withholds the whole PR. Up to 16 linked PR reads share 20 seconds; each PR covers at most 100 commits, reviews and recent check-workflow runs, and the pure aggregator bounds 500 tasks, 200 PRs and 10,000 history entries. Truncation is explicit. S10 exposes pure band-breach proposal data; S7 owns rolling baselines, band configuration, automatic scheduling and board-card creation.
package/docs/security.md CHANGED
@@ -7,7 +7,7 @@ agent-hub connects agents that can each run commands. This page says what the hu
7
7
  - **Other agents' text is untrusted.** Every message that crosses from one peer to another is framed as untrusted input: a channel tag with `meta.source` for Claude started with `ahub claude`; otherwise a fixed header line (`frame()`, `[agent-hub message from ...]`) with an instruction that says it is untrusted, the plugin's server instructions for a tools-only Claude reading `hub_inbox` and a standing instruction for the others. A message body cannot forge the hub's own headers: such lines are quoted (`sanitize`). Replies inherit a hop count capped at 3, so agents cannot ping-pong forever; neither a digest nor a steer can reset it.
8
8
  - **The control link is loopback plus a secret.** The daemon and the Codex proxy bind 127.0.0.1 only. The control WebSocket requires a per-run token (`.agenthub/state/control-token`, mode 600), and both servers refuse any request that carries an `Origin` header: any web page can open a WebSocket to localhost, and browsers always send `Origin`. The bearer header a Codex TUI sends to the proxy carries only the terminal launch id (#586): it binds terminal ownership and is not a credential. External clients cannot claim the console user's id or a hub-managed peer's id.
9
9
  - **Permission prompts stay on unless an operator selects a permission mode.** `ask` leaves native decisions unchanged. `ask-when-needed` grants scoped Claude file tools or Pi/local writes and edits; Kimi and Codex use their own native policies. `never-ask` requires explicit runtime confirmation or a person's console `y` for a config default at hub start. Until that config confirmation the peer runs in `ask`. Kimi's and `local`'s permission requests are relayed to the console and cancelled after `approvals.timeout_s` (default 120 s) of silence; the macOS notification for a waiting request carries the peer and the tool name only. The one exception is the hub's own tools (`hub_send` and the task tools, matched by exact name): Kimi's requests for them are approved once without a prompt and logged by name, the same trust Codex gets through `approval_mode` in the hub's config. They invoke hub-owned operations rather than arbitrary file or shell tools, and every call passes the hub's own checks. A call the always-ask policy classes (below) is never approved this way, whatever its title. Identity comes from the exact permission title, or from the earlier tool-call title bound to the same call id and resolved against the configured MCP servers when the permission title contains argument JSON (Qwen). Payload text and unrelated display titles never establish identity. Configured ACP kinds (#451) never get this exception: a request whose title spells a hub tool exactly still goes to the person approval path, and only a mapped native mode changes what the agent itself asks. For ACP agents, a payload longer than the console shows is marked as cut and never offers a session-wide grant. Pi's write, edit, shell and git write requests are relayed the same way and can be answered `Always allow <tool> until Pi restarts`: later calls of that tool by the same Pi start skip the prompt but stay inside the path guard and sandbox. The grant covers every later call of that tool whatever its payload, is kept in memory only, cannot be given from the dashboard, and each call it allows is logged by tool name. A Pi or `local` request whose call was cancelled or whose turn ended is withdrawn at once: a later answer is refused and grants nothing, an `Always allow` included. One shell command runs without a hub approval: the `!command` a person types in the managed Pi TUI. It stays inside the same sandbox and denylist, and the hub takes it only from the verified owner process of a Pi in TUI mode, while Pi is idle, on a reservation that is used once; a model's own bash call never takes that path (issue #253). `--unattended` turns prompts off, says so loudly, and is never the default; a classed call from a hub-owned ACP peer, the local worker or Pi still waits for a person under it.
10
- - **A committed config cannot choose launch commands, credential files, data endpoints, permission modes or a wider sandbox.** The machine-local fields (`kimi_cmd`, `permission_modes`, `codex_bin`, `pi.cmd`, `checks`, `mlx.bin`, `mlx.runtimeDir`, `mlx.modelPath`, `omniroute.urls`, `omniroute.access_hosts`, the `omniroute` key files, `memory.worker_url`, `local.read_allow`, `local.bash_network`, `local.network_allow`, `terminal.open`, `instances`, `action_policy`) apply only from a config file git confirms nobody committed (`instances` and `action_policy` are read only from an untracked `.agenthub/config.local.json`; in `config.json` they are ignored even when untracked): `.agenthub/config.json` or `.agenthub/config.local.json`, matched by file identity so no other spelling the file system accepts slips past, and `.agenthub` itself not a committed symlink or submodule. Without a repository, or when git fails, they keep their defaults; an empty value always means the default. "Untracked" is answered by the repository that contains the project: a checkout copied or extracted into an unrelated repository, or into an ignored directory of one, is trusted like your own files. So a cloned repository cannot choose a launch command, a completion check, a gateway to send a key file to, a memory endpoint, a permission mode, the command that opens a terminal, or a wider sandbox. A command in `checks` runs as you, outside the local worker's sandbox, like a git hook. So do the `setup` and `verify` commands of a benchmark suite (issue #251): `ahub bench run <suite.json>` is a person's command, refused from an agent session, and it resets the work tree for each attempt, so it runs only with `bench.enabled`, at the root of a clean repository that tracks nothing under `.agenthub/`. Nothing in `routing.toml` or in task text is ever run. `routing.toml` and the other shared fields still come from the checkout, and they matter: `routing.toml` picks the models the local worker and the hub's inference use at your gateway and can turn the PII constraint off, and roles and budget shape who does what. Review them in a repository you do not trust. The hub's own git calls outside the sandbox (snapshots, facts, config trust, project discovery, memory recall, the bench runner) all use the single hardened implementation in the exact-pinned first-party `@staix/agent-mem/platform` package, re-exported by `src/hub/git.ts`, which disables the programs a repository's configuration can name — the file-system monitor, hooks, external diff, the pager, and the credential and askpass helpers, with prompts failing closed — so a config that ever does get written cannot run code with the hub's rights. Of those, the monitor, hooks and external diff are reachable by commands the hub issues (the planted-program test exercises all three; a `diff --no-index` reads the repo's config, so that call also passes `--no-ext-diff` and `--no-textconv`); the pager is not (every call pipes stdout). Credential and askpass helpers are disabled for project repositories; memory sync alone may use credentials for its own memory repository, without prompts. The cost: `core.fsmonitor=false` also turns off a person's enabled built-in monitor for the hub's own calls, so the per-turn snapshot's `add -A` scans the whole tree (the snapshot Git calls of one burst share a 250 ms budget, each call at most 10 s; a boundary that runs out is recorded as unknown, never as a stale baseline) (#576). git has no global switch for filter drivers, and of the hub's commands only the snapshot's `add`/`restore` would run one (its driver command lives in the protected `.git/config`).
10
+ - **A committed config cannot choose launch commands, credential files, data endpoints, permission modes or a wider sandbox.** The machine-local fields (`kimi_cmd`, `permission_modes`, `codex_bin`, `pi.cmd`, `checks`, `mlx.bin`, `mlx.runtimeDir`, `mlx.modelPath`, `omniroute.urls`, `omniroute.access_hosts`, the `omniroute` key files, `memory.worker_url` (still accepted, ignored since 0.16.0), `local.read_allow`, `local.bash_network`, `local.network_allow`, `terminal.open`, `instances`, `action_policy`) apply only from a config file git confirms nobody committed (`instances` and `action_policy` are read only from an untracked `.agenthub/config.local.json`; in `config.json` they are ignored even when untracked): `.agenthub/config.json` or `.agenthub/config.local.json`, matched by file identity so no other spelling the file system accepts slips past, and `.agenthub` itself not a committed symlink or submodule. Without a repository, or when git fails, they keep their defaults; an empty value always means the default. "Untracked" is answered by the repository that contains the project: a checkout copied or extracted into an unrelated repository, or into an ignored directory of one, is trusted like your own files. So a cloned repository cannot choose a launch command, a completion check, a gateway to send a key file to, a memory endpoint, a permission mode, the command that opens a terminal, or a wider sandbox. A command in `checks` runs as you, outside the local worker's sandbox, like a git hook. So do the `setup` and `verify` commands of a benchmark suite (issue #251): `ahub bench run <suite.json>` is a person's command, refused from an agent session, and it resets the work tree for each attempt, so it runs only with `bench.enabled`, at the root of a clean repository that tracks nothing under `.agenthub/`. Nothing in `routing.toml` or in task text is ever run. `routing.toml` and the other shared fields still come from the checkout, and they matter: `routing.toml` picks the models the local worker and the hub's inference use at your gateway and can turn the PII constraint off, and roles and budget shape who does what. Review them in a repository you do not trust. The hub's own git calls outside the sandbox (snapshots, facts, config trust, project discovery, memory recall, the bench runner) all use the single hardened implementation in the exact-pinned first-party `@staix/agent-mem/platform` package, re-exported by `src/hub/git.ts`, which disables the programs a repository's configuration can name — the file-system monitor, hooks, external diff, the pager, and the credential and askpass helpers, with prompts failing closed — so a config that ever does get written cannot run code with the hub's rights. Of those, the monitor, hooks and external diff are reachable by commands the hub issues (the planted-program test exercises all three; a `diff --no-index` reads the repo's config, so that call also passes `--no-ext-diff` and `--no-textconv`); the pager is not (every call pipes stdout). Credential and askpass helpers are disabled for project repositories; memory sync alone may use credentials for its own memory repository, without prompts. The cost: `core.fsmonitor=false` also turns off a person's enabled built-in monitor for the hub's own calls, so the per-turn snapshot's `add -A` scans the whole tree (the snapshot Git calls of one burst share a 250 ms budget, each call at most 10 s; a boundary that runs out is recorded as unknown, never as a stale baseline) (#576). git has no global switch for filter drivers, and of the hub's commands only the snapshot's `add`/`restore` would run one (its driver command lives in the protected `.git/config`).
11
11
  - **Telemetry holds no bodies.** `.agenthub/state/events.jsonl` (issue #40) records envelope ids, routing and sizes, task ids and states, overlapping paths and token counts, and the task id (with a PII flag) that each usage and token record is attributed to. It never records a message body, a task title or detail, and marks private (PII) envelopes and tasks as such. Budget pauses, resumes and quota resets carry the peer, window, times and the hub's own reason text, and a person's release authorization its commit, version and docs digest. The usage roll-up (#683) folds the usage, token and limit records into `hub.db` and joins issue, PR and release from the board when read; `ahub usage` and the console's Usage panel answer only the console and a configured planner, and a planner not during a private turn. Kimi's token records (#285) are counted from its own session wire log under its home: only the four token integers, the model label and the row time are copied, and the record id is an opaque hash, never a path, session id or text. The log stays on the machine; `ahub export` only prints it. Two opt-in stores copy numbers from it into your home directory, shared by every project on the machine (directories mode 700, files 600): research records (issue #247, `research.enabled`) in `~/.agenthub/research/` hold per approved task the ids, states, counts, tokens, times and PII flag the events hold, a person's label from a closed list, and a hash in place of the project id; benchmark runs (issue #251) in `~/.agenthub/bench/` hold suite name and hash, arm label, the attached peers' ids, models and permission modes, task ids, outcomes, exit codes, counts, tokens and times, never suite text or a command's output.
12
12
  - **Snapshots stay in your repository.** Per-turn snapshots (issue #33) are git objects in the project's own object store, written through a temporary index; nothing is referenced, pushed or copied elsewhere, and `git gc` prunes them. They hold what the work tree held, including untracked files that are not ignored, so keep secrets in ignored files. They carry the repository's own permissions, and nothing caps their disk use but `git gc`. A turn of a peer holding an open PII task is not snapshotted; a PII file left in the project is snapshotted by later turns like any other file. `ahub undo` restores only files whose current content is exactly what the turn left.
13
13
  - **The edit hook reads, never decides.** `ahub check-path --hook` (issue #32) reads hub.db and returns context for Claude and a line for you; it sets no permission decision, so your permission rules stay in charge. It names other owners' task ids, titles and states, which then reach Claude's model; PII tasks are left out.
@@ -23,7 +23,7 @@ agent-hub connects agents that can each run commands. This page says what the hu
23
23
 
24
24
  ## Native memory authority and profile selection
25
25
 
26
- The portable core is the exact-pinned first-party `@staix/agent-mem` 0.7.0 package; it does not replace the hub's caller identity, PII policy or task-write authority. Deny, hardened git, real paths and process identity are single-sourced in its Node-safe platform entry. Current project privacy is applied by the hub before promotion and again before model-facing reads. Standalone person reads are unscreened, and the standalone `--policy-free` acknowledgement grants no hub or peer authority.
26
+ The portable core is the exact-pinned first-party `@staix/agent-mem` 0.8.0 package; it does not replace the hub's caller identity, PII policy or task-write authority. Deny, hardened git, real paths and process identity are single-sourced in its Node-safe platform entry. Current project privacy is applied by the hub before promotion and again before model-facing reads. Standalone person reads are unscreened, and the standalone `--policy-free` acknowledgement grants no hub or peer authority.
27
27
 
28
28
  `hub_memory_search`, `hub_memory_show` and `hub_memory_graph` take the project from the daemon, require `memory_read`, refuse private turns and validate UTF-8 input before logs or storage work. Complete records and graph metadata are screened before counts and truncation. Search is at most 20 lines/4 KiB (default 8); show accepts at most 8 ids and returns at most 8 KiB; graph is at most 120 nodes/240 edges/6 KiB (default 60 nodes), with no edge lacking an endpoint. Unknown, ambiguous and withheld ids share one neutral refusal. Arguments and results are excluded from hub-managed capture; this does not govern a vendor's independent transcript or memory hooks.
29
29
 
@@ -235,9 +235,23 @@ move other than the hub's own was not made from the console (to another peer
235
235
  only with `assign` when its capabilities are listed).
236
236
  A conductor may inspect public state, assign or escalate work, start a peer in
237
237
  the start mode a person set (it cannot choose the mode) and place its own
238
- delivery holds. It cannot answer approvals,
239
- resolve durable deliveries, override budget pauses, release a human hold or stop
240
- a peer: `peer_stop` is taken from the console role only, and no hub tool sends it.
238
+ delivery holds. Through the `ahub` CLI under its own peer identity it may also
239
+ run the closed operational family of #812: `roster add|remove`, `stop <peer>`,
240
+ `queue show|resolve|discard`, `task cancel`, `doctor --orphans --kill`, `logs`,
241
+ `export` and `settings set` on twelve named live keys. Each is admitted by the
242
+ daemon after a recheck of the conductor role, audited as a closed label with no
243
+ argument text, and bounded: logs and export are public projections of the last
244
+ 100 lines and 1000 events, orphan cleanup signals only a positively identified
245
+ hub-owned process of the selected project, and a stop keeps the existing
246
+ ownership and recovery guards (`peer_stop` from a non-console connection is
247
+ admitted only through this conductor check; no hub tool sends it). It still cannot answer
248
+ approvals, override budget pauses, release a human hold, trust an agent version,
249
+ authorize a release or run recovery resume, abort and reattach: those stay with
250
+ the person. Other agents may run only the read-only forms of #812 (`doctor`,
251
+ `projects`, `models status`, `recovery status`, `queue list`, `roster`,
252
+ `settings` reads, `sdlc status|explain|queue|release`, `release show` and the
253
+ `--dry-run` upgrade and restart previews) through the public views, never the
254
+ console identity.
241
255
  Pending approval summaries exclude titles; PII tasks remain public stubs.
242
256
  Conduct events contain ids only. Supervision feeds use structured reasons and
243
257
  never carry check output or approval bodies.
@@ -346,13 +360,23 @@ Antigravity (`agy`, #485) is optional, cloud and off roster by default. The laun
346
360
 
347
361
  Configured ACP adapters (#451 A3) are declared only by the person, in a Git-confirmed untracked `.agenthub/config.local.json`. The whole declaration (command, argv, identity, roles and mode maps) is machine-local authority: a shared or tracked declaration is ignored with a notice, settings never edit it, and changes need a hub restart. The hub runs only the declared argv, never a module or callback. Argv that selects a leader, attach, connect or remote mode is refused, and a Grok declaration needs `--no-leader`. Each start requires the pinned native identity, an exact `session/load` echo on resume, and positive ownership of the spawned process and its original process group; unknown ownership refuses, and stop signals only that verified group. An agent that hands its work to a process outside that group is outside this boundary. Without a mode map native policy is unmanaged, and there is no hub sandbox either way. Configured kinds are cloud peers (never PII), headless only and off roster by default, and the hub makes no quota, context or controlled-recovery claims for them. A read-only instance needs the native read-only mode acknowledged before a prompt and is limited to read and review hub tools, both in the published MCP list and at the host; its native non-read tool requests are rejected, and no setting or runtime change can raise it. These agents' stderr and error text are withheld from logs, and previews redact the executable, argv and session id.
348
362
 
349
- External adapter modules (#451 A4) are the one way to run adapter code that ships outside the hub, and the pin is code authorization, not a sandbox. `adapter_modules` is read only from a Git-confirmed untracked `.agenthub/config.local.json` (never from a shared, tracked or cloned config, and never written by settings or a hub tool). Only daemon startup imports a module, from the captured bytes that matched the `sha256` pin, not by reopening the path; the file must sit inside the project with no symlinked component and not be world-writable. Its imports are bound to the running hub's own `@staix/agent-hub/adapter` and builtins (`bun:ffi` and `bun:test` excluded), and dynamic imports, `require`, `eval` and `Function` are refused by a conservative scan. Once imported, the module runs with the daemon's privileges: the 2 s import deadline stops waiting but does not cancel its code or undo side effects, and the host's default against hub-tool title auto-approval holds only while the module keeps the factory options it is given. Inspect the whole bundle before recording its hash. Refusal reasons withhold source and module error text. A loaded kind keeps its own family, cloud trust (never PII), headless start and off-roster default, and fixed-builtin capabilities are refused.
363
+ External adapter modules (#451 A4) are the one way to run adapter code that ships outside the hub, and the pin is code authorization, not a sandbox. `adapter_modules` is read only from a Git-confirmed untracked `.agenthub/config.local.json` (never from a shared, tracked or cloned config, and never written by settings or a hub tool). Only daemon startup imports a module, from the captured bytes that matched the `sha256` pin, not by reopening the path; the file must sit inside the project with no symlinked component and not be world-writable. Its imports are bound to the running hub's own `@staix/agent-hub/adapter` and builtins (`bun:ffi` and `bun:test` excluded), and dynamic imports, `require`, `eval` and `Function` are refused by a conservative scan. Once imported, the module runs with the daemon's privileges: the 2 s import deadline stops waiting but does not cancel its code or undo side effects, and the host's default against hub-tool title auto-approval holds only while the module keeps the factory options it is given. Inspect the whole bundle before recording its hash. Refusal reasons withhold source and module error text. A loaded kind keeps its own family, cloud trust (never PII), headless start and off-roster default, and fixed-builtin capabilities are refused. The daemon reads a native proof port (a status notice, the Codex frontend proxy, a native turn revert, a Claude launch proof) only from a fixed builtin definition, so a module product that carries one gains nothing (#790).
350
364
 
351
365
  # Transient ACP activity (#286)
352
366
 
353
367
  - Watch activity can contain file contents, thoughts and command output. It is console-role only, kept in a bounded in-memory ring and exposed only through the `watch` control request. Agent shells, conductor tools, dashboard snapshots, hub tools and memory observers cannot read it. Terminal bodies pass through the console's sanitization and wrapping helpers. Slow sockets close instead of accumulating unbounded output.
354
368
  - Private/PII turns show a single stub; the adapter latches privacy for that turn. The existing final-answer bus and native approval paths (console and dashboard) remain independent, with their existing privacy rules. Watch never adds activity to either path or any disk log.
355
- - There is no Qwen dual-output input file or TUI transport in this slice; Q2-Q6 are unverified and TUI mode stays refused.
369
+ - Qwen's dual-output TUI (#286 Part 3, [receipt](verification/2026-10-25-qwen-dual-output-t0.md)) is a separate transport, covered next.
370
+
371
+ # Qwen dual-output input file (#286 Part 3)
372
+
373
+ - **The input file is a prompt channel.** Whatever is appended to `<state dir>/qwen/qwen-dual/input.jsonl` is read by Qwen as typed input: a `submit` line is a prompt, a `confirmation_response` line answers a tool approval. The files are created 0600 in a 0700 directory owned by the person running the hub, opened without following links, refused if they are not regular files owned by that person with no group or other access, and written only by the daemon's adapter. No other peer can write them through a hub tool.
374
+ - Qwen's own write and shell tools can reach the file, but only through approval. The hub's approval path therefore refuses, without asking anyone, any Qwen tool request other than the hub's own tools whose input names the hub state directory, its real path, `.agenthub/state`, the `qwen-dual` directory or `input.jsonl`. That check is a lexical one on the request, as the managed local and Pi tools' state-dir guards are: it is not a sandbox, and a Qwen started with `--approval-mode yolo` by a person in their own terminal is outside it.
375
+ - Text another agent wrote reaches Qwen only inside a hub-framed digest. A digest that would begin with `/`, `!`, `@` or `?` (a slash command, a shell line, a file include or a help key in Qwen's prompt) is prefixed with a hub line first.
376
+ - Hub tools are auto-approved by the exact `mcp__agent-hub__<tool>` name Qwen reports for the call and never by prefix, title or argument text. Everything else asks the console, and an always-ask class call (#636) is never auto-approved. A prompt answered in the Qwen terminal withdraws the hub's pending approval: a late answer finds nothing to grant.
377
+ - Identity: the adapter attaches only a Qwen whose `session_start` states a verified version, protocol 2 and the events the hub reads, and it binds the running owner from the host's process table (the root of the process tree whose argv names this adapter's input file: Qwen 0.26.1 runs as a wrapper, a node launcher and a `cli.js` child that all name it; two separate roots is no owner), never from a message the launcher sends. The hub never signals that process and never types into its terminal; the person's terminal owns it.
378
+ - Environment: `ahub qwen --mode tui` execs Qwen in the person's own environment (Qwen needs their PATH, HOME and provider key, which the daemon never copies) with the hub's peer marker over it. Every other `AGENTHUB_*` variable but `AGENTHUB_HOME`, the other agents' vendor markers, and recovery and unattended authority are dropped, so a Qwen started from another agent's shell never carries that agent's identity, launch id or mode.
379
+ - Not defended: a person who gives Qwen yolo mode, an agent that Qwen runs and that writes the file through a path this lexical check does not recognise, and another process of the same user (the usual same-user boundary of the state directory).
356
380
 
357
381
  ## Global attention metadata (#280, phase 3)
358
382
 
@@ -455,9 +479,9 @@ A terminal Claude implementer or verifier instance (#525-5) owns write-class tas
455
479
 
456
480
  The headless Claude implementer (#525-4) runs one owned `claude -p` child per turn in its declared worktree, with `--setting-sources ""`, `--strict-mcp-config`, native permission mode `default`, the hub's managed PreToolUse hook and `--permission-prompt-tool mcp__agent-hub__hub_permission_request`; never `--bare`, `acceptEdits`, `dontAsk` or `bypassPermissions`, and `never-ask` is refused. Each task keeps its own persisted session; unscoped chat reuses one only while exactly one task is in progress, and a `hub_inbox` read refuses a batch attributed to another task. The approval path fails closed: `hub_permission_request` is accepted only from the current child's launch and generation, goes to the console's existing approval queue and `approvals.timeout_s`, and offers allow-once and deny only, with no allow-always or session grant. A timeout, a prompt failure, a disconnect, a hub that is down or refusing, or a stale child binding answers deny; a failed hook decides nothing, so the call still waits for a person. The approval title is the tool name and a JSON rendering of the arguments passed through `sanitize()` and the public-text screen (any PII, secret or private-path match withholds the whole value as `[withheld]`, before any cut), never task text, cut at 600 characters with a `[cut, N chars]` marker; the original arguments stay with the child and are not logged. The MCP wire cannot tell a native permission dispatch from a call the model makes itself; either way it needs a fresh allow-once for its own arguments and grants nothing later. A stop holds an uncertain delivery until the owned process group is confirmed gone. The [native permission receipt](verification/2026-10-23-headless-claude-permissions.md) is model-free on Claude Code 2.1.309; it is not sandbox proof, and a real account, model and person interaction remains a live leg.
457
481
 
458
- The fresh Codex reviewer (#519) needs `review_mode: "fresh"` and a compat-admitted Codex (receipt-verified 0.146.0, 0.163.0 and 0.164.0; `ahub agents` shows the current policy); an unverified/untrusted version is unavailable. Before each review it requires a model-free isolation receipt: binary and config unchanged, only the two hub tools in the configured MCP inventory, plugins, hooks and Apps disabled (Apps by a separate feature readback), and a named filesystem profile whose fixtures prove public reads, private-path denial (exact, glob and symlink), standard host credential denial and write denial. Credential and tool-home roots are denied even with an empty `local.deny`; that list does not cover every personal file. A dangling or unreadable credential root refuses the reviewer, and the reason names only its path class (`~/.docker`, `$DOCKER_CONFIG`), never the path or its contents. A failed per-review start names one class from a closed list (`version`, `binary`, `mcp_inventory`, `plugins_hooks_apps`, `sandbox_proof`, `credential_root`, `timeout`, `unknown`) with a fixed sentence in the unavailable reason, `hub.log` and the `reviewer` line of `ahub status`; it never carries a path, native CLI text or a token (#722). Its shell is sandboxed read-only, not Claude's Read/Grep/Glob whitelist. Network and web search are configured disabled but not observed: managed requirements can still select cached web search, and no run has shown the review selecting the profile through `default_permissions`. A failed receipt or start never falls back to a continuing native session, and a child that ends without a saved verdict is never approval. The agy reviewer slot is an unavailable placeholder and never runs.
482
+ The fresh Codex reviewer (#519) needs `review_mode: "fresh"` and a compat-admitted Codex (receipt-verified 0.146.0, 0.163.0 and 0.164.0; `ahub agents` shows the current policy); an unverified/untrusted version is unavailable. Before each review it requires a model-free isolation receipt: binary and config unchanged, only the two hub tools in the configured MCP inventory (each approved by its own name, never by a server-wide default, #783), plugins, hooks and Apps disabled (Apps by a separate feature readback), and a named filesystem profile whose fixtures prove public reads, private-path denial (exact, glob and symlink), standard host credential denial and write denial. Credential and tool-home roots are denied even with an empty `local.deny`; that list does not cover every personal file. A dangling or unreadable credential root refuses the reviewer, and the reason names only its path class (`~/.docker`, `$DOCKER_CONFIG`), never the path or its contents. A failed per-review start names one class from a closed list (`version`, `binary`, `mcp_inventory`, `plugins_hooks_apps`, `sandbox_proof`, `credential_root`, `timeout`, `unknown`) with a fixed sentence in the unavailable reason, `hub.log` and the `reviewer` line of `ahub status`; it never carries a path, native CLI text or a token (#722). Its shell is sandboxed read-only, not Claude's Read/Grep/Glob whitelist. Network and web search are configured disabled but not observed: managed requirements can still select cached web search, and no run has shown the review selecting the profile through `default_permissions`. A failed receipt or start never falls back to a continuing native session, and a child that ends without a saved verdict is never approval. The agy reviewer slot is an unavailable placeholder and never runs; it binds nothing, so agy's own `hub_task_show` reads are not fenced as a review (#792).
459
483
 
460
- The fresh Kimi reviewer (#533) needs `review_mode: "fresh"` and a compat-admitted Kimi Code (receipt-verified 2.2.0 and 2.3.0; `ahub agents` shows the current policy) with its native OAuth login; an unverified/untrusted version, or a failed preparation, is unavailable. The [2.3.0 receipt](verification/2026-10-22-kimi-qwen-compat.md) is model-free and does not prove real login or a model review. It is a native tool-authority boundary, not a sandbox: no Seatbelt wraps it, and same-user native code is not isolated from the filesystem or network. Each review starts a new child in a private `KIMI_CODE_HOME` with managed-provider-only config, a clean temporary working directory and an empty skills directory. Its agent may use only `hub_review` and `hub_task_show`, served by a loopback MCP session bound to the task, head and review revision that never carries the hub's control token; it has no native read, file or shell tool and sees only the screened review brief and host facts. Its `credentials/` and `oauth/` directories and `device_id` link to the person's native ones, so refresh keeps Kimi's own lock and atomic rename; no token is copied and `KIMI_DISABLE_OAUTH_LOCK` is never set. A stop revokes its verdict authority before termination; killing it mid-refresh may require a native re-login. Private tasks are never given to it, and a child that ends without a saved verdict is never approval.
484
+ The fresh Kimi reviewer (#533) needs `review_mode: "fresh"` and a compat-admitted Kimi Code (receipt-verified 2.2.0, 2.3.0 and 2.3.1; `ahub agents` shows the current policy) with its native OAuth login; an unverified/untrusted version, or a failed preparation, is unavailable. The [2.3.0](verification/2026-10-22-kimi-qwen-compat.md) and [2.3.1](verification/2026-10-25-kimi-231-reviewer-compat.md) receipts are model-free and do not prove real login or a model review. It is a native tool-authority boundary, not a sandbox: no Seatbelt wraps it, and same-user native code is not isolated from the filesystem or network. Each review starts a new child in a private `KIMI_CODE_HOME` with managed-provider-only config, a clean temporary working directory and an empty skills directory. Its agent may use only `hub_review` and `hub_task_show`, served by a loopback MCP session bound to the task, head and review revision that never carries the hub's control token; it has no native read, file or shell tool and sees only the screened review brief and host facts. Its `credentials/` and `oauth/` directories and `device_id` link to the person's native ones, so refresh keeps Kimi's own lock and atomic rename; no token is copied and `KIMI_DISABLE_OAUTH_LOCK` is never set. A stop revokes its verdict authority before termination; killing it mid-refresh may require a native re-login. Private tasks are never given to it, and a child that ends without a saved verdict is never approval.
461
485
 
462
486
  The review brief (#520) withholds private changes whole (both sides of a move) and counts them; a rule file that fails the private-data screen is named, not shown. Attributes and rules are read at the merge base, so a change cannot loosen its own review. Optional changed symbols (#684) come from ripwire 0.6.7 on the daemon's host PATH, run as an owned process on a disposable snapshot of the task's commit; each path, name and kind passes the hub's public metadata screen before the brief, the console's task detail or the dashboard shows it, a private task stores and shows none, and a failed or missing read is `unknown`, never a gate. Spec acceptance, plan review, plan conformance, premature-review, cost, test-integrity (#481 S4), issue and PR identity linking (#694) and dogfood scorecard (#687) reads fetch GitHub metadata only (edit times, check states, thread authorship and resolution, changed file names, open PR heads, workflow run conclusions), never issue bodies, comments or patches. Issue identity linking at propose and accept (#723) parses the task's title and refs locally, reads nothing from GitHub and skips PII tasks. The CI workflow keeps `contents: read` and `actions: read`.
463
487
 
@@ -475,6 +499,14 @@ SDLC metrics and control bands (#481 S7). `ahub sdlc metrics` and `hub_sdlc_metr
475
499
 
476
500
  The runner uses Apple `container`, pinned Bun 1.4.2 and a verified `hostOnly` internal network with DNS disabled, two CPUs and 4 GiB. It starts a named, labeled VM with a bounded idle process, copies a hub-owned snapshot into the running VM, then invokes a fixed `container exec` check. This run/copy/exec sequence is required because Apple `container copy` rejects a stopped container. No host directory, home, credential directory, hub state or socket is mounted. Source blobs are read from the exact Git commit; symlinks and gitlinks anywhere in the source refuse the run before exclusions. Directory prefixes and filenames are checked for NFC/Unicode case-fold collisions before staging writes, including denied entries; folding must reach a fixed point within a bounded number of passes or the run refuses. This is a conservative collision screen, not a claim of exact parity with every host filesystem. Denied and PII paths are excluded and counted. Dependency files require an equal lockfile, bounded regular files/directories and internal relative symlinks. No dependency installation or Internet access occurs in the VM.
477
501
 
502
+ The optional remote backend (#793) selects `checks.runner = "remote"` and a strict `checks.remote` object containing `host`, digest-pinned `image` and absolute `root`. Both the grant and destination must come from an untracked `config.local.json`; a tracked or shared-file grant is refused. The destination is host-owned configuration, never a peer argument. OpenSSH uses BatchMode, disables TTY allocation (`-T`), agent/X11 forwarding (`-a`, `-x`), configured forwardings and local SSH commands, and retains only the hub-owned agent socket for local SSH authentication. It uses fixed Docker operations, and every remote argument is shell-quoted because OpenSSH joins the command before the remote shell interprets it. SSH diagnostics are discarded. Only actual check execution merges remote command stderr into screened output; readiness JSON keeps stdout separate from withheld diagnostics. Exit 255 discards stdout too; retained container stdout is screened and destination/root strings are replaced before receipt/output persistence. Public image identity is digest-only. Platform comes from inspected Linux image metadata and the actual fixed Bun probe architecture, with only a numeric Docker version prefix. No host, account or remote root is included in those projections.
503
+
504
+ Remote checks reuse the same request validation, exact Git snapshot, queue, authority fences, repository heavy lease, receipt retention and cleanup lifecycle. They stream a trusted host tar producer into `docker exec -i --user 65532:65532 ... tar -xf - -C /work`; no working-tree sync or Mac `node_modules` copy occurs. Darwin pins the system /usr/bin/tar regardless of PATH; Linux keeps the hub-owned executable resolver. The archive excludes extended attributes, macOS metadata and ACLs, and normalizes owner/group identifiers and names instead of carrying host identity. The staging parent remains mode 0700; snapshot directories are explicitly 0755 and files 0644 or Git-executable 0755 even under umask 077, so non-root execution can traverse and read the copy. Extraction does not restore archive ownership: the copied files belong to the container uid and may be changed only in its throwaway work tmpfs. No privileged chown or root extraction is used. Only the throwaway work tmpfs and home/temp tmpfs can be writable, never the host source. The tar producer is cancelled and reaped on failure or cancellation.
505
+
506
+ Docker checks use a non-root numeric uid, `--network none`, read-only root filesystem, home/temp/work tmpfs, all capabilities dropped, no-new-privileges and fixed pids, CPU (4) and memory (4 GiB) caps. No host path, socket, home or credential mount is allowed. A uniquely named/labeled run uses only a locally present image (`--pull never`). Removal is confirmed through project/name-filtered Docker inventory before the existing heavy lease is released. Inventory projects individual hub label fields instead of parsing Docker's comma-joined label display, and does not enumerate unrelated projects' containers. Readiness checks Docker reachability/version, the exact image digest and architecture, and a fixed hardened network-none probe that uses the same archive transfer and non-root readback as a run; any failed proof is `remote runner unavailable`, with no local fallback. Probe cleanup remains bounded and runs even after its admission signal is cancelled. Exact project/probe-UUID/name rows without a task run id are left to their fixed 600-second expiry and excluded from task-run orphan warnings. Malformed probe rows and unknown task-run owners still produce warnings; labels alone never authorize killing a probe.
507
+
508
+ The remote backend's `root` is reserved, validated metadata until the separate #793 dependency-preparation slice needs it; no current Docker command uses a host path. The current read-only named dependency volume is a project-key-scoped, explicitly unprepared placeholder. Even after a successful transport probe, tests capability stays unavailable with `remote dependencies unprepared` until a preparation contract lands. This slice neither installs dependencies nor prepares a shared writable cache; lockfile/Bun-keyed preparation (AC3), actual machine opt-in and a live remote receipt (AC6) remain separate work. Fake transport/argv tests establish the source contract. A bounded [local Docker transport receipt](verification/2026-10-25-remote-docker-transport.md) verifies transfer and cleanup under its disclosed CPU adjustment; remote SSH, dependency readiness and pi's live tools remain unverified.
509
+
478
510
  Dependency copies detect observed inode, size and timestamp changes and refuse final-component symlinks using `O_NOFOLLOW`. A same-user writer can still exchange and restore a parent directory between observations; this is not an atomic immutable-dependency guarantee. Source commits remain immutable. Do not treat a dependency copy as protection against a hostile process running as the same host user.
479
511
 
480
512
  Checks serialize through the repository's existing `ahub-heavy.lock`, with one active and at most two queued runs, a ten-minute execution ceiling after heavy-slot admission and a bounded heavy-slot wait. Container startup has a 120-second cap for cold VM boot, and copy has a 180-second cap for the bounded 512 MiB source plus 1 GiB dependency corpus. Each stage is clamped to the remaining ten-minute budget and uses the same cancellation signal; exec receives only the remaining budget. Network/probe commands retain a 15-second cap, also clamped and cancellable during an admitted run. Named-container cleanup has its separate bounded teardown commands and must still be confirmed before releasing the heavy slot. Queued cancellation settles its receipt and releases queue capacity immediately; active cancellation, timeout and shutdown stop only the named check container. Cleanup is confirmed through inventory before releasing the heavy slot; unknown cleanup keeps the slot and reports an error. Startup removes only this project's labeled containers whose signed recorded hub owner is confirmed gone. Unknown owners are reported for the person, never killed.
package/docs/smoke.md CHANGED
@@ -1,6 +1,44 @@
1
1
  # Live smoke checklist
2
2
 
3
- `scripts/check.sh` covers everything against fakes. The legs below need real accounts and an interactive terminal, so they are run by hand and recorded here.
3
+ `scripts/check.sh` covers everything against fakes. The legs below need real accounts, a real terminal or a person's judgement, and their results are recorded here. An explicitly authorized agent runs most of them itself with `ahub --live-test` in a disposable project (see [Agent-operated verification](#agent-operated-verification-408)); each open leg carries one class (#784):
4
+
5
+ - `agent-executable`: an authorized agent runs it and records the result.
6
+ - `agent-evidence / person-verdict`: an agent runs it and collects the evidence; the person gives the verdict named in the row.
7
+ - `person-only`: only the person can do it (a login, a native choice, a consent).
8
+ - `external-blocked`: waiting on something outside the hub; the row names it.
9
+
10
+ ## Open live legs by class
11
+
12
+ | Leg | Section | Class | Person verdict or blocker |
13
+ | --- | --- | --- | --- |
14
+ | Off-campus Cloudflare Access headers (#1) | Issue #7 verification ledger | external-blocked | Access credential files; then agent-executable |
15
+ | Natural Codex budget pause with a TUI attached (#1) | Issue #7 verification ledger | external-blocked | a natural weekly reading above the gate |
16
+ | Native Pi restoration and the operator plan (#273) | Issue #273 operator execution plan | agent-executable | |
17
+ | Hub memory live run (#258) | Hub memory | agent-executable | |
18
+ | Office view look (#264) | Office view | agent-evidence / person-verdict | the look in a browser, light and dark |
19
+ | Add-on arms and fingerprints (#248) | Add-on arms and fingerprints | agent-executable | |
20
+ | Settings from the dashboard and the terminal (#269) | Settings from the dashboard and the terminal | agent-evidence / person-verdict | the dashboard controls as a person sees them |
21
+ | Codex resumed approval policy stickiness (#270) | Codex resumed approval policy | person-only | the explicit native choice in the person's TUI |
22
+ | Interactive upgrade screens (#272) | Interactive upgrade | agent-executable | |
23
+ | `ahub reset` on a scratch project (#214) | ahub reset on a scratch project | agent-executable | |
24
+ | Claude without channel evidence (#205) | Claude without channel evidence | agent-executable | |
25
+ | Console layout and readability (#213, #201) | Console layout and readability | agent-evidence / person-verdict | readability at 80 and 120 columns |
26
+ | `ahub bench` live run (#251) | `ahub bench` live run | agent-executable | |
27
+ | Codex and Pi permission-mode legs (#240, #242) | Permission modes | agent-executable | |
28
+ | Dashboard themes (#246) | Whole-board task progress and dashboard themes | agent-evidence / person-verdict | light and dark legibility |
29
+ | Command output (#284) | One-shot command output; Remaining command output | agent-evidence / person-verdict | terminal rendering at 80 and 120 columns |
30
+ | Qwen and Kimi cancellation timing (#304) | ACP cancellation settlement | agent-executable | |
31
+ | Kimi TUI pull mode (#316) | Kimi TUI pull mode | agent-executable | |
32
+ | One issue through merge and release in advise mode (#257) | SDLC gates | agent-evidence / person-verdict | release consent |
33
+ | Qwen native turn and recovery (#256) | Qwen Code production peer | agent-executable | |
34
+ | Jcode instance sign-in per `jcode_home` (#332, #529) | Jcode peer | person-only | signing in to each instance's account |
35
+ | Planner grooms a backlog into two releases (#263) | Project board; Planning and release controls | agent-executable | |
36
+ | Character glyphs in real terminals and tmux (#280) | Console attention; Dashboard character | agent-evidence / person-verdict | glyph rendering in physical terminals |
37
+ | Console Settings panel in a native terminal (#439) | Settings reload and console fixture | agent-evidence / person-verdict | the panel as a person sees it |
38
+ | Reviewer pool native legs (#519) | Reviewer pool native legs | agent-executable | after #783 ships |
39
+ | Antigravity TUI close (#485) | Antigravity P1 | agent-executable | |
40
+ | Managed Pi live receipts (#418 AC8) | none yet (issue #418) | person-only, then agent-executable | the person opts in to a check runner and answers the approvals |
41
+ | Kimi credential probe (#285 AC9) | Kimi usage counting | person-only | the owner's approval of the probe |
4
42
 
5
43
  ## Issue #273 operator execution plan (2026-10-16)
6
44
 
@@ -56,6 +94,16 @@ operationId, console hello and initiating-peer audit (225 related tests passed).
56
94
  This is fixture/CLI proof, not an actual native Pi restoration; that live leg
57
95
  remains owed in #273.
58
96
 
97
+ ## Kimi usage counting (#285 AC11)
98
+
99
+ The [2026-10-25 Kimi 2.3.1 receipt](verification/2026-10-25-kimi-usage-ac11.md)
100
+ records an isolated-HOME ACP session. Native `/usage` exactly matched the
101
+ main-agent four-counter wire total but excluded subagent usage; neither
102
+ `/usage` call added a `usage.record`. The ordinary main row appeared after the
103
+ prompt-result read and within 1,000 ms, while the subagent turn's main and
104
+ subagent rows were present at its result. See the receipt for the counts,
105
+ occupancy timing and isolation boundary.
106
+
59
107
  ## Console task and queue operations (#424)
60
108
 
61
109
  2026-10-16, macOS, source `691f00ac`: a disposable Git project and fake ACP
@@ -1953,13 +2001,13 @@ Complete these legs in a disposable project with authorized accounts after sourc
1953
2001
  - Request a delta review and confirm the same eligible independent reviewing family receives a new session with prior findings. After an earlier same-family fallback, a newly available independent family still takes precedence. Exercise configured re-rotation separately. Stop or exhaust one reviewer and confirm the pure routing walk chooses the next eligible candidate; no failed start or child ending without a saved verdict becomes approval. Check the next candidate even when the missing verdict followed a vendor quota error; no text-based quota classifier is assumed.
1954
2002
  - Make all pool candidates unavailable and confirm a blocked review awaiting planner/person action. Then test a different fresh session in same-family fallback only when no other family can run. Verify stale-child, changed-head and private-review verdict refusals.
1955
2003
  - Verify compatibility separately: a Codex/Kimi headless reviewer-role declaration without `review_mode` keeps its native app-server/ACP behavior and existing worktree/permission options, and stays outside the fresh pool. An explicit fresh declaration must refuse an unsupported backend without downgrading.
1956
- - The default logical pool is Claude, Codex, Kimi. Kimi is runnable for a compat-admitted version when runtime preparation passes (receipt-verified 2.2.0 and 2.3.0; inspect `ahub agents`); an unverified/untrusted version or failed preparation is unavailable with its reason. Seed modulo includes only kinds with a declared candidate, even when blocked; undeclared kinds remain trailing trace entries. Verify seeds 0..5 with only Claude/Codex declared alternate 3/3, and an offline declared kind still retains its slot. Agy stays unavailable as a reviewer with exact reason `agy is an implementer peer (#485)`, including explicit pool requests. #234 adds no agy reviewer launch or live leg.
2004
+ - The default logical pool is Claude, Codex, Kimi. Kimi is runnable for a compat-admitted version when runtime preparation passes (receipt-verified 2.2.0, 2.3.0 and 2.3.1; inspect `ahub agents`); an unverified/untrusted version or failed preparation is unavailable with its reason. Seed modulo includes only kinds with a declared candidate, even when blocked; undeclared kinds remain trailing trace entries. Verify seeds 0..5 with only Claude/Codex declared alternate 3/3, and an offline declared kind still retains its slot. Agy stays unavailable as a reviewer with exact reason `agy is an implementer peer (#485)`, including explicit pool requests. #234 adds no agy reviewer launch or live leg.
1957
2005
 
1958
2006
  Record source head, exact executable version/hash, isolation receipt, task/head identities, route walk, distinct native session evidence and persisted verdict separately. Do not record credentials or raw transport environments. Codex sandboxed shell must not be described as Claude's Read/Grep/Glob-only whitelist. Network policy is configured disabled; no network-denial probe or native LLM isolation result has been recorded by this investigation.
1959
2007
 
1960
2008
  ## Kimi reviewer and task-bound MCP (#533)
1961
2009
 
1962
- Kimi receipt-verified 2.2.0 and 2.3.0 are available when runtime preparation passes; a failed or unsupported preparation remains unavailable. The [2.3.0 receipt](verification/2026-10-22-kimi-qwen-compat.md) is model-free; the following authenticated evidence belongs to 2.2.0. Completed evidence includes account-free two-tool/native OAuth race checks, host/fake lifecycle refusals and authenticated fresh `changes_requested` verdicts. The complete-collector live run observed project parity/canary absence, authenticated exact-two hub inventory/calls, zero decoy requests, cleanup and unchanged real config hashes. Its original aggregate receipt remains failed because of an unattributed extra observer error; the later account-free GET reproduction/correction is not a new live pass. These separate outcomes are preserved in the R0/live report. The remaining actual interactive-TUI/coexistence and usage-observation checks are **UNVERIFIED**; they do not turn completed live/fake evidence into an unrun test. Person ruling 2026-10-19 (8281ec35) limits #234 to Kimi and task-bound MCP and supersedes all earlier agy reviewer implementation/admission/live-leg rulings. Agy is an implementer under #485; its retained reviewer slot remains unavailable with exact reason `agy is an implementer peer (#485)`. Earlier agy probes and unresolved meta-tool/resource findings remain historical in the [R0 report](verification/2026-10-19-kimi-agy-reviewer-isolation.md), not active smoke requirements here. Kimi's native-permission/shared-account ruling supersedes its earlier R0-only hold and Seatbelt requirement. The [spec amendment](specs/2026-09-19-agent-hub-design.md#kimi-reviewer-and-task-bound-mcp-amendment-533-hub-task-234) defines the active contract and residuals.
2010
+ Kimi receipt-verified 2.2.0, 2.3.0 and 2.3.1 are available when runtime preparation passes; a failed or unsupported preparation remains unavailable. The [2.3.0](verification/2026-10-22-kimi-qwen-compat.md) and [2.3.1](verification/2026-10-25-kimi-231-reviewer-compat.md) receipts are model-free; the following authenticated evidence belongs to 2.2.0. Completed evidence includes account-free two-tool/native OAuth race checks, host/fake lifecycle refusals and authenticated fresh `changes_requested` verdicts. The complete-collector live run observed project parity/canary absence, authenticated exact-two hub inventory/calls, zero decoy requests, cleanup and unchanged real config hashes. Its original aggregate receipt remains failed because of an unattributed extra observer error; the later account-free GET reproduction/correction is not a new live pass. These separate outcomes are preserved in the R0/live report. The remaining actual interactive-TUI/coexistence and usage-observation checks are **UNVERIFIED**; they do not turn completed live/fake evidence into an unrun test. Person ruling 2026-10-19 (8281ec35) limits #234 to Kimi and task-bound MCP and supersedes all earlier agy reviewer implementation/admission/live-leg rulings. Agy is an implementer under #485; its retained reviewer slot remains unavailable with exact reason `agy is an implementer peer (#485)`. Earlier agy probes and unresolved meta-tool/resource findings remain historical in the [R0 report](verification/2026-10-19-kimi-agy-reviewer-isolation.md), not active smoke requirements here. Kimi's native-permission/shared-account ruling supersedes its earlier R0-only hold and Seatbelt requirement. The [spec amendment](specs/2026-09-19-agent-hub-design.md#kimi-reviewer-and-task-bound-mcp-amendment-533-hub-task-234) defines the active contract and residuals.
1963
2011
 
1964
2012
  - For Kimi regression smoke, confirm a fresh child per review, no resume, source evidence supplied only by screened `hub_task_show`/review brief and host facts, and no native Read/file/shell tool. Confirm the in-process host MCP exposes only the task-bound `hub_review` and `hub_task_show` capability, without the full hub control token. Exercise wrong task/head/review revision, stale/replaced child, private review, malformed arguments, unknown tool and stopped-child refusals; verify no saved approval on any failure.
1965
2013
  - For each compat-admitted Kimi version, confirm no Seatbelt wrapper, a private `KIMI_CODE_HOME` with managed-provider-only config, clean temporary cwd and explicit empty skills directory. Verify exactly the two `mcp__agent-hub__...` tools in model disclosure and invocation; reject native read/write/command and extra MCP calls. Confirm `credentials/` and `oauth/` are directory symlinks to the native locations, `device_id` links the native identity, no token copy/extraction/static provider is used, and `KIMI_DISABLE_OAUTH_LOCK` is absent. The concurrent account-free OAuth print stand-in proved two expired generations serialize refresh through shared directory links, but it was not an attached interactive TUI. Actual TUI refresh/atomic rename/lock coordination and shared-account quota/pause fanout remain separate live legs. Existing project MCP/config byte-parity observations remain recorded in the R0/live report. Record identities/metadata without token values.
@@ -2070,7 +2070,8 @@ reviewer may also call it for that task. They get exactly the conductor's
2070
2070
  view: `publicConductorTask` over `Tasks.publicView(task, true)`, so the whole
2071
2071
  history with its done summary and check line, notes that match a PII pattern
2072
2072
  withheld, and a PII task as a stub with no history. Any other peer is refused
2073
- with the conductor-role error, whatever the task. That refusal is about the
2073
+ with the conductor-role error, whatever the task, except the configured planner,
2074
+ which reads any task through the same view (#812, below). That refusal is about the
2074
2075
  tool, not a secret: the view holds nothing private beyond the PII stub, and the
2075
2076
  control op `task_show` (`ahub task show` from an agent shell) already returns
2076
2077
  the same public view to any peer. These reads write no conduct audit event, as
@@ -2461,7 +2462,7 @@ acts on the stopped state directory.
2461
2462
  - A console-role `{ t: "peer_stop", peer }` request stops a hub-owned headless ACP, Pi or local runtime through its adapter's owned-runtime teardown. Success is `{ ok: true, state: "offline" }`; refusal or incomplete stop is `{ ok: false, error }` naming the reason and next action. New requests already receive an unknown-request reply on older hubs, so `PROTOCOL` stays unchanged.
2462
2463
  - Refuse agent/tools-role callers, unknown or offline peers, Claude/Codex and Pi TUI owners, and recovery/shutdown-held mutations. Starts, requested stops and permission changes are fenced per peer; recovery preparation waits until requested stops finish. A TUI is ended in its terminal, or by the upgrade plan's verified terminal binding.
2463
2464
  - A requested stop marks each in-flight delivery `needs_review` with a fixed ids-only stop reason, cancels pending approvals, and fences stale native completion/failure callbacks. It never completes or implicitly redelivers interrupted work, invokes failure escalation, or triggers Pi's idle auto-restart. The operator inspects effects and explicitly resolves the delivery before replay. Queued unattempted work remains for the next manual start under existing holds/pauses.
2464
- - `ahub stop <peer>` is human-only in `classifyPeerCommand`, prints the returned state, and reports an older hub's unknown request as requiring an upgrade, with a nonzero exit. The upgrade plan's headless `endPlannedPeer` uses the same console request; the daemon does not close a native terminal.
2465
+ - `ahub stop <peer>` is available to the person or the selected project's configured conductor in `classifyPeerCommand` (#812). The conductor retains its authenticated peer identity and audit; other agents receive the person guidance promptly, including when the hub is unreachable. It prints the returned state and reports an older hub's unknown request as requiring an upgrade, with a nonzero exit. The upgrade plan's headless `endPlannedPeer` uses the same console request; the daemon does not close a native terminal.
2465
2466
 
2466
2467
  - Requested stop admission also holds per-peer drain, pull and steering before any teardown await, without changing operator/budget pauses. Arriving work remains queued for explicit restart. Idle Pi captures live empty-session proof before stopping; a first accepted prompt with unpersisted history is not silently recreated as empty.
2467
2468
 
@@ -2649,6 +2650,28 @@ the queue.
2649
2650
  - Activity contains delivery ids/senders, message/thought chunks, bounded tool summaries, plans, permission request/outcome ids and turn end. It never enters persistent events/logs, UI, tools or cloud memory. Existing final answers still travel through the answer bus.
2650
2651
  - Kimi TUI defaults remain pull-only. Qwen TUI and native visualizer support remain gated by unverified T0 checks. See [verification](../verification/2026-10-11-acp-watch-t0.md).
2651
2652
 
2653
+ # Qwen dual-output TUI amendment, 2026-10-25 (#286 Part 3)
2654
+
2655
+ - T0 ran live on Qwen Code 0.26.1 under an isolated HOME ([verification](../verification/2026-10-25-qwen-dual-output-t0.md)): Q2, Q3, Q5 and Q6 pass, Q4 fails. The owner decided on 2026-10-25 to build Part 3 with the two limits below stated, so AC9's condition reads "Q2, Q3, Q5 and Q6 pass; Q4's limit is stated".
2656
+ - Launch: `ahub qwen --mode tui` runs `qwen --input-file <in> --json-file <out> --mcp-config <hub server> --approval-mode default` in the person's terminal, as `ahub pi --mode tui` does. Both files live under the project state dir, mode 0600. The event file is a regular file the adapter tails from a saved offset.
2657
+ - Version gate: a separate compat entry for the dual-output contract, verified 0.26.1 only. It does not widen the headless `qwen` entry, whose ACP receipts are separate.
2658
+ - Delivery: `deliver()` sets busy synchronously and appends one `{"type":"submit","text"}` line with the digest. Qwen queues a submit that arrives during a turn and runs it after that turn ends.
2659
+ - Turn end: Qwen emits no `result` event in this mode. A turn ends at an `assistant` event whose `stop_reason` is not `tool_use`, followed by `message_stop`; that assistant text is the turn's shared answer. A `result` event, if a later build emits one, also ends the turn.
2660
+ - Approvals: `control_request` with `subtype: can_use_tool` goes to the hub approval path, and the answer is `{"type":"confirmation_response","request_id","allowed"}`. Allow or deny only. The hub's own tools are approved by the exact `tool_name` (`mcp__<server>__<tool>`), never by prefix. A `control_response` for a pending request that the hub did not answer means the person answered in the TUI: withdraw the hub's pending approval. A `control_response` error for a stale id is logged, never retried.
2661
+ - Limit 1, deny: a hub deny cancels Qwen's whole turn and Qwen emits nothing for it. After sending `allowed: false` the adapter ends the turn itself (idle, no shared answer, a notice that the turn was denied).
2662
+ - Limit 2, no external stop: the input protocol has only `submit` and `confirmation_response`, and SIGINT does not stop a turn. The inactivity watchdog never cancels a Qwen TUI turn. It reports the stalled turn to the person (status, console and dashboard: "stalled; press Esc in the Qwen terminal") and keeps the peer busy. The hub never types into the terminal or signals the process to stop a turn.
2663
+ - Prompt channel: the input file is a prompt channel. Qwen's own write and shell tools reach it only through approval, and the hub's approval path refuses any write or command that names the state dir. No other peer can write it.
2664
+ - Detached: the TUI keeps working when nobody reads the event file (T0 Q5). A daemon that cannot read the file treats the peer as detached, not idle; on reattach the adapter resumes from its saved offset.
2665
+ - Recovery: `qwen -r <session_id>` with the same two files resumes the conversation, does not replay earlier submits, and appends to the event file. The adapter records `session_id` from `system/session_start` and resumes from its saved offset, so events from before the restart are not processed twice.
2666
+ - Implementation notes (#449, measured on 0.26.1 where stated):
2667
+ - Files and state: `<state dir>/qwen/qwen-dual/{input.jsonl,events.jsonl,mcp.json,state.json}`; `state.json` holds `session_id`, the read offset, a generation counter and the bound owner (pid with its `processSignature`). `ahub qwen --mode tui` is explicit only: Qwen's `start.forms` stays `["headless"]`, so Qwen is not a start-mode peer and the hub never opens this terminal itself. The TUI driver is the definition's `start.createTui` on the `qwen-dual` daemon port, a fixed-builtin capability.
2668
+ - Gate: checked twice, by `qwen --version` against `COMPAT["qwen-dual"]` before any file is made, and by the TUI's own `session_start` (version, `protocol_version` 2, the events the adapter reads). An unverified TUI is not attached and the person gets a notice.
2669
+ - Event file: Qwen opens an existing `--json-file` at byte 0 without truncating it (it overwrote the head of a 3 KB file in place), so appending after a restart is not what it does. A fresh or resumed launch therefore starts from empty files (the old event file is kept as `events.prev.jsonl`, the saved offset restarts at 0 for the new file), and a hub restart while the person's Qwen keeps running adopts it and reads on from the saved offset with no launch. Qwen starts reading the input file at its size at start, so earlier submits are not replayed either way.
2670
+ - Owner: bound from the host's process table (the one process whose argv contains `--input-file <this adapter's file>`; two matches is no owner), at `session_start` and then every second. No launcher message is trusted for it. When the owner is gone the peer goes offline, an open hub turn settles `needs_review` and a notice says the terminal ended. The hub never signals it.
2671
+ - A prompt in the event stream that is not the hub's submit (matched by its text) is the person typing: the peer is busy, nothing is shared, and a `can_use_tool` request in that turn is the person's own (the console is not asked). A hub turn that a person's prompt cuts off (Esc, then a new prompt) settles `needs_review`.
2672
+ - `ahub stop qwen` refuses for the TUI (end it in its terminal); a headless Qwen cannot be started while the TUI runs, and the reverse. Crash and upgrade recovery refuse to restore this terminal as a headless worker (`recoveryRefusal`), as for Kimi's own TUI.
2673
+ - Not verified live: the T0 receipt covers Q2 to Q7 on 0.26.1; this PR's tests use a fake dual-output Qwen (`test/fakes/qwen-dual.ts`) and no live model, so end-to-end use with a real account remains the person's check (`docs/dogfood.md`).
2674
+
2652
2675
  # Historical workflow amendment, 2026-10-11 (#257, superseded by #481)
2653
2676
 
2654
2677
  - The original optional workflow selector used existing SDLC receipts and Tasks authority; its current replacement is the #481 amendment below. A reviewer/person-owned checklist stores bounded criterion ids, hashed evidence refs and verified/failed/unverified claims bound to the reviewed head.
@@ -3262,7 +3285,7 @@ PII authority, complete-row host screening, capture exclusion, byte budgets and
3262
3285
 
3263
3286
  ### Bounded sandboxed-peer capabilities (#418)
3264
3287
 
3265
- The frozen issue #418 contract supplies task-bound `hub_task_spec` and the finite `hub_check_start/result/cancel` tools; memory reads reuse #412 P2. `checks.runner = "container"` remains an untracked, person-owned opt-in. Specs include validated canonical GitHub intake identity as well as bounded textual references and remain complete-or-refused; checks run immutable Git source in a credential-free, internal-network Linux VM with fixed commands and binding-scoped receipts. Default sandbox permissions remain unchanged.
3288
+ The frozen issue #418 contract supplies task-bound `hub_task_spec` and the finite `hub_check_start/result/cancel` tools; memory reads reuse #412 P2. `checks.runner = "container"` or `"remote"` remains an untracked, person-owned opt-in; the remote grant and destination require `config.local.json`. Specs include validated canonical GitHub intake identity as well as bounded textual references and remain complete-or-refused; checks run immutable Git source in a credential-free, isolated Linux VM/container with fixed commands and binding-scoped receipts. Default sandbox permissions remain unchanged. The remote backend (#793) streams a metadata-stripped, owner-normalized archive to non-root `docker exec -i ... tar` extraction in its work tmpfs, rather than Docker cp into a read-only-root container. Readiness proves that same transfer and readback path plus network isolation; project/name-filtered inventory reads individual hub labels and confirms cleanup. Until the separate dependency-preparation slice supplies prepared dependencies, remote tests capability remains unavailable even after transport readiness succeeds.
3266
3289
 
3267
3290
  Apple container 1.5.0 requires a running VM for `copy`. The implementation therefore uses `run --detach --rm` with the fixed bounded idle process, copies the validated source/dependencies, executes only the selected finite check argv and confirms cleanup. A create/copy/start probe failed on the stopped-container copy and left no container; the run/copy/exec probe passed. `--internal` is verified as network configuration `mode: hostOnly`, not an inferred network name. Inactive sandboxed peers establish the cached spec authentication probe at first start/use rather than making unrelated host-embedding fixtures contact real accounts. Source and dependency staging ceilings are 100,000 entries each, 512 MiB source and 1 GiB dependencies; exceeding one refuses the check.
3268
3291
 
@@ -3280,7 +3303,7 @@ Evidence: `docs/verification/2026-10-17-418-probes.md`. The feasibility probe is
3280
3303
  - Ready ownerless tasks retain the ordinary routing decision and reservation guards. A persisted cause/replanning signature admits one ids-only console and supervision notice for an ineligible reserved peer, no eligible peer or a cancelled dependency. Repeated release passes with the same cause add neither history nor notices; a changed cause or explicit replanning admits one new notice. The designated planner (the settled configured planner roles) receives bounded ids-only advice in its next preface, while the configured supervision stream receives the structured item under its existing scope. Advice does not manufacture an ordinary task turn beside a stale backlog batch. Without a planner, the console attention list remains the recipient. Existing owner/reviewer ladders remain unchanged.
3281
3304
  - A current proposed offer to a pull-only peer remains console attention until acceptance, decline, supersession or positive delivery evidence for that exact offer envelope and recipient. Tasks records the original envelope id, assignment revision and offer-time pull-only classification before publication. This classification survives detach and restart; current attachment is not evidence that an unread offer disappeared. Actual adapter completion, a correlated reply or a completed pull records positive evidence atomically with journal completion, surviving terminal pruning and reopening. Accepted-only receipts, unrelated inbox activity and operator/hub completion are not read evidence. Advisory queries never dequeue, steer or settle work. Decoded offer/stall candidates are cached by board revision and live eligibility inputs; positive journal read evidence is checked afresh. The derived revision advances atomically with task and stall writes, independently of unrelated delivery-journal writes.
3282
3305
  - The disabled-by-default sweep adds `ready-unassigned` with `ready_min: 60`. A ready proposed ownerless task follows the existing deterministic, restart-safe ladder, without treating quiet stall/offer bookkeeping as activity. A person-unassigned task stays suppressed until explicit person assignment or reservation. Planner-pulled work retains reserved-only/WIP admission, and a saved-but-undelivered reassignment does not stop the remaining sweep. Cancellation does not satisfy a dependency. This change does not enable any project's live sweep configuration.
3283
- - `cancelled` is terminal and retained. The person may cancel backlog, proposed, in-progress, in-review or changes-requested work, never approved work. A configured planner tool may cancel only its own backlog/proposed task with no historical acceptance. `Tasks.cancel` validates the optional flow revision, screens the reason/reference and rechecks authority, revision and recovery/shutdown exclusion after asynchronous screening before the sole board mutation. History retains actor, reason and optional supersession reference. After the cancellation write commits, advisory cleanup and notices cannot change its successful result. No outcome memory, native cancellation, branch deletion or dependent release occurs.
3306
+ - `cancelled` is terminal and retained. The person may cancel backlog, proposed, in-progress, in-review or changes-requested work, never approved work. A configured planner tool may cancel only its own backlog/proposed task with no execution evidence: never started, or accepted and then declined or returned untouched (#812, which replaces the earlier rule of no historical acceptance). `Tasks.cancel` validates the optional flow revision, screens the reason/reference and rechecks authority, revision and recovery/shutdown exclusion after asynchronous screening before the sole board mutation. History retains actor, reason and optional supersession reference. After the cancellation write commits, advisory cleanup and notices cannot change its successful result. No outcome memory, native cancellation, branch deletion or dependent release occurs.
3284
3307
  - Cancellation releases overlap/reservation participation and sends each distinct former owner/reviewer one stop notice. Its trusted cancellation revision marker keeps that stop notice relevant; earlier notices for the cancelled task become moot under #434. The classifier never uses timestamps alone to identify the stop notice. Dependents keep waiting and receive the cancelled-dependency cause. List, show, explain, dashboard and release projections treat cancellation as closed, retaining the current privacy screen and distinguishing cancellation from successful approval.
3285
3308
  - The daemon advertises `task_cancel` in hello and status features. Current clients refuse cancellation without that feature. Upgrade and restart persist the verified target's exported features with its immutable root/digest before prepare, including cached targets, and reuse that receipt on resume after verifying the same bytes. A legacy staged receipt without feature evidence is probed only while a project is not yet past commit; fully committed projects do not require another probe. This preserves #215: a transient probe failure cannot refuse a target that was already staged with verified evidence. Preflight checks only uncommitted sources and refuses a target lacking `task_cancel` while cancelled rows exist. Source prepare/commit recheck before holds/snapshots and around asynchronous work; cancellation cannot write through recovery preparation. Refusals name task ids and never rewrite rows. An older binary started manually can misclassify cancelled rows as open or fail to move them; this is the explicit unsupported compatibility ceiling. Never map cancellation to approval.
3286
3309
  - The post-release live step remains separate: do not cancel the historical task or enable the project's sweep from implementation tests. AC7 needs installed-runtime receipts.
@@ -3846,7 +3869,9 @@ Owner decision 2026-10-18 (#450 comment 6248184754), R1-R5. R3's mechanism, chos
3846
3869
  - **R1/R5, routing.** `assign()` prefers a dedicated reviewer (holds `reviewer`, not `planner`) among the eligible peers, busy or not; family independence and the PII rule (the person reviews) are unchanged. When a dedicated reviewer is declared but not eligible (offline, not started, paused) the planner reviews as before and the assignment note says `reviewer=planner (fallback)`. With no dedicated reviewer declared nothing changes. The reviewer is chosen at assignment, as before (ponytail: a task assigned during a fallback keeps the planner; re-pick at done if that matters).
3847
3870
  - **Budget.** The reviewer shares the `claude` account and its pauses; usage from each child's `result` event is added to the peer's tokens. No quota is classified from its text.
3848
3871
  - **Privacy.** The diff lists changed paths first, with rename and copy detection, and leaves out every change touching a path that `local.deny` or `pii.paths` covers (a move or copy whole, when either side is private), noting only how many were withheld (ponytail: git's 50% similarity decides what counts as a move); a diff that matches a private-data pattern is withheld whole. Those entries are also passed to the child as Claude `Read` deny rules (Claude evaluates them, not the hub's path policy). A private review is never given to it.
3849
- - **Narrative on the read tool.** `hub_task_show` from a review-only instance returns the task with history notes kept only on `approved` and `changes_requested` entries; the done summary and other notes are dropped.
3872
+ - **Narrative on the read tool.** `hub_task_show` from a review-only instance keeps history notes on `approved` and `changes_requested` entries and the bounded planner-ruling exception below; the done summary and all other notes are dropped.
3873
+
3874
+ - **Planner-ruling exception (#835).** Review requests and review-only task views also carry `plan reviewed` notes after the current ownership boundary, the latest `accepted` or `claimed` event (none without either). The author must be the current effective `taskPlanner(task)`, including its legacy role-holder fallback, and still hold the planner role. Keep the oldest five rulings, screen and sanitize each full note before cutting its displayed text to 1000 characters, and retain the withheld marker. PII tasks show none. Each ruling names its author and timestamp; these rulings inform the reviewer and amend review context only. They do not accept a plan departure: departure acceptance stays the person's and `accepted: false` remains unchanged.
3850
3875
  - **Verdict authority.** A headless reviewer's `hub_review` is accepted only while the child that started for that task still runs (a per-child token, so a replacement child for the same task is a new authority). It is captured when the call starts and rechecked before every write, after each await. The leader's exit, a watchdog or a stop revokes it first. A review ends only when the child's whole process group is confirmed gone; survivors hold the delivery as `needs_review` and keep the peer busy with its handle: a stop fails and leaves it busy (never offline), and a start is refused, until they exit.
3851
3876
  - **Settlement.** A start failure before any review in a delivery ran is `failed_safe`; after one ran, the delivery is held as `needs_review`. A failing facts or verdict-check callback is logged, and the review counts as without a recorded verdict.
3852
3877
  - **Never an owner.** The reviewer variant carries `routing.reviewOnly`; assignment refuses it as an owner for every task class and when named.
@@ -4006,7 +4031,7 @@ pure decision and name the chosen instance. Reviewer independence compares
4006
4031
  families (the kind); an eligible other family takes precedence even when busy.
4007
4032
  Only when no other family is eligible may an assignment use its own family,
4008
4033
  recording `sameFamilyFallback=true` in assignment history. This independence rule
4009
- also applies to an explicit same-family reviewer nomination. A declared owner of
4034
+ also applies to an explicit same-family reviewer nomination. `review.same_family = "never"` (#820, default `"fallback"`) removes that last resort in the pool and the legacy path: the planner reviews when it holds the reviewer role, is not the owner, is of another family and is not blocked, else the person, and a same-family nomination is refused with `same family as <owner>; review.same_family is never`. A declared owner of
4010
4035
  implement, bulk-edit or test work requires the current implementer role and positive
4011
4036
  host evidence tied to the actual adapter generation started in a verified noncanonical
4012
4037
  worktree; a role-only reload cannot promote a canonical reviewer process. External
@@ -4716,7 +4741,21 @@ export interface ClaudeLaunchProof {
4716
4741
 
4717
4742
  ### Call-site inventory
4718
4743
 
4719
- Counts are call occurrences in `src/hub/daemon.ts` (`grep -o`). `ripwire . --callers=<predicate>` lists the same functions and lines.
4744
+ The following inventory is historical, audited at `bab87cb2` (2026-10-22). Counts are call occurrences in `src/hub/daemon.ts` (`grep -o`). `ripwire . --callers=<predicate>` lists the same functions and lines.
4745
+
4746
+ Slice 2a freezes the current baseline at `f11a2a1c3e24d1c431937941ecd40b07ca00a5cc` in `test/daemon-native-branches.json`, with executable checking in `test/daemon-native-branches.test.ts` using the shared `scripts/lib/ts-ast.ts` helper. The JSON had 61 predicate/function rows and 85 calls at that baseline; #449 (the Qwen dual-output TUI, #286 Part 3) added three reviewed `isQwenDualPeer` rows (`startPeerBody` 2, `status` 1, `stopPeer` 1: 64 rows, 89 calls) across `src/hub/daemon.ts` and `src/hub/claude-launch-proof.ts` (the launch-proof reader currently contributes zero native predicate calls). Each row carries its current reason; additions and removals both require an explicit reviewed table edit. Tests never regenerate the table.
4747
+
4748
+ | Predicate | Historical count | Current count | Delta |
4749
+ |---|---|---|---|
4750
+ | `isCodexPeer` | 12 | 13 | +1 |
4751
+ | `isPiPeer` | 20 | 21 | +1 |
4752
+ | `isWsPeer` | 25 | 27 | +2 |
4753
+ | `isAcpPeer` | 12 | 13 | +1 |
4754
+ | `isLocalPeer` | 6 | 8 | +2 |
4755
+ | `isJcodePeer` | 1 | 1 | 0 |
4756
+ | `isReviewerPrintPeer` | 2 | 2 | 0 |
4757
+
4758
+ The seven additional calls retain later native proof behavior: `actionEnforcement` adds one call each for Codex, Pi, Ws, ACP and Local; `actionSession` adds Local's intentionally separate action-allowance session; `currentExecutionOwner` adds the terminal Claude socket-generation/worktree proof. Slice 2a removes none of these calls to match the historical plan. The detailed historical assignments below remain context for later slices; the committed table is the current per-function ratchet.
4720
4759
 
4721
4760
  | Predicate | Count | Ported | Intentional |
4722
4761
  |---|---|---|---|
@@ -4835,15 +4874,15 @@ Common acceptance criteria:
4835
4874
 
4836
4875
  1. **2a: Ratchet and trust gate.** Area note: `tests.md`. Size S.
4837
4876
  - Paths:
4838
- - new `test/daemon-native-branches.test.ts` and `test/daemon-native-branches.json` (rows `{predicate, function, count, reason}`; TypeScript AST over `src/hub/daemon.ts` and `src/hub/claude-launch-proof.ts`, reusing the source walk in `test/helpers/registry-inventory.ts`; never imports daemon code);
4877
+ - new `test/daemon-native-branches.test.ts` and `test/daemon-native-branches.json` (rows `{predicate, function, count, reason}`; TypeScript AST over `src/hub/daemon.ts` and `src/hub/claude-launch-proof.ts`, reusing `scripts/lib/ts-ast.ts` from #684 phase 5; never imports daemon code);
4839
4878
  - `src/adapters/registry.ts` (`isFixedBuiltinDefinition`; `session.nativeRevert` restricted);
4840
4879
  - `test/adapter-capability-scope.test.ts`;
4841
4880
  - `src/adapters/conformance/kit.ts` and `enrollment.ts`, with a ports leg.
4842
4881
  - ACs:
4843
- - The table equals today's counts (the inventory above).
4882
+ - The table equals the current baseline above (61 predicate/function rows, 85 calls at `f11a2a1c`); the older inventory remains historical context.
4844
4883
  - An inline fixture with one extra predicate call fails, and so does a row with an empty reason.
4845
4884
  - A configured definition that declares `session.nativeRevert` is refused.
4846
- - Kit leg: a non-fixed-builtin definition whose product has `frontendProxy`, `statusNotice` or `revertTurn` fails the kit. (Today no product has these members, so the leg passes at 2a and starts biting from 2d.)
4885
+ - Kit leg: a non-fixed-builtin definition whose product has `frontendProxy`, `statusNotice` or `revertTurn` fails the kit. (The proposed `frontendProxy` and `revertTurn` members arrive in 2d; Codex already has `statusNotice`. The foreign-product check is active in 2a and rejects presence even when a value is undefined.)
4847
4886
  - The ACP ports leg (`deliveryOnly`, `hubOwned`, `form: "headless"`) is not part of 2a: no ACP product exposes those members until 2b adds them, so 2a cannot assert them. 2a only reserves the leg in `UNSUPPORTED_LEGS` with the reason "ports added in 2b".
4848
4887
  - Builtins that do not apply name their reason in `UNSUPPORTED_LEGS`.
4849
4888
  2. **2b: Delivery-only, owned stop, form and roster detach.** Area note: `adapters.md` (Spawned processes; Turns). Size M.
@@ -5117,3 +5156,10 @@ Ceiling: a scan lists the board once per status revision (at most one per flush)
5117
5156
  - Default CodeFacts review/completion queries have priority over optional neighbours. Interrupt an active optional CLI, await all existing owned cleanup and then admit priority work; do not run native tree preparations concurrently. Optional unknowns have a bounded five-second TTL and never poison priority cache lookups.
5118
5157
  - Dashboard code work requires the Memory view to be open and visible. Derive starts share a five-second floor; known topology lives at most thirty seconds, with current note rows refreshed before publication. A single hardened native HEAD probe establishes a bounded HEAD/ref proof; cache checks do not launch Git.
5119
5158
  - Console task counts use cache-only optional context and return unknown on a miss within the existing task-detail deadline. Snapshot the native brief cap before awaits. Preserve stored nodes when adding derived edges and apply tier/kind/peer filters to neighbour notes; a non-symbol selection resets the toggle.
5159
+
5160
+ ## Amendment: the planner reads any task and cancels its untouched declined task (#812 AC5)
5161
+
5162
+ - **Supersedes** the #208 sentence that refuses every peer but the conductor, a task's owner and its reviewer, and the #438 sentence that limits a planner's cancellation to work with no historical acceptance.
5163
+ - `hub_task_show` also admits the configured planner (a peer holding the `planner` role, `Tasks.isConfiguredPlanner`, `ConductorHooks.isPlanner`) for any task outside the backlog (a staged backlog row stays hidden from agent task tools, #263, and is read through the planner tools; a peer that is also the conductor takes the audited conductor path). It gets exactly the conductor's view (`publicConductorTask` over `Tasks.publicView(task, true)`), so a PII task is still a stub with no history. The read writes no conduct audit event and grants nothing else: the planner cannot assign, escalate, hold or start through the conductor tools, and every other peer is refused with the conductor-role error as before.
5164
+ - `hub_task_cancel` by the planner keeps every check it had (its own proposal, `backlog` or `proposed` state, planner authority, no private turn on ordinary work, the flow revision, recovery and shutdown exclusion) and replaces only the acceptance test with the evidence test `hub_backlog_return_untouched` uses (`taskHasWorkEvidence`): a branch, commit, PR, recorded local check, done, verdict or reopen refuses with the unchanged text `planner may cancel only its own historically unaccepted backlog or proposed work`. An accepted-then-declined task with none of them (the observed case, #454) is cancellable; the history keeps the acceptance and the decline.
5165
+ - Reason: the planner could not read a task it had proposed once an owner took it, and could not clear the board of a task its owner declined untouched. The person's cancellation is unchanged.
@@ -327,7 +327,9 @@ the runner (refusing while one is alive) and append to the receipt's audit:
327
327
  live instance through the lifecycle stop, which verifies the instance and
328
328
  waits until its manifest and registry claim are gone, and is then inspected
329
329
  again. A daemon this operation does not own, or one already released, is left
330
- running. Each project's outcome is recorded as it happens; a disposition that
330
+ running. Its audit entry keeps the operation's error at that moment as
331
+ `cause` (#776): the error itself is cleared on disposal, and without it an
332
+ abandoned operation could not be diagnosed. Each project's outcome is recorded as it happens; a disposition that
331
333
  stops partway keeps the lock, and resume and `--fresh-session` then refuse
332
334
  until `--stop-and-archive` is run again and finishes. A stopped project's committed
333
335
  snapshot of this operation moves to `restart.abandoned.<hash>.json`, which is