@staix/agent-hub 0.20.0 → 0.22.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 (183) hide show
  1. package/CHANGELOG.md +66 -3
  2. package/README.md +2 -2
  3. package/docs/agent-notes/README.md +11 -0
  4. package/docs/agent-notes/adapters.md +7 -3
  5. package/docs/agent-notes/benchmarks.md +27 -0
  6. package/docs/agent-notes/budget.md +2 -1
  7. package/docs/agent-notes/bus.md +3 -3
  8. package/docs/agent-notes/daemon.md +17 -8
  9. package/docs/agent-notes/local-worker.md +4 -0
  10. package/docs/agent-notes/models.md +2 -0
  11. package/docs/agent-notes/tasks.md +17 -9
  12. package/docs/agent-notes/tests.md +9 -2
  13. package/docs/bench.md +1 -1
  14. package/docs/data-policy.md +2 -0
  15. package/docs/dogfood.md +39 -4
  16. package/docs/events.md +2 -1
  17. package/docs/memory.md +9 -3
  18. package/docs/operations.md +610 -62
  19. package/docs/profiles.md +13 -1
  20. package/docs/quickstart.md +18 -9
  21. package/docs/research.md +7 -4
  22. package/docs/sdlc.md +54 -10
  23. package/docs/security.md +396 -34
  24. package/docs/smoke.md +133 -0
  25. package/docs/specs/2026-09-19-agent-hub-design.md +1101 -22
  26. package/docs/verification/2026-10-27-0.19.0.md +5 -5
  27. package/docs/verification/2026-10-28-claude-2.1.311-2.1.312-bash-permission.json +635 -0
  28. package/docs/verification/2026-10-28-claude-2.1.311-2.1.312-permission-transport.json +403 -0
  29. package/docs/verification/2026-10-28-claude-implementer-permission-triage.md +123 -0
  30. package/docs/verification/2026-10-28-jcode-0.99.1-usage.md +41 -0
  31. package/docs/verification/2026-10-28-realpath-name-audit.md +222 -0
  32. package/docs/verification/2026-10-28-routing-f0bb8ad0.md +1000 -0
  33. package/docs/verification/2026-10-29-0.21.0.md +61 -0
  34. package/docs/verification/2026-10-29-model-effort-capabilities.md +123 -0
  35. package/docs/verification/2026-10-29-modelrouting-b152ddb9.md +953 -0
  36. package/docs/verification/2026-10-30-0.22.0.md +49 -0
  37. package/docs/verification/2026-10-30-445-capability-probes.md +172 -0
  38. package/docs/verification/2026-10-30-bus-delivery-operational-settings.md +77 -0
  39. package/docs/verification/2026-10-30-jcode-ac4.md +24 -0
  40. package/docs/verification/2026-10-30-jcode-ac6.md +52 -0
  41. package/docs/verification/2026-10-30-recovery-wait-settings.md +141 -0
  42. package/docs/verification/claude-implementer-permission-triage.ts +160 -0
  43. package/docs/verification/claude-permission-transport-native.py +71 -14
  44. package/docs/verification/profiles/YYYY-MM-DD-research.md +1 -1
  45. package/docs/verified.json +15 -15
  46. package/package.json +2 -2
  47. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  48. package/plugins/agent-hub/server.js +1016 -280
  49. package/src/adapters/acp.ts +79 -3
  50. package/src/adapters/channel-peer.ts +4 -0
  51. package/src/adapters/claude-channel.ts +13 -6
  52. package/src/adapters/claude-implementer.ts +60 -7
  53. package/src/adapters/codex-appserver.ts +77 -4
  54. package/src/adapters/conformance/fakes/jcode-agent.ts +6 -2
  55. package/src/adapters/conformance/kit.ts +3 -9
  56. package/src/adapters/definitions/builtin-metadata.ts +3 -114
  57. package/src/adapters/definitions/native-config-defaults.ts +1 -1
  58. package/src/adapters/definitions/native-config-policy.ts +3 -2
  59. package/src/adapters/definitions/native-context.ts +12 -0
  60. package/src/adapters/external-metadata.ts +48 -0
  61. package/src/adapters/external-modules.ts +63 -3
  62. package/src/adapters/jcode.ts +6 -2
  63. package/src/adapters/local-worker.ts +40 -25
  64. package/src/adapters/native-proof-ports.ts +10 -0
  65. package/src/adapters/registry-types.ts +4 -0
  66. package/src/broker/protocol.ts +203 -0
  67. package/src/broker/server.ts +314 -0
  68. package/src/broker/store.ts +548 -0
  69. package/src/cli/bench.ts +3 -2
  70. package/src/cli/broker.ts +58 -0
  71. package/src/cli/conductor-recovery.ts +142 -0
  72. package/src/cli/configured-acp-entry.ts +26 -6
  73. package/src/cli/console-state.ts +59 -17
  74. package/src/cli/console.ts +17 -2
  75. package/src/cli/facts-hook.ts +3 -1
  76. package/src/cli/help.ts +29 -16
  77. package/src/cli/identity.ts +25 -10
  78. package/src/cli/init.ts +34 -7
  79. package/src/cli/intake-output.ts +33 -0
  80. package/src/cli/learn-output.ts +38 -0
  81. package/src/cli/main.ts +334 -73
  82. package/src/cli/output.ts +41 -1
  83. package/src/cli/recovery-package.ts +18 -1
  84. package/src/cli/recovery-waits.ts +26 -0
  85. package/src/cli/role-holders.ts +35 -0
  86. package/src/cli/room.ts +304 -0
  87. package/src/cli/say-input.ts +16 -0
  88. package/src/cli/schedule-output.ts +49 -0
  89. package/src/cli/sdlc-output.ts +44 -6
  90. package/src/cli/smoke-pty-bridge.ts +82 -0
  91. package/src/cli/smoke-pty.ts +153 -0
  92. package/src/cli/smoke-run.ts +139 -0
  93. package/src/cli/smoke-target.ts +130 -0
  94. package/src/cli/source-output.ts +29 -0
  95. package/src/cli/task-output.ts +3 -2
  96. package/src/cli/upgrade-runtime.ts +112 -28
  97. package/src/cli/upgrade.ts +68 -8
  98. package/src/hub/ask.ts +76 -12
  99. package/src/hub/board.ts +132 -22
  100. package/src/hub/bus.ts +416 -65
  101. package/src/hub/child-process.ts +2 -0
  102. package/src/hub/ci-watch.ts +42 -20
  103. package/src/hub/claude-failure-stage.ts +85 -0
  104. package/src/hub/claude-shell-wrapper.ts +11 -5
  105. package/src/hub/conductor.ts +17 -4
  106. package/src/hub/config-apply.ts +34 -6
  107. package/src/hub/config-reload.ts +7 -5
  108. package/src/hub/config-trust.ts +7 -1
  109. package/src/hub/context-window.ts +33 -3
  110. package/src/hub/control-client.ts +14 -2
  111. package/src/hub/corpus-config.ts +34 -0
  112. package/src/hub/corpus-evidence.ts +184 -0
  113. package/src/hub/corpus.ts +373 -0
  114. package/src/hub/cost-governance.ts +26 -0
  115. package/src/hub/daemon.ts +1209 -183
  116. package/src/hub/deliverable-files.ts +8 -2
  117. package/src/hub/delivery-journal.ts +200 -73
  118. package/src/hub/dogfood-profile.ts +32 -5
  119. package/src/hub/dogfood-read.ts +6 -2
  120. package/src/hub/envelope.ts +11 -0
  121. package/src/hub/eval-gate.ts +131 -0
  122. package/src/hub/events.ts +1 -1
  123. package/src/hub/external-instance.ts +69 -0
  124. package/src/hub/facts.ts +15 -5
  125. package/src/hub/folder-trigger.ts +223 -0
  126. package/src/hub/hub-tools.ts +2 -1
  127. package/src/hub/inference.ts +20 -5
  128. package/src/hub/instance-config.ts +69 -3
  129. package/src/hub/instance-execution.ts +6 -2
  130. package/src/hub/instances.ts +32 -4
  131. package/src/hub/intake.ts +125 -0
  132. package/src/hub/learning.ts +117 -0
  133. package/src/hub/lifecycle.ts +7 -3
  134. package/src/hub/limits.ts +12 -5
  135. package/src/hub/native-permission-request.ts +77 -0
  136. package/src/hub/operational-authority.ts +63 -5
  137. package/src/hub/operational-settings.ts +154 -0
  138. package/src/hub/operational-view.ts +1 -1
  139. package/src/hub/packs.ts +132 -0
  140. package/src/hub/path-policy.ts +7 -0
  141. package/src/hub/peer-doing.ts +130 -0
  142. package/src/hub/peers.ts +12 -0
  143. package/src/hub/queue-backpressure.ts +58 -0
  144. package/src/hub/read-tools.ts +2 -0
  145. package/src/hub/recovery-authority.ts +151 -0
  146. package/src/hub/recovery-policy.ts +220 -0
  147. package/src/hub/research.ts +24 -14
  148. package/src/hub/restart.ts +7 -0
  149. package/src/hub/routing.ts +54 -11
  150. package/src/hub/schedules.ts +266 -0
  151. package/src/hub/sdlc-merge.ts +38 -14
  152. package/src/hub/sdlc-metrics.ts +32 -10
  153. package/src/hub/sdlc-queue.ts +7 -1
  154. package/src/hub/sdlc-release.ts +1 -1
  155. package/src/hub/sdlc.ts +37 -7
  156. package/src/hub/security-baseline.ts +38 -0
  157. package/src/hub/settings.ts +27 -2
  158. package/src/hub/shipped-packs.ts +8 -0
  159. package/src/hub/snapshots.ts +72 -6
  160. package/src/hub/source-read.ts +160 -0
  161. package/src/hub/sources.ts +311 -0
  162. package/src/hub/tasks.ts +987 -110
  163. package/src/hub/workflow.ts +1 -1
  164. package/src/local/doc-read.ts +69 -0
  165. package/src/local/proxy.ts +4 -13
  166. package/src/local/tools.ts +37 -3
  167. package/src/local/web-fetch-config.ts +43 -0
  168. package/src/local/web-fetch.ts +183 -0
  169. package/src/memory/brief.ts +85 -13
  170. package/src/memory/capture.ts +1 -1
  171. package/src/rooms/application.ts +114 -0
  172. package/src/rooms/boundary.ts +103 -0
  173. package/src/rooms/commands.ts +294 -0
  174. package/src/rooms/crypto.ts +371 -0
  175. package/src/rooms/link.ts +584 -0
  176. package/src/rooms/policy.ts +82 -0
  177. package/src/rooms/remote-peer.ts +80 -0
  178. package/src/rooms/service.ts +426 -0
  179. package/src/rooms/status.ts +146 -0
  180. package/src/rooms/store.ts +1210 -0
  181. package/src/rooms/wire.ts +465 -0
  182. package/src/ui/index.html +91 -4
  183. package/templates/packs/example.pack.json +13 -0
package/CHANGELOG.md CHANGED
@@ -1,9 +1,72 @@
1
1
  # Changelog
2
2
 
3
- Issue and pull request numbers in the entries for 0.7.7 and earlier refer to the previous repository, archived on 2026-09-30 when this repository's history was rewritten; the one exception is the open smoke-check issue, formerly #12, which moved here as #1. Numbers in newer entries refer to this repository.
3
+ Issue and pull request numbers in the entries for 0.7.7 and earlier refer to the previous repository, archived on 2026-09-30 when this repository's history was rewritten; the one exception is the open smoke-check issue, formerly #12, which moved here as #1. Numbers in newer entries refer to this repository. In newer entries and the docs, `#N` is only an issue or pull request of this repository; a hub board task is written `task N`, without `#` (#879).
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.22.0
8
+
9
+ - Five bus and delivery limits are settings (#489 H3, task 444, #975): `bus.max_attempts` (1-100, default 3), `bus.digest_max` (1-10, default 10), `bus.pull_budget_bytes` (1-65536, default 65536), `bus.notes_kept` (1-10, default 10) and `bus.terminal_history_limit` (1-2048, default 2048), live from `config.local.json` at their next use. Unset behaves as before; only `bus.max_attempts` may rise above what shipped, and a reload never trims queues, resets attempts or prunes history.
10
+ - Five upgrade-recovery waits are settings (#489 H4, task 445, Refs #489): `recovery.reconnect_wait_s`, `recovery.unmanaged_reconnect_wait_s`, `recovery.control_timeout_s`, `recovery.shutdown_wait_s` and `recovery.terminal_idle_wait_s`. Unset behaves as before; every person-visible wait can be raised and never cut below what shipped, a bad or unreadable file is the default, and a wait already running keeps its deadline.
11
+ - `ahub learn list [--all] | accept <id> | reject <id> [--json]` (#514b, person only, refused for agent shells and the configured conductor): the verdict on the recurring-unmet-criterion cards #514a files. `accept` adds that card's one `review_checklist` line to your current checklist in `.agenthub/config.local.json` through the settings writer (this machine only, never a tracked file, `ahub settings undo` puts it back, applies after a restart) and closes the card; `reject` only closes it. The line and its task count are rebuilt from the card's key line and the board, never parsed from the card's text. The #514a card sentence "nothing in the hub writes it" is amended to say the hub writes it only on `accept` (earlier cards keep the old sentence). The settings event and undo reply for this key carry a line count, not your lines.
12
+ - The macOS PTY bridge no longer hangs in `Bun.spawn` under load, and no longer outlives its caller (#560, Refs #559). Bun up to 1.4.3 can block for good in `Bun.spawn({ terminal })` when the child writes and exits before the call returns (oven-sh/bun#45300, fixed in 1.4.4); the bridge now starts its child behind a bounded gate it opens after the call returns, and stops the child when its caller is gone. The test retry helper `runStarted` is removed.
13
+
14
+ ## 0.21.0
15
+
16
+ - Tasks and routing operational settings (#489 H2, task 441): six more numbers use the H1 mechanism (fixed rows in `src/hub/operational-settings.ts`, the closed `config-apply.ts` schema, the settings table), all live and with the default equal to the constant they replace: `tasks.demote_at` (1.5), `tasks.demotion_half_life_h` (24), `tasks.contradiction_window_days` (7), `tasks.rescreen_max` (10), `tasks.split_min_observations` (5) and `tasks.split_max_failure_share` (0.3). Each is read where its constant was used, so a change applies at the next demotion read, verdict, re-screen or prediction without a restart, and unset behaves exactly as before. The split thresholds only feed the shadow prediction, which never changes assignment. Nothing here is a safety floor or a contract. `docs/operations.md` lists the other 35 H0 operational items of `tasks.ts`, `routing.ts` and `task-sweep.ts` that stay as they are, and why.
17
+ - Operational numeric settings (#489 H1, #966): one fixed descriptor per setting supplies the bounds, the apply class, the console and dashboard row and the default, through the existing settings path. The worked example is `tasks.groom_fyi_window_min` (1 to 1440 minutes, default 10, live at the next planner notice); unset is the old constant. No new config file, env knob or per-call override, and no conductor authority.
18
+ - `hub_send` recipients (#963, #967): a single peer id given as a string reaches only that peer instead of broadcasting. Any other malformed `to` (null, an empty string, a number, a mixed array) is refused with `not sent: to must be an array of peer ids` before anything is published. An omitted `to` or an empty array still broadcasts, and a reply still inherits its sender.
19
+ - The agent-compat workflow selects its tracking issue by its exact title, never by the label alone (#964, #969), so it can no longer overwrite or close another issue that carries the `agent-compat` label. Two matching issues, malformed input or a truncated issue list fail the step without writing.
20
+ - Status no longer runs git on every write while a terminal Claude instance is bound (#525-5, task 400, #971): the status and stall views read the cached execution binding, and every effect (routing admission, the facts reply, recovery, starts) still revalidates it freshly.
21
+ - Model-routing promotion gate (#637 part 2, task 324): `scripts/benchmarks/model-routing-corpus.json` (44 cases, two splits) with `model-routing-gate.ts` (`--receipt`, `--freeze`, and a by-hand `--live`) and `scripts/check-model-routing-receipt.ts`, run as a second step of the `routing-receipt` workflow. A PR that changes the model-routing keys of `templates/routing.toml` (`hub_routes`, `local`, `fixed_model`, `stay_switch`, the `[pi]` windows) needs a `docs/verification/*-modelrouting-*.md` receipt that CI rebuilds from the corpus and the two files. The corpus drives the real `HubRouteRuntime` with a scripted host, so no model runs in CI; the judge cases and the served model ids come only from the live receipt, which CI checks for shape and consistency (it cannot check that a model said it). No live gateway call has been made yet: the baseline receipt says `live-not-run`.
22
+ - Test timing (#559, follow-up to #548): three tests that tipped over on hosted macOS shards no longer depend on runner speed. The #541 hardlink loops in `test/path-policy.test.ts` are five parts of 1000 iterations per layout (5000 per layout, as before) instead of one test of 15000; the code-facts deadline test in `test/code-facts.test.ts` holds the deadline timers and fires the analysis's only after its child marker exists (a barrier, with the real deadline code), so the descendant exists first whatever the runner's speed; the PTY tests in `test/smoke-pty.test.ts` whose property is not the timeout run once more when the macOS bridge timed out with no output at all and its first attempt's cleanup was confirmed (a measured hang of the bridge's `Bun.spawn` with a terminal under load, local macOS only, not fixed here, so a hosted run can still fail twice in a row). No product change.
23
+ - Conductor-bound recovery continuation and task unassign (task 535, #900): `restart --yes`, `upgrade --yes --to <exact-version>` and `recovery resume|abort` are limited to a source-issued, same-actor, project/target/policy-bound continuation; unsupported coordinator handoff remains person-only. The proof is separate from public recovery JSON, rechecked before effects and does not grant general console commands. `task unassign` retains the conductor actor and manual-offer suppression. Fixture coverage does not establish current installed or real-account behavior.
24
+ - Pin `@staix/agent-mem` 0.13.0 (was 0.12.0). The hub uses the library API; amem 0.13.0's CLI behaviour change (attributed agent and non-TTY sessions, readable default output with `--json` byte-identical) does not change hub recall, capture or briefs.
25
+ - `ahub down` stops this project's hub as the counterpart of `ahub up`. It is the same verified graceful stop as `ahub kill` (owned peers stopped, manifest and registry claim cleared) and stays person-only from an agent shell.
26
+ - The person keeps only trust, money, attribution and interactive terminals in an agent shell (#933, a minor-version change): `permission`, `agents` beyond `--json`, `role assign`, `budget` changes, every `--as-user` form, a launcher's person-only flags, `pet --window`, `watch`, `recovery reattach` and unknown commands are still refused with the same cause line. The configured conductor may now run `board import`, `intake approve`, `schedule run|pause|resume`, `research backfill`, `bench run` (its tasks are proposed as the conductor), `source add`, `goal link`, start the Jcode and Antigravity (`ahub agy`) launchers as `ahub up` already could, and set `tasks.escalate_after`; the hub rechecks the role and records one `agent_cli` row for each (a usage error is refused before that row), and refuses another agent with the operational refusal. Memory changes and the other goal changes stay the person's, because the pinned amem records their actor as the user; `upgrade --yes` keeps the prior person-only rule except for #900's exact source-issued, same-conductor continuation when supported. `tasks.wip_limits.*`, `tasks.auto_pull`, `tasks.queue_backpressure.*` and the routing keys stay the person's. Any agent may `task cancel` its own task as `hub_task_cancel` allows, `task role` (not while it is in review) and `task label` a task it owns (its research label names it, and a person's label for the same task always wins), each refused by the hub for another peer's task, and list `ahub deliverables` for the tasks `hub_task_show` would show it. `route` has no form beyond `explain`, so nothing changed there; restart and `task unassign` are #900.
27
+ - Test fix (#554): a daemon test that holds the snapshot budget clock (`snapshotClock`, #661) still had every git child killed at the remaining budget (250 ms) by `spawnSync`, so a loaded runner timed git out and the owner-write evidence came back unknown (about 3 of 100 runs at 100 parallel in `owner-write-evidence-daemon`). With a host-held clock the child now gets the 10 s ceiling; production, which never sets that clock, keeps the budget-bound timeout and unknown stays unknown.
28
+ - CI (#548): the macOS check leg runs as four shards of the full suite (`AHUB_CHECK_SHARD=i/n` in `scripts/check.sh`, balanced by `scripts/test-timings.json` through `bun test --shard --timings`), with the docs, typecheck, role, bundle and package steps on shard 1 only, so no shard approaches the hang backstop; the macOS backstop returns from 2400 s to the Linux 1500 s. The required name `check (macos-latest)` is now an aggregator over the shards, like `seeded guards`. `bun scripts/make-test-timings.ts <log>` regenerates the timings from a leg log. A shard execs the system git once before its tests (`AHUB_CHECK_WARM_GIT`), because a cold hosted macOS runner's first `/usr/bin/git` execs outlast the 1000 ms some tests allow it.
29
+ - Realpath-name audit (#552, the #541 class): Bun's realpath can name another hardlink of the same file on macOS APFS, so three more callers that decided from the resolved name alone now judge the typed name as well, adding refusals only: `Facts.rel` (secrets denylist, `.git` and hub-owned names), the deliverable file check (hub-owned names, `local.deny`) and `ahub source add` (`pii.paths`, through `screenRegistration`'s new `typedPath`). The typed name is checked when it lies inside the project, by a segment test (`insideRoot` in `src/hub/path-policy.ts`) rather than a string prefix, so a directory called `..x` is judged too. In a shadow repository `Facts.rel` also asks the ignore policy about the typed name, so a path through a symlinked directory is no longer named by that spelling (git cannot judge it); its real path still is. The audit of every `realPath` call site (170 rows) is `docs/verification/2026-10-28-realpath-name-audit.md`.
30
+ - Test fix (#553): the daemon web_fetch test made a real round trip to iana.org through the real proxy and waited 4 s for it, so it failed about 3 of 100 runs at 30 parallel. It now runs against a fixture transport (`DaemonOptions.webFetchTransport`, a host-only test seam like `configWatch`), so it needs no network and no wait. No change to the tool or the proxy.
31
+ - Domain pack format (#513a): `.agenthub/roles/<name>.pack.json` bundles a base profile, the catalog roles it expects, a review rubric whose ids are #507 criterion ids and display-only example checks. `ahub init --profile <profile|pack>` previews what a pack adds (ids only, never text) and applies it as `profile_overrides.review_checklist` and `example_checks`; the same pack again is a no-op, and a different profile or different existing overrides are refused. A pack is data: no field selects or runs a command. A shipped layer (`templates/packs/`, only the format example `example` until #513b adds the writing, analysis and ops packs) loads under the project layer, and a project pack of the same name replaces it. Existing `profile_overrides` that hold the same data in another key order count as already applied. Init does not copy role files and a check is a note, not a command; the spec amendment "domain packs (#513a)" records why.
32
+ - Recurring unmet criteria become a candidate (#514a): reviews that name the same acceptance-criterion id unmet on at least 3 tasks of one profile and class in 30 days that make up at least 2 separate pieces of work (tasks sharing a PR or branch count once) file one backlog card for a person, with the `profile_overrides.review_checklist` line as text (`src/hub/learning.ts`, reusing the #481 S3 recurrence path). The hub never applies it; `profile_overrides` is not machine-local, so the card names both config layers. PII tasks never count.
33
+ - Claim verdicts and fact_check provenance for the research scorecard (#901, follow-up to #799): `ahub task claim <id> <slot> <supported|unsupported|unclear> [--model <label>] [--model-said <...>]` (console only) records a person's verdict on one declared plan claim by its slot, never its text; the hub stamps the `checks` key that ran (and the example citation check's missing count, from its exact last line) on its own `check passed` and `check failed` entries. The research rows for claims reaching a verdict, model label, person verdict, agreement and `fact_check` selection, pass and fail and missing keys per run now fill from those records, and stay unknown, never zero, without them (an older keyless check entry makes the check rows unknown for its group). A PII task counts its verdicts and names no label. Each verdict is bound to a hash of the claim list it evaluated, so replacing or reordering the claims leaves it naming no claim of the new list, and recording one never discards a running check.
34
+ - Local worker path guard (#541): `guardPath` now judges the secrets denylist and the hub-owned names (`.git`, `.agenthub`) on the name the caller typed as well as the resolved one, because Bun's realpath can name another hardlink of the same file on macOS APFS. It only adds refusals, and only for a typed name inside the project (`insideRoot`, #555; a name outside it that links in is judged by its resolved name, as before #541); the flaky `hardlink aliases (#259)` assertion now compares file identity.
35
+ - Dirty submodules are named, not hidden (#798 steps 2 to 5): a turn that leaves a git submodule dirty lists it as `changed (contents unobserved)` in `ahub turns` (`unobserved` in `--json`), a task that declares a path inside a submodule gets a notice at propose and accept (never a refusal), and `ahub undo` says the submodule's contents cannot be restored. A project without `.gitmodules` makes no extra git call; with one, a turn end adds one bounded `git status` probe. A probe that cannot answer records nothing: unknown, never none.
36
+ - Jcode usage and context readings (#801, 0.99.1 only): a Jcode peer's ACP `usage_update` `{used, size}` is now a source-labelled context reading (`jcode_usage_update`) in `ahub status`, the console and the dashboard, and each prompt's result `usage` is recorded as one `jcode_acp` usage event with the provider's own input, output and cache buckets (never Jcode's `totalTokens`, which counts the cached read twice for OpenAI-style providers). Neither feeds a token window or a quota window; a malformed, wrong-session or stale value stays unknown. Jcode still has no native quota reader (`ahub budget` says `no reading`): its `usage --json` gives no window length. Receipt: `docs/verification/2026-10-28-jcode-0.99.1-usage.md`.
37
+ - Pin @staix/agent-mem 0.12.0 (readable `amem doctor`, `status` and `setup` output with next actions; Codex and Qwen setup fixes). No storage or API change for the hub.
38
+ - Eval gate for agent-configuration PRs (#481 S5, mechanism only): `eval_check` and `config_paths` in `.agenthub/sdlc.toml`. A PR touching a configured path (default AGENTS.md, `docs/agent-notes/**`, `templates/**`, role texts, routing.toml, hub tool definitions) requires the named check on every sdlc readback and the guarded merge, and the approval note records its pass rate and suite version. No suite is chosen or defaulted (suite choice pending the person); with no `eval_check` the gate only reports it (a note, never a blocker); code-only PRs and off mode are unchanged.
39
+ - SDLC metrics remainder (#442, #481 S7 and AC12): the guarded merge records the merged PR's review and CI figures once (`cost` on the merge record: CI runs, same-head reruns, GitHub reviews, merge time), `ahub sdlc metrics` shows them with an honest unknown for anything not read, `ahub sdlc release` shows release interval, approval-to-release lead time and hotfix rate, and two new bands, `hotfix_rate` and `security_open_alerts`, use the existing one-card-per-episode path. An opt-in `[security_scan]` table records GitHub's open-alert counts on a schedule (counts only, a source it cannot read is unknown, never zero). The per-release gates-run record stays unbuilt.
40
+ - `ahub source add <path|url>` and `ahub source list` (#508, person-only): register an input as a content-hashed source. The id is `src:` plus the first 16 hex digits of the SHA-256, so the same bytes are one source however often they are added; the snapshot (`meta.json`, `raw`, `text`) lives under the hub state dir `sources/`. A URL is fetched once by the CLI, https only, at most 5 redirects, up to 16 MiB; nothing is truncated. A path under `pii.paths` or text matching `signals.pii_patterns` registers private, and privacy only ever goes up. The model PII screen, `hub_source_read`, plan validation and the dashboard list are later slices.
41
+ - Machine-local schedules (#512 AC1 and AC3): `schedules` in `.agenthub/config.local.json` (entries `{id, every, class, title, detail?, sources?, owner?, queue?}`, `every` as `<n>m|h|d` with a 15 minute minimum, `cron` refused with "cron not supported yet; use every"). Due runs are checked on their own timer, which runs with `task_sweep` off, and propose through the normal task flow: at most one task per schedule per tick, none while its previous task is open, and a held hub skips and coalesces. Last and next run times live in `hub.db` and show in `ahub status`. A committed config cannot set it, and a settings write cannot either.
42
+ - `ahub schedule list|run <id>|pause <id>|resume <id>` and each schedule's next run on the dashboard (#512 AC4 and AC3 UI, person-only): `list` shows every schedule with its next run, state, last run and task (never a title or detail); `run` proposes one now through the same hold, open-task and PII path as the timer, under the timer's single pass guard, and restarts its period; `pause` stops the timer proposing it without counting skipped ticks and `resume` starts a fresh period so a pause is never replayed as a run (a `paused` column on `schedule_runs`, older rows unpaused). `ahub status` shows a paused schedule with the command to resume it, and the Agents view of the dashboard has a read-only Schedules table with a next-run column. Folder triggers are a later slice.
43
+ - `ahub intake "<text>" [--source <id>...]`, `approve <id>` and `discard <id>` (#511 AC1 CLI and AC2 to AC4, person-only): a plain request becomes a draft backlog card (`draft: true`) that nothing offers or pulls. The configured planner is asked to split it with `hub_backlog_split`, or the hub model proposes the split, validated against the same schema and the #507 criteria format; invalid output leaves the draft whole with the reason. Children inherit the mark, a PII request never goes to a planner and is split only by an on-prem model, approval clears the mark on the draft and its parts in one transaction, and discard cancels them through `task cancel`. The dashboard text box is a later slice.
44
+ - Folder triggers (#512 AC2, machine-local): `folder_triggers` in `.agenthub/config.local.json` (entries `{id, folder, glob?, class, title, detail?, owner?, queue?}`) poll a folder on the schedules timer, never watch it. The folder must lie inside the project or a declared `data.readonly` folder (by real path), and a vault, a denylisted or a `pii.paths` folder is refused with a reason. A trigger's baseline (what is there when it starts, over several polls for a big folder) only records; after that a file whose SHA-256 the trigger has not seen is screened like `ahub source add`, registered as a source (private when flagged) and proposes one task that names the source id and never the file's name. Renames and touches propose nothing, symlinks are never read, at most 20 files per poll, and nothing is read while the hub is held. Never editable from settings and ignored in a committed config.
45
+ - `web_fetch` for the local worker (#515 AC1 and AC3, off by default): `local.web_fetch: { "allow": ["docs.example.org"] }` in `.agenthub/config.local.json` lets the on-prem worker read web pages as plain text. It is refused for the whole turn of a PII task, whatever the host, because the URL or a query could carry PII to the web. Otherwise it runs only through its own egress proxy (never the shell's), to the listed hosts over https, as a bare GET with no header or body from the model, re-checking every redirect, and returns bounded text through `sanitize()`; the result and the URL are never captured to memory. A committed config cannot set it and settings cannot change it.
46
+ - `DOING` per peer (#878 P1+P2): `ahub status`, the console Peers table, the dashboard and `hub_status` show what a peer is working on, derived from signals the hub already receives: a Claude session's `ai-title` (read from its transcript where the last read stopped, at most 1 MiB per read) and the in-progress `TodoWrite` or `TaskUpdate` item from its PreToolUse hook; a Codex thread's name and its plan's in-progress step; an ACP plan's in-progress entry. The current item wins over the title, cut to 120 characters. A peer with no source shows nothing. The value passes the public metadata screen and is a `[private]` stub while the peer owns or reviews a PII task, in a private turn, when it was observed in either, or when the screen refuses or fails. It lives in daemon memory only: not in `status.json`, logs, events or memory capture. No agent tool or reporting is added.
47
+ - Jcode peers are told their hub tools are deferred MCP tools (#912): a Jcode session's first prompt now starts with guidance to search for and load `hub_send`, `hub_task_accept`, `hub_task_done`, `hub_review` and the rest from the `agent-hub-jcode` server before deciding a tool is missing, and never to report the hub unavailable unsearched. It is written in one place (`JCODE_DEFERRED_TOOLS_INSTRUCTION`) and sent once per session to the base peer and every extra instance; other peers' prompts, the AGENTS block and the profile templates are unchanged, and Jcode's own deferral and cache settings are not touched.
48
+ - Knowledge corpus config and index (#510 AC1, library only): `corpus.paths` in `.agenthub/config.local.json` names read-only folders of plain notes. A folder that is, lies in or contains an Obsidian vault (a `.obsidian/` entry, checked on the real path, in every ancestor and anywhere below) is refused with a reason naming the Obsidian MCP tools; `pii.paths`, the deny rules and `signals.pii_patterns` apply first, and a root that holds a protected folder (never listed, so not provably vault-free) or any unreadable evidence is refused. The index parses frontmatter, headings and wikilinks, is bounded and incremental, stores no note text and writes nothing. Nothing reads it yet: ask and brief citations are the next slice.
49
+ - Task-routing promotion gate (#637 part 1): a frozen corpus of 47 board-shaped cases with a train/test split and a content hash (`scripts/benchmarks/routing-corpus.json`), a gate script that runs the pure `assign()` and writes a receipt to `docs/verification/` (`bun scripts/benchmarks/routing-gate.ts`), and a CI check (`routing-receipt`) that fails a PR changing `templates/routing.toml` without a receipt that names the file, the current default and the corpus hash, which it rebuilds whole from the corpus and the two files (configurations, changed keys, every metric, every case and the rendered text), so no field of a receipt can be edited. The promotion rule (not worse on owner top-1 accuracy, no PII or refusal regression) is in `docs/agent-notes/benchmarks.md`. Model routing is part 2. The same job publishes a `routing-eval` check run carrying `eval_pass_rate=` and `eval_suite=` markers, so the person can name it as `eval_check` in `.agenthub/sdlc.toml` (#481 S5).
50
+ - `doc_read` for the local worker (#515 AC2 and AC3, off by default): `local.doc_read: true` in `.agenthub/config.local.json` lets the on-prem worker read a PDF, DOCX or HWPX document through the project's `text_projection` converters, inside the projection sandbox and bounded. The shared path guard, `pii.paths` (by path and by text patterns: an ordinary turn is refused, a PII task turn may read), and `sanitize()` apply, and the text is never captured to memory. A committed config cannot set it and settings cannot change it. `web_fetch` is a separate task.
51
+ - Hub board task ids in the 0.19.0 entries, the 0.19.0 verification report and the user documents are written `task N` instead of `#N`, so GitHub no longer links them to unrelated issues (#879).
52
+ - `ahub sdlc merge <task> [--dry-run]` and the conductor's `ahub doctor --orphans --kill` print for a person by default (#899, the last two sites of #814): the merge shows the mode, the decision, every gate that refused it, the `gh` command and GitHub's readback after a run; the orphan cleanup shows the pids the hub stopped and whether any candidate stayed unconfirmed. `--json` prints exactly what each printed before, and the exit codes are unchanged. The print guard's not-yet-converted list is empty.
53
+ - Rooms across machines (#446 X1 to X5b), all person-run and off until a room exists:
54
+ - authenticated room crypto and wire primitives (#908);
55
+ - a blind broker, `ahub broker serve`, that relays room ciphertext with bounded queues, plus `ahub broker room drop` (#910);
56
+ - outbound membership with out-of-band enrollment and signed presence, through `ahub room create|invite|join|leave|revoke|expose` (#922);
57
+ - screened chat between native peers across rooms, with durable signed receipts (#941);
58
+ - room metadata in the console (panel `8`) and the dashboard (#956);
59
+ - a durable person-only inbox for remote replies to `user@label`, through `ahub room inbox` (#959).
60
+ - Opt-in queue backpressure for automatic task offers (#800): `tasks.queue_backpressure.max_queued` and `.max_oldest_s`, both machine-local and 0 (off) by default (#927). Held offers resume after an admission disconnect (#951).
61
+ - Opt-in context-headroom admission (#802): `context.max_assignment_used`, machine-local and 0 (off) by default. New automatic owner offers skip a peer whose fresh current-session context occupancy is at or above it; unknown occupancy never blocks (#961).
62
+ - Codex planner-only instances must confirm a read-only sandbox, and task show, snapshots, the console and the dashboard name the stage holders with their reasons (#905).
63
+ - External module-kind instances are bound to host proof: the module pin, the catalog, the actual owned spawn, the worktree and the MCP identity (#754, #894).
64
+ - Failed owned Claude turns record a bounded host-observed failure stage, and PII withholds it (#950).
65
+ - Merge queue: a narrower input fence, a recovery recheck at dispatch, and the draft blocker pattern now matches (#786, #928).
66
+ - The legacy builtin adapter catalog is retired, with no behaviour change (#625, #903).
67
+ - Docs: native model and effort control capabilities verified per vendor (#266 AC1, #952); headless Claude permission transport characterized (task 544, #942).
68
+ - Test fixes for flakes under load: #896, #898, #939, #940, #946, #947.
69
+
7
70
  ## 0.20.0
8
71
 
9
72
  - Pin @staix/agent-mem 0.11.0 (ranked search explanations, dated eval sets, and import and consolidation boundaries: Claude Code project-directory encoding, accepted-only relation targets, consolidation review receipts that survive pruning). Every amem that shares a memory home must run 0.11.0 or later to keep those receipts, so upgrade a global `amem` CLI too. Refs agent-mem#13.
@@ -29,10 +92,10 @@ Issue and pull request numbers in the entries for 0.7.7 and earlier refer to the
29
92
  - Qwen dual output: Qwen's own background-task notice is not the person's prompt and ends no hub turn (#861).
30
93
  - Conductor operations keep their own error and are audited as refused only on an authority refusal; safe history is cached (#812, #839).
31
94
  - CI admission retries a transient GitHub 5xx or 429 twice inside its deadline and stays fail-closed otherwise (#872, #873); check-runner receipt saves are serialized per run (#867, #868).
32
- - The suite watchdog stops a run on lost progress, not elapsed time (#516): `scripts/progress-tee.sh` forwards bun's output and appends one byte per test result line or file header to a progress file, and `scripts/hang-watch.sh` kills a suite whose progress file has not grown for `AHUB_CHECK_QUIET_S` seconds (300 by default), printing the process table below it first. `AHUB_CHECK_HANG_S` stays as a backstop (1500 s on Linux and 1800 s on macOS in CI, with 35 and 40 minute job limits). A slow suite that keeps passing tests is no longer cut at the total bound.
95
+ - The suite watchdog stops a run on lost progress, not elapsed time (task 516): `scripts/progress-tee.sh` forwards bun's output and appends one byte per test result line or file header to a progress file, and `scripts/hang-watch.sh` kills a suite whose progress file has not grown for `AHUB_CHECK_QUIET_S` seconds (300 by default), printing the process table below it first. `AHUB_CHECK_HANG_S` stays as a backstop (1500 s on Linux and 1800 s on macOS in CI, with 35 and 40 minute job limits). A slow suite that keeps passing tests is no longer cut at the total bound.
33
96
  - Pin @staix/agent-mem 0.10.0 (standalone MCP screening/ranking and capture-worker prefetch improvements; unchanged hub policy and storage contracts). Refs agent-mem#13.
34
97
  - Pin @staix/agent-mem 0.9.1 (split-home sync holds a verified foreign-generation prune as "held, not applied" instead of refusing the sync, cached MCP policy screening, `--capture-days` claude-mem capture import; 0.9.0 was never published). Refs agent-mem#13.
35
- - Tests and docs: the snapshot clock is held in the projection-surface daemons (#515, #854), benchmark Python bytecode is ignored (#840), and the cut-payload guard is stated as refusing session-wide grants (#843, #860).
98
+ - Tests and docs: the snapshot clock is held in the projection-surface daemons (task 515, #854), benchmark Python bytecode is ignored (#840), and the cut-payload guard is stated as refusing session-wide grants (#843, #860).
36
99
 
37
100
  ## 0.18.0
38
101
 
package/README.md CHANGED
@@ -5,7 +5,7 @@ Code, Codex, Kimi Code, Pi and a hub-owned local worker (plus Qwen Code, Jcode a
5
5
  Antigravity if you want them) share a message bus, a task board and a memory, each
6
6
  connected through its own native interface.
7
7
 
8
- Status: 0.20.0, control protocol 16. Bun + TypeScript, one loopback daemon per
8
+ Status: 0.22.0, control protocol 16. Bun + TypeScript, one loopback daemon per
9
9
  project. Site: [agent-hub.staix.net](https://agent-hub.staix.net).
10
10
 
11
11
  ## Quick start
@@ -94,7 +94,7 @@ uses the home must run a version that understands the new layout.
94
94
  [#258](https://github.com/STAIxBWLB/agent-hub/issues/258)).
95
95
 
96
96
  Live checks that still need a real account or a person are listed in the
97
- [release verification report](docs/verification/2026-10-27-0.20.0.md) and the
97
+ [release verification report](docs/verification/2026-10-30-0.22.0.md) and the
98
98
  [smoke checklist](docs/smoke.md).
99
99
 
100
100
  ## Documentation
@@ -54,3 +54,14 @@ Background behind the 0.19.0 rules:
54
54
  - Code-scope verdicts (#852, tasks.md): two review rounds showed that any classifier reading a `code` reviewer's unmet text files real defects, such as "the CI workflow is missing the trigger", as verification-only, so the evidence-only rule reads only the structured findings.
55
55
  - Policy-table fixtures (#843, tests.md): a fixture with a fixed list of keys or versions, which a planned `COMPAT` entry was not in, was reported twice in #843.
56
56
  - Suite watchdog (#516, tests.md): a total-time kill cut suites that were still passing tests line by line (hosted ubuntu runs at 1080 s, the macOS leg of PR #857 at 1440 s), so a lost-progress bound became the hang detector and the total bound only its backstop.
57
+
58
+ Background moved out of the area notes for 0.21.0:
59
+
60
+ - Cold shim exec (#519, tests.md): the first exec of a newly written executable cost about 250 ms on macOS against 6 to 8 ms afterwards, the whole of a 250 ms product bound; `test/memory-code-head-684.test.ts` failed 24 of 30 isolated runs until it warmed the shim. The real probe with a plain `git` measured 10 ms at worst over 600 calls under CPU load.
61
+ - Held snapshot clock (#550, #554, tests.md): `test/settings-reload-daemon.test.ts` without `snapshotClock` failed 138 of 150 single-test runs at 50 parallel, 0 of 150 with it. Before #554 a held clock still left each git child 250 ms, so a loaded runner turned a slow git into unknown evidence (3 of 100 at 100 parallel in `owner-write-evidence-daemon`); the fix landed in `src/hub/snapshots.ts`, outside the planned paths, with the planner's approval.
62
+ - macOS shards (#548, tests.md): counting the shard names in `gateJob()` was planner-approved outside the card's paths. In the first hosted run of PR #954 a cold `/usr/bin/git` took over a second, and 8 tests that run it with `PATH=/usr/bin:/bin` and a 1000 ms timeout were killed at about 1000 ms, against about 140 ms in an unsharded leg that had run git earlier. Without the `AHUB_CHECK_SHARD` unset, any shard but 1 skipped the docs step `test/check-lock.test.ts` expects.
63
+ - Status ordering (#531, tests.md): the slow-snapshot status test took 0.52 s idle and failed 17 of 20 runs with 64 CPU spinners on 4 cores at a 1 s bound.
64
+ - Config watcher off (#546, tests.md): the pet reload test failed 8 of 200 runs at 40 parallel on 18 cores until the watcher was off, and 0 of 200 after.
65
+ - Web fetch transport (#553, tests.md): the daemon test's answer waited on iana.org for 99 percent of its time and failed about 3 of 100 runs at 30 parallel on a 4 s bound; a longer wait would only have measured the network.
66
+ - Jcode usage (#801, budget.md): Jcode's `totalTokens` adds the cached read on top of an input that already holds it for OpenAI-style providers, and `jcode usage --json` names its limit with free text and no window length.
67
+ - Claim verdicts (#901, tasks.md): review of #937 found `factCheck.agreement` counting person-verdict frequencies, which is why a person verdict alone is never an agreement.
@@ -7,13 +7,13 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
7
7
  ## Adapter definitions, the registry and external modules (#451)
8
8
 
9
9
  - Keep per-kind native policy in `src/adapters/definitions/` and consume it through the registry. Private CLI implementations accept only fixed host-bound ports; keep identity, audit, lifecycle admission and effect ordering in the host. Except for the verified person-owned A4 startup-module contract, do not add config-selected callbacks or runtime imports from registry leaves into eagerly initialized hub/CLI modules; use type-only ports for existing host planners.
10
- - Adapter registry (#451): kind-specific policy lives in that peer's `AdapterDefinition` under `src/adapters/definitions/` (runtime definitions `NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN` in `builtin-metadata.ts` plus `qwen.ts`; the daemon constructs every native driver through `start.create` and `DAEMON_PORTS`; `BUILTIN_DEFINITIONS_WITHOUT_QWEN` is the frozen A1 export, read by no runtime consumer). Outside `src/adapters/`, read a semantic field of `definitionOf(id)`: never a raw peer-id comparison, a resolved kind (`kindOf`, `definitionOf(...).kind`) or a constant alias compared with a native name, a vendor-keyed object, a peer-name array, or a closed-list import from `peer-catalog.ts`, `start-mode.ts` or `permission-mode.ts`; the guard resolves aliases and import/re-export bindings. `test/registry-guard.baseline.json` is `[]` and stays empty: a new site moves into a definition, never into the baseline. The only exceptions are the cross-release literals `KNOWN_PEERS`, `OPTIONAL_PEERS` and `START_MODE_PEERS`, which `test/registry-guard.test.ts` holds equal to the derived lists. `LaunchContext.nativeCommand` carries only validated config argv (or its redacted preview), never model input; a definition's `permission.grants` text is what `permissionGrant` shows, ahead of the built-in wording. Keep native mode-id maps in `definitions/*-native.ts`, free of registry factories (`test/adapter-import-order.test.ts`).
10
+ - Adapter registry (#451): kind-specific policy lives in that peer's `AdapterDefinition` under `src/adapters/definitions/` (runtime definitions `NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN` in `builtin-metadata.ts` plus `qwen.ts`; the daemon constructs every native driver through `start.create` and `DAEMON_PORTS`). Outside `src/adapters/`, read a semantic field of `definitionOf(id)`: never a raw peer-id comparison, a resolved kind (`kindOf`, `definitionOf(...).kind`) or a constant alias compared with a native name, a vendor-keyed object, a peer-name array, or a closed-list import from `peer-catalog.ts`, `start-mode.ts` or `permission-mode.ts`; the guard resolves aliases and import/re-export bindings. `test/registry-guard.baseline.json` is `[]` and stays empty: a new site moves into a definition, never into the baseline. The only exceptions are the cross-release literals `KNOWN_PEERS`, `OPTIONAL_PEERS` and `START_MODE_PEERS`, which `test/registry-guard.test.ts` holds equal to the derived lists. `LaunchContext.nativeCommand` carries only validated config argv (or its redacted preview), never model input; a definition's `permission.grants` text is what `permissionGrant` shows, ahead of the built-in wording. Keep native mode-id maps in `definitions/*-native.ts`, free of registry factories (`test/adapter-import-order.test.ts`).
11
11
  - Registry load runs `assertDaemonEngine` and `assertNativeCapabilityScope` before publication. A definition that is not a fixed builtin (object identity in `fixedBuiltinDefinitions`, never a kind name) may not declare `start.acpCommand`, `attach.admission`, `budget.resetSoftTokensOnResume`, `session.crashIdentity`, `session.nativeRevert`, a `pi-rpc` or `claude-implementer` daemon port, `identity.currentChild`, `routing`, `projection`, `reviewerVariant`, `headlessVariant`, `reviewer`, `hooks.factsHook`, `cli.entry`, `cli.preview`, `cli.usage` or `instances.modelSelect`. Only `definitionOf(id, instances)` picks a kind's `reviewerVariant`, for a declared extra with `start_mode: "headless"`, roles exactly `["reviewer"]`, and either a variant whose `reviewer.declaration` is `"automatic"` (Claude, agy) or `review_mode: "fresh"` (Codex, Kimi); without that opt-in a reviewer-only Codex or Kimi instance keeps its native adapter. It picks a kind's `headlessVariant` (Claude, #525-4) for a declared headless extra whose roles include `implementer` and hold only `implementer` and `verifier`. Metadata-only reviewer variants never establish native peer support; `isSupportedPeer` shares the registry runtime/validated-instance predicate with the catalog and daemon. A new field that depends on fixed-builtin assumptions or host-global native state joins that list (`test/adapter-capability-scope.test.ts`). `trust.onPrem` admits PII work (rule: `tasks.md`).
12
12
  - Native branch ratchet (#625): `test/daemon-native-branches.test.ts` counts every call of a native class predicate (an `is*Peer` imported from an adapter module other than `registry.ts`) per enclosing function in `src/hub/daemon.ts` and `src/hub/claude-launch-proof.ts`, and the counts must equal `test/daemon-native-branches.json` (`predicate`, `function`, `count`, `reason`). A PR that adds, moves or removes such a call edits its row by hand with a concrete reason; never regenerate the table from a test. Bind a predicate alias with `const` only and never call one through a computed namespace key: the scan refuses both.
13
13
  - Native proof state (`frontendProxy`, builtin `statusNotice`, `revertTurn` with `session.nativeRevert`, the Claude launch proof and terminal execution root) is read in the daemon only after `isFixedBuiltinDefinition(definitionOf(peer))`: a same-name foreign definition gets none of it. A product of a definition that is not a fixed builtin may not have a `frontendProxy`, `statusNotice` or `revertTurn` member at all, even undefined (`assertNativeProofPorts`, run by `createFromDefinition` and the conformance kit). Use `frontendProxyState` for the daemon proxy reads: absence keeps the generic path, present undefined refuses with `frontend proxy state unknown`. Keep approval policies and restoration debt inside CodexPeer. The remaining Codex `actionEnforcement`/`actionGated` predicate is intentional until a separately specified action-policy port; never add it to `FrontendProxyPort`.
14
14
  - Adding a native peer: declare its `AdapterDefinition` under `src/adapters/definitions/` (or its own directory, as agy) and list it in `ADAPTERS` (`src/adapters/registry.ts`). `KNOWN_PEERS`, `OPTIONAL_PEERS`, `SUPPORTED_PEERS`, `START_MODE_PEERS` (a kind with both a TUI and a headless form), `ROLE_ORDER`, `HELP_ORDER` and `PLANNER_PEERS` are derived from it by `deriveAdapterLists`: set `optional`, `projection` (`roleRank`, `startRank`, `helpRank`, `plannerEligible`; a configured or external definition cannot) and, for an `ahub <kind>` launcher, `cli.entry`, `cli.usage`, `cli.preview` and `cli.startWords`, never a list edit (`builtinCliEntries(definitions)` projects the entries). The literal cross-release copies `KNOWN_PEERS` and `OPTIONAL_PEERS` in `src/hub/peer-catalog.ts` and `START_MODE_PEERS` in `src/hub/start-mode.ts` stay hand-kept for older coordinators: `test/registry-guard.test.ts` fails when one differs from the derived list. Adding a peer is the one change that updates their probe counts and `VENDORS` in `test/helpers/registry-inventory.ts` (`REGISTRY_COMPATIBILITY_PROBES`), the probe literal in `test/adapter-registry-a2.test.ts`, the `ADAPTERS` order in `test/adapter-registry.test.ts` and the literal in `test/adapter-catalog-a2.test.ts`; say so in the PR as an expected test change. `isSupportedPeer` answers from `ADAPTERS`, and for a declared instance from its kind's runtime definition unless the instance resolves to an unavailable reviewer variant; `isKnownPeer` and `defaultPeerRoles` answer from `definitionOf`; `STARTABLE_PEERS` reuses `SUPPORTED_PEERS`.
15
15
  - When an adapter lands, add its project config directories to `AGENT_CONFIG_SEGMENTS` (`src/hub/permission-mode.ts`); scoped automatic writes must ask there, while explicit approval and `never-ask` keep their existing behavior.
16
- - Load external modules only at daemon startup from trusted untracked local declarations: verify the self-contained bundle and content pin before importing captured bytes. Keep CLI/reload paths metadata-only, modules restart-bound and never settings-editable, builtin capabilities restricted, and external-kind instances refused in A4 (follow-up #287). Export conformance registrations and fake assets without starting them on import.
16
+ - Load external modules only at daemon startup from trusted untracked local declarations: verify the self-contained bundle and content pin before importing captured bytes. Keep CLI/reload paths metadata-only, modules restart-bound and never settings-editable, builtin capabilities restricted, and external-kind instances admitted only under the accepted #754 headless opt-in, live captured catalog, separate worktree and host-managed current-owner proof; preserve the live-module controlled-recovery refusal. Export conformance registrations and fake assets without starting them on import.
17
17
  - Bind scanned external runtime SDK imports to this daemon's own `/adapter` entry; never resolve them against project packages. Refuse `/conformance`, `bun:ffi`, `bun:test` and CommonJS loading at runtime. Isolate module refusals from hub startup and retain unknown module roster rows as inert when metadata is unavailable.
18
18
  - Require the immutable captured project catalog at instance, roster, execution and permission resolver boundaries; choose a builtin catalog explicitly only for builtin-only callers; accept `adapters` only as wholly machine-local trusted data and require restart for changes. Refuse missing/mismatched pins, non-echoed load sessions, leader/attach argv and unknown owned-group evidence; require an explicit standalone `--no-leader` before argv termination for Grok (kind, executable or pinned native name); signal only the original verified group. Keep unmapped policy unmanaged, absent budgets exempt and read-only instances non-raisable through native acknowledgement plus host/MCP mutation gates. Claim native support only with its isolated T0 receipt.
19
19
  - Consume quota/context through the resolved definition's declared readers; absence stays unknown. Use explicit `attach.externalHello` and `instances` policy for admission, never `identity.mcp`; keep runtime owner/root and reviewer isolation proof in the host. Never let configured `roles` include `conductor`. Keep native stderr, prompt-failure, permission-mode and transport error text out of configured-kind logs and turn-failure reasons. A configured CLI start (`src/cli/configured-acp-entry.ts`) accepts only `--mode headless`, `--as`, `--session-id` and one preview flag, and its preview never shows argv or session values.
@@ -26,11 +26,12 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
26
26
  - Auto-approval covers the hub's own tools by exact `mcp__agent-hub__<name>` title and only ever picks `allow_once`. Identity comes from the permission request title or, when an agent titles the request with the argument JSON (Qwen), from the announced `tool_call` title bound to the call id and resolved against the session's configured MCP servers, never from payload text, a prefix or an unrelated display title. Never widen it to a prefix or replace it with an agent-selected session mode. Configured ACP kinds (`nativePolicy` or `requireOwnedGroup` set) never auto-approve from a request or tool-call title, even an exact hub tool name; under a locked read-only mode, reject any request whose ACP `kind` is not read, search or think, and never offer `allow_always`. Set Kimi `yolo`/`auto` only for its verified native identity and an operator mode; config never-ask waits for a console y, and other ACP agents stay unmanaged on fresh/resumed starts without setting/resetting modes or failing over their reported mode; refuse runtime changes until their map exists; await the advertised mode change after session new/load and before a prompt, and fail startup on refusal. Use the shared `KIMI_MODE_IDS` mapping (`src/adapters/definitions/kimi-native.ts`, re-exported by `src/hub/permission-mode.ts`).
27
27
  - Permission modes (#242) are a person's and live in the daemon's memory only: `permission` and `permission_default` are console-role control messages, `ahub permission` is `console` in `classifyPeerCommand`, and no hub tool or conductor tool sets one. The dashboard and `ahub settings` set one only through `settingsSet` (`src/hub/daemon.ts`, #269), which calls `changePermission` inside `permissionChange` as the console does: never add another path that writes a runtime choice to `permissionModes`; a reloaded project default (#439) is applied only by `applyPendingPermissionDefault`, and only while that peer is absent or offline. Never write a runtime choice to a restart snapshot.
28
28
  - Proven read-only shell grants (#816, `src/hub/read-only-shell.ts`, `read-only-shell-proof.ts`) apply to Pi/local and the #845 headless Claude host wrapper under `ask-when-needed`, at the host permit boundary: parsing grants nothing, and a grant needs the closed program and flag table, trusted executable lookup and the path guard. For Pi/local, between the grant and the spawn the host calls `shellExecutionCheck` synchronously; `ask` re-asks once with `forceAsk` and a second unstable proof refuses without running, and an aborted or superseded turn (`executionGeneration`) never spawns. Never widen the table or skip the recheck to make a command pass.
29
- - Agent shell authority (#812, `classifyPeerCommand` in `src/cli/identity.ts`, `src/hub/operational-authority.ts`): the Allowed read forms live in `peerReadForms` and are admitted only when the daemon also admits the same form for an ordinary tools connection; the conductor family is the closed `conductorOperation` list, each admitted by `admitOperational` (role recheck, closed audit label, no argument text; `queue show`, a settings preview and the authorize handshake recheck through `operationalCurrent` without an audit record). Throw `OperationalAuthorityError` only from an authority fence: only it is audited as refused and gets the conductor or person guidance, and an ordinary operation failure keeps its own error (#839). A new CLI form is `console` until it is added to one of those tables with a test in `test/identity.test.ts`.
29
+ - Agent shell authority (#812, `classifyPeerCommand` in `src/cli/identity.ts`, `src/hub/operational-authority.ts`): the Allowed read forms live in `peerReadForms` and are admitted only when the daemon also admits the same form for an ordinary tools connection; the conductor family is the closed `conductorOperation` list, each admitted by `admitOperational` (role recheck, closed audit label, no argument text; `queue show`, a settings preview and the authorize handshake recheck through `operationalCurrent` without an audit record). Throw `OperationalAuthorityError` only from an authority fence: only it is audited as refused and gets the conductor or person guidance, and an ordinary operation failure keeps its own error (#839). A new CLI form is `console` until it is added to one of those tables with a test in `test/identity.test.ts`. #933: `console` is only trust, money, attribution and interactive terminals; a conductor form joins `CONDUCTOR_DAEMON_OPERATIONS` (its own daemon request calls `admitOperational`) or `CONDUCTOR_LOCAL_OPERATIONS` (its effect runs in the CLI, so the `admit` handshake is its one audit row, sent only after its `LOCAL_OPERATION_ARGS` check in `src/cli/main.ts` passed), never both; a form whose write records the person as actor (amem's `user`) or whose runner inherits the agent markers stays `console`, except the recovery-bearing daemon forms (`restart --yes`, `upgrade --yes --to <version>`, `recovery resume|abort`, #900), which also require their source-issued continuation proof (rule: `daemon.md`), and every reclassified form changes its daemon check in the same PR.
30
30
  - A headless ACP Kimi or Codex whose mode lives in the native agent joins the bus only through `reconcileNativeStart` (`src/hub/daemon.ts`): after `start()` the daemon's current mode is applied, then `bus.add` (it can drain at once), then the state `start()` reached is replayed. Every mode change runs inside `permissionChange`, one per peer at a time.
31
31
  - Generic permission and generation dispatch in the daemon reads the optional `PeerAdapter` ports (`src/hub/peers.ts`, #667): `permissionModeState`, `setPermissionMode` (resolves only after native acknowledgement; changes approval frequency, never the sandbox), `sessionGeneration` and `checkpointUsesSessionGeneration`; never add a native-class predicate (`isAcpPeer`, `isCodexPeer`) for it. An absent `sessionGeneration` keeps the recovery-session/owner-token fallback; a present but undefined one is unknown and refuses. `BasePeer` installs no default generation, and only an explicit `checkpointUsesSessionGeneration = false` (Pi) skips the checkpoint generation fence. A `permissionModeState` of `unknown` or `unmanaged` refuses a change.
32
32
  - Delivery, lifecycle and session dispatch in the daemon reads the same optional ports (#625 slices 2b-2c): `deliveryOnly` (absent or `false` permits `hub_delivery_retry`; `true` or a present `undefined` refuses), `hubOwned` (only `true` lets `peer_stop` stop the runtime), `pendingOwnedProcess`, `form` (a present `undefined` is unknown: `peer_stop` refuses, and a definition with `permission.unmanagedForms` treats it as a TUI), `captureResume`, `detachForRoster`/`resumeFromRoster`, `pullOnly`, `recoveryReady` and `nativeSession` (rules: `daemon.md`). An adapter the hub owns and may stop sets `hubOwned = true`, a delivery-only transport sets `deliveryOnly = true`; never add an `is*Peer` call for any of them (ratchet above).
33
33
  - Codex takes a mode as an `approvalPolicy` override on `turn/start`, and the override persists on the thread: pass the hub's and the TUI's `turn/start` alike through `applyPermissionPolicy`, and return to `ask` only by sending the captured native policy once. With none captured, refuse while the TUI is attached; never just stop sending the override. Keep the captured native policy and the restoration debt per thread (`nativeApprovalPolicies`, `overriddenApprovals`) across detach and `thread/start` or `thread/resume`; clear a debt only on a successful response to its restoring `turn/start` while the mode is `ask` (`settlePermissionRestore`), so a rejected turn keeps it. While a thread owes a restoration with a known baseline, in any mode, never replace that baseline: a `thread/resume` response and the TUI's own `turn/start` can both echo the sticky override, and the restoring turn sends the kept baseline whatever the TUI supplied. That protection is one turn: after the restoration succeeded, a policy the TUI sends is taken as the person's choice, captured and passed through, because the proxy cannot tell a repeated echo from one. A detached proxy's `clearPermissionMode` is never refused; if the native policy is unknown, return a CLI-visible note that the thread keeps its last policy. Attached refusal directs the person to close the TUI, then run `ahub permission codex ask`.
34
+ - Codex planner threads (#525-6, `threadSandbox` in `codex-appserver.ts`, live from `isPlannerOnly` for a declared instance): every `thread/start`/`thread/resume` goes up with `sandbox: "read-only"` and is adopted only when its response positively confirms the `readOnly` policy (`confirmsReadOnlySandbox`). An unconfirmed or malformed reply, a reused RPC id, an `unavailable` policy (a planner with a declared worktree) or a throwing reader refuses that RPC and closes the TUI link before adoption or delivery, and `deliver`, `steer` and `steerText` recheck `threadSandboxReady` right before they send. Never count missing evidence as read-only, and never let a role change promote an unconfirmed thread.
34
35
  - An unanswered `session/set_mode` leaves the native mode unknown, not unchanged: `AcpPeer` invalidates the turn, goes offline, stops its process group and refuses further changes until restarted. Never report the old or the requested mode after a timeout.
35
36
  - A mode grant in `piPermit` or the local worker's `permit` (`src/hub/daemon.ts`) answers as the prompt path does: allowed only while that owner is still the attached one and the call was not aborted. `ask-when-needed` covers `PI_EDIT_TOOLS` on a `grantablePath` only (path rule: `local-worker.md`).
36
37
  - Pi and local approvals (#253): `permit` answers a `ToolApproval` (contract: `local-worker.md`) and `requestPermission` (`src/hub/daemon.ts`) tells the adapter's `ApprovalObserver` what settled it. An expiry counts toward the stop only as `source: "expired"` with `eligibleExpiry` not false (no person answered a request of that peer while it was pending); `person` resets the count, `automatic` (a mode grant, Pi's `always`) never does, and a new turn starts it over. The second one ends the turn, never the peer: local throws `ApprovalWaitStop`, Pi sends `abort_budget` with `cause: "approval"`, and both settle the delivery `completed` with the reason; a Pi whose extension did not announce `approvalTurnAbort` goes offline with the delivery held. `requestPermission` deletes a request from `permissions` before it runs an observer, resolves or reports, and an aborted signal closes it as `cancelled`: a late answer must find nothing to grant, and a failed event, notice or console push never strands the caller.
@@ -48,6 +49,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
48
49
  - Codex: the adapter never sends its own `initialize`. It is a proxy; hub requests use negative ids and their responses must not reach the TUI.
49
50
  - A durable Codex steer stays pending while its native turn runs. Its RPC may answer after `turn/completed`: bound unanswered replies from that turn's end, requeue a definitive late refusal, and hold a late acceptance as uncertain without attaching it to a later turn's audience (#296).
50
51
  - Codex reports each thread it adopts, started or resumed (`onThread`, the `native_thread` event), before any turn on it starts: the upgrade planner (Codex's `session.zeroTurn` in `src/adapters/definitions/native-recovery.ts`, read through `nativeZeroTurn` in `src/cli/upgrade-runtime.ts`, #215) gives each codex `turn_start` to the thread adopted last before it and plans a thread it saw start with no turn (and no rollout) as a fresh start.
52
+ - Peer DOING (#878): an adapter reports a native session title or its plan's in-progress item only through `onDoing` (set by `Bus.add`), with `reset` when it binds a new session or thread; never put that text in a log line, event, message, status notice or memory capture. Codex reads only `thread/name/updated` and `turn/plan/updated` of the bound thread, ACP only a `plan` update inside the current turn; never guess a value for a peer without such a source.
51
53
  - Codex 0.154.0 `agentMessage` items carry `text` and `phase` (not `content[]`); only the last non-`commentary` message of a turn is shared.
52
54
  - Hide `hub_delivery_retry` from delivery-only ACP/local tool lists and omit its retry hint. Treat `AGENTHUB_DELIVERY_ONLY` only as an exposure hint; the daemon must refuse by the actual attached adapter, and native children must not inherit the marker. A Claude push that cannot reach its model reports `failed_safe`; if the daemon refuses that optional receipt state, try `needs_review` and retain uncertainty when neither receipt is confirmed.
53
55
  - ACP `onActivity` is a closed, transient person-only projection. Never publish it through the bus, log/event writers, UI, tools or memory. Emit only for an active turn; latch private envelopes and associated PII work, including unreadable privacy state, and discard pending chunks on turn teardown. Native terminal references do not authorize filesystem/terminal RPC reads.
@@ -100,6 +102,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
100
102
  - Jcode steer (#572): reach a busy owned jcode only through `nativeSteer` on its private server socket, never by cancelling its turn: `ping` must list `soft_interrupt_ids`, `subscribe` uses `target_session_id` (the ACP session id) and `continue_on_disconnect`, `state` must report `is_processing` (a soft interrupt sent while idle starts a turn by itself), then `soft_interrupt {content, urgent: false, client_id}`. `canSend` rechecks the captured turn, busy state, owned server process, socket identity and session immediately before the write. Every refusal before the send is `failed_safe` with nothing sent; a sent message that is not acknowledged, or whose turn cannot be confirmed, is `needs_review`, never `failed_safe` or `completed`. Read `currentTurn()` before sending and adopt only through `AcpPeer.adoptIntoTurn(turn, ...)`, which refuses a different or ended turn; adopted senders join the turn answer's audience (`replyParent` and `replyAudience` over the turn envelopes, so the hop cap holds), and each steer receipt is `accepted` at once and settles with the turn (`completed` on a normal end, `needs_review` on a cancel, failure, stop or watchdog). Headless Kimi and Qwen have no steer: status reports their waiting important messages only through `importantWaitingStatus` (`src/hub/daemon.ts`) past `budget.stale_min`. A turn that ends and restarts between jcode's `state` reply and the write still receives the message, held `needs_review` and adopted into nobody: keep that ceiling until upstream offers an atomic per-turn steer, and never add a cancel-to-deliver path.
101
103
  - Jcode MCP set (#572): `jcode acp` and `jcode serve` start every server named in `<JCODE_HOME>/mcp.json` and the project's `.jcode/mcp.json`, `.mcp.json` and `.claude/mcp.json`, and no flag or variable selects them (`--mcp-tools`, `JCODE_MCP_TOOLS`, `JCODE_DISABLED_TOOLS` and `JCODE_DISABLE_CLAUDE_MCP` do not). Do not paper over it: `ahub doctor` lists the extra names through `jcodeExtraMcpServers` (`src/adapters/definitions/native-cli-jcode-mcp.ts`), names only, those four paths only, no home search, no link followed. Below the project root or home, `.jcode` and `.claude` must be real directories and the file a regular file (per-component `lstat`, an `O_NOFOLLOW` open and an `fstat` identity recheck); a system alias at or above the configured root stays allowed. Revisit if upstream adds a server selector.
102
104
  - Extra Jcode instances (#685, #368): one `JcodePeer` per declared id (at most 8), each with its own `jcode_home`, socket and state. The person authenticates inside Jcode; the hub passes `-p`, `--model` and `--provider-profile` exactly as declared, handles no model credential or URL, and only checks that the home is a directory the person owns (`jcodeHomeRefusal`: no read or write under it). Never put `AGENTHUB_PEER_ID` in the shared `.jcode/mcp.json` entry (`jcodeMcpEntry`): every owned launch supplies it through `peerChildEnv(<id>)` or the TUI launch env, together with its `AGENTHUB_LAUNCH_ID`. The MCP server with no `AGENTHUB_PEER_ID` in tools mode refuses every tool and never defaults to a peer id. The daemon admits a `tools` hello for a peer whose definition is `owned-acp` only when it carries the launch id of that peer's current owned launch (`JcodePeer.ownsLaunch`, from `jcodeLaunches`); never trust the env alone. `jcode --as <id>` never opens a TUI. Declare the semantic `instances.nativeHome` on a definition, never compare the kind name (`registry-guard`); it is a fixed-builtin capability that `assertNativeCapabilityScope` refuses elsewhere. `checkJcodeInstanceHomes` (`src/hub/instances.ts`, run once at daemon startup) refuses a home equal to or nested in the base Jcode's or another instance's; compare homes only through its `canonicalHome` (`realPath`, case-folded on a volume that folds case, inode), never `resolve()` text. The preview of `--as <id>` passes the instance to `previewJcode` so it shows the daemon's real flags.
105
+ - Jcode defers MCP tools (#912, `JCODE_DEFERRED_TOOLS_INSTRUCTION` in `src/hub/envelope.ts`): a Jcode peer lists only its built-in tools until the model searches for an MCP tool, so without a hint it reports the hub tools missing and answers in its final message. The guidance is written in exactly one place, that constant, and `JcodePeer` sends it through `jcodePreamble` (`src/adapters/jcode.ts`) ahead of the role contract, once with a session's first prompt, for the base peer and every extra instance. Never copy it into `STANDING_INSTRUCTION`, `templates/AGENTS.block.md` or a profile template (the Jcode bullet there is the same text in every template, and `test/jcode-deferred-tools.test.ts` pins both), and never change Jcode's own deferral or cache settings from the hub. The tools it names must exist, and the server name must be `JCODE_MCP_NAME`.
103
106
  - Antigravity P1 (`src/adapters/agy/`): keep it optional, cloud, off roster by default, implementer/verifier only and pull-only. Until per-mode native prompt behavior is verified, reject hub permission/unattended mappings, headless starts and resume selectors; do not infer a quota source. The launcher refuses extra instances, any `agy --version` that compat `agy` refuses (1.4.2 and 1.4.7 are T0-verified; other 1.4.x patches launch with a stderr warning, #589; another version needs the person's trust, #691), and a hub without the `native_hook_launch` feature.
104
107
  - Only the person's `ahub agy` launcher manages `.agents/plugins/agent-hub/{plugin.json,mcp_config.json,hooks.json}`. Refuse linked/foreign entries before any write; cleanup only the matching launch. Never write project-level `.agents/hooks.json` or personal Gemini settings. The plugin MCP server is named `agent-hub_agent-hub` by the pinned native client.
105
108
  - Managed native facts use the definition's typed `NativeFactsPort` and the daemon's ephemeral `native_hook_launch` authority. Bind every observation to the daemon, launch, native session, project and current socket generation. Stop requires `fullyIdle`, normal termination and a fresh validated completion row; a missing readback keeps the peer busy. Native fact acknowledgement accepts only SDK ephemeral-message rows in the exact session transcript path. `decodeAgyHook` classifies as `Read` only the observed native read (`view_file` `AbsolutePath`, or 1.4.3 `read_file` `Path`); a `StartLine`/`EndLine` range becomes `offset`/`limit`, so a partial read never counts as a whole-file view. Unobserved edits get no scoped grant or effect attribution. The daemon records Pre/PostToolUse through `Facts.preTool`/`postTool` and offers nothing on them (#555).
@@ -124,6 +127,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
124
127
  - Per-task native sessions live only in `ClaudeTaskSessions` (`src/adapters/claude-task-sessions.ts`, file `claudeStateFile("claude-task-sessions", id)`, ids only). A reserved UUID becomes `--resume` only after `markStarted` saw a stream-json frame naming that session; forget it only when `transcriptPersisted` answers false (unknown keeps it); an unreadable or invalid state file throws, never resets. Chat without a task ref goes to the instance's single `in_progress` task (`chatTask`), else to a `--no-session-persistence` one-off child.
125
128
  - A private envelope refuses the delivery before spawn; an item `admit` refuses is dropped without vetoing the others. Settle `failed_safe` until the child's stdin was released (`inputReleased`), `needs_review` after; a failed group cleanup keeps the handle and stays `busy`.
126
129
  - The child's tools-mode server (`AGENTHUB_PERMISSION_TOOL=1`) lists only `IMPLEMENTER_HUB_TOOL_NAMES`. `hub_permission_request` returns the original input only for the person's allow-once. Automatic Bash grants require the #845 host wrapper, a current owned public task/root/branch, existing read-only or closed task-command proof, and wrapper protocol 1 on request and reply; never return an automatic allow for the original native command. Consume the expiring single-use ticket before rechecking and executing with the fixed host environment, absolute executable and proven cwd; changed proof/task/child/root/branch runs nothing. Require the local-worker mutating Seatbelt profile for check/test/build with an additive deny for all project Git metadata and the canonical hub state. Only closed host Git add/commit/push and gh stay outside it; refuse configured filter/external drivers/textconv for add/commit only (global Git LFS must not disable push/gh), retain the config-include/partial-clone refusal for privileged Git/gh, and disable signing/automatic maintenance. Pin default-branch authority from the remote, never mutable local origin/HEAD. Unknown or unavailable confinement asks. Enabled action classes and `ask` still ask; malformed requests, no hub, expiry or a changed child deny. The original arguments stay in the child's request; the person sees only `permissionPayload` (`src/hub/permission-payload.ts`): the whole value withheld on any screen match, cut with `[cut, N chars]` inside 600 characters. Daemon admission of that child: `daemon.md`.
130
+ - Failure stages (#950): a failed turn's reason comes only from the launch-bound `ClaudeFailureTrace` (`src/hub/claude-failure-stage.ts`), sealed at leader exit and rendered by `formatClaudeFailure`; it is `details withheld` unless `failurePrivate` answers `false` (absent, unknown or a throw is private), and a trace error never changes cleanup or settlement. The tools-mode server denies an invalid `hub_permission_request` locally whatever the hub answers: `acceptsNativePermissionRequest` keeps the approval path's checks, and the strict `validateNativePermissionRequest` only classifies a diagnostic probe (at most `NATIVE_PERMISSION_PROBE_MAX_PER_LAUNCH` per launch); never use it to narrow or widen approval.
127
131
 
128
132
  ## Always-ask (#636)
129
133
 
@@ -14,3 +14,30 @@ Scope: the CooperBench runners, their fixtures, teardown and recovery. Read befo
14
14
  - Treat an earlier journal-less ACP attempt as possibly affected by cancellation overlap (#304) when its recorded peer `state` goes idle without an answer near `watchdogMs` after the last recorded activity, followed by rapid `failure` entries and `qwen_log` prompt-failed lines, then `active_end` as `peer-failure` with that reason. The driver's tap does not record `undeliverable`, main logged no watchdog cancel, and Qwen's busy-refusal wording is still unmeasured: these are candidate signs, not proof, and absence does not clear an attempt. Keep historical results unchanged and the native measurement owed in `docs/smoke.md`.
15
15
  - After #304, an unreported cancelled prompt leaves a cancellation-deadline line in `qwen_log`, a recorded offline state about 30 s after the watchdog, and `active_end` as `native-failure` on the next driver tick. The driver starts each peer once and cannot restart it within the attempt; its in-memory queue ends with the process. The end cause of affected attempts is not comparable across this fix (formerly `peer-failure` or the wall limit); keep that distinction in later collections, per issue #304's Decision, without changing historical scores or results.
16
16
  - The benchmark local-worker tool surface is `src/local/tools.ts`: its `bash` permit now carries a host shell recheck (#816, see local-worker.md). A benchmark driver that stubs `ctx.permit` needs no change, since an absent `shellExecutionCheck` means `valid`.
17
+
18
+ ## Routing promotion rule (#637 part 1)
19
+
20
+ A default that decides who takes a task is changed with a receipt, not by hand. Scope: `templates/routing.toml` (task routing: class peer lists, `local_allowed`, `[signals]`, `[constraints]`, `[pi]` windows) and what `assign()` reads of it. Model routing (`hub_routes`, `local.route`, `fixed_model`, the gateway aliases) is part 2 of #637 and has its own corpus, receipt and check below; this rule's receipt still says `served_models: not applicable`.
21
+
22
+ - **The frozen corpus** is `scripts/benchmarks/routing-corpus.json`: board-shaped cases (class, signals, peer states, reservations, exclusions, dependencies), each with the owner it must get (a peer or nobody), optionally the reviewer and the refusals that must appear. Every case has a `train` or `test` split, and the file carries a content hash over everything but the hash itself, the split included. The cases freeze the roles they route with (`defaults.roles`), so a change to the shipped roles cannot move them. Each split must hold at least one PII case and one refusal case, and a PII case that must reach `local` as well as one that must reach nobody.
23
+ - **Expectations are written from the policy's documented rules** (the `note` of each case names the rule), never copied from a run. A case a plausible default change would flip is the point; a corpus every edit passes measures nothing. `test/benchmarks/routing-gate.test.ts` edits the template eleven ways and requires each to be blocked.
24
+ - **The gate** is `bun scripts/benchmarks/routing-gate.ts`: no flag prints the current default's numbers (it must be 100% on both splits); `--receipt [--base <ref>]` compares the work tree's `templates/routing.toml` with the default at the base (the merge base with `origin/main` by default) on the test split and writes `docs/verification/<date>-routing-<sha8>.md`; `--freeze` rewrites the hash after an intended corpus change. It runs the pure `assign()`: no clock, no network, no model, so CI can run it and `ahub route explain` cannot drift from it.
25
+ - **The rule.** A PR that changes `templates/routing.toml` links a receipt whose candidate is not worse on the primary metric (owner top-1 accuracy on the test split) and regresses no PII or refusal case. Any regression on a PII case blocks, even when the primary metric rises. A case counts as a regression when the base passed it (owner, the stated reviewer and the stated refusals) and the candidate fails it.
26
+ - **CI** is `.github/workflows/routing-receipt.yml` running `scripts/check-routing-receipt.ts`. It fails a PR that changes the template unless a receipt under `docs/verification/` names that exact file (sha256), was made for the current corpus hash and for the default at the merge base, is on the test split, and is, field for field, what the gate would write for the corpus and the two files: the check REBUILDS the whole receipt (both policy summaries, the changed keys, every metric, every per-case outcome, the served-model note) and compares it canonically with the machine block, then compares the text outside the block (the summary and the case table) with what the gate renders from that data. The receipt chooses only its date (`YYYY-MM-DD`, also in the file name) and a short label for the base ref; a receipt edited by hand in any field, or a block with an extra field, cannot pass, and a receipt whose verdict is `blocked` does not pass either. The corpus never changes in the same PR as the default (land the corpus change and its refreeze first). A PR that does not touch the template needs nothing.
27
+ - **A corpus change voids every earlier receipt** (their corpus hash no longer matches). That is intended: the receipt of an old default is evidence about an old measuring stick.
28
+ - **Do not**: edit a case's `expect` to make a candidate pass, relax a guard, move a case between splits to flatter a candidate, or regenerate a receipt from a different base than the merge base. A candidate that fails a case the policy documents wrong is a spec question for the person, not a corpus edit in the same PR.
29
+ - A check that certifies a generated file recomputes ALL of it and compares it whole, never a chosen subset of fields (#321 review: the first version compared the verdict and counts only, so a receipt could carry any configuration, changed keys and per-case table and still be certified). Add a tampering test for every field the file carries, and one for the text around a machine block.
30
+ - **The routing gate is the repository's eval check (#481 S5, person's scope of 2026-10-28).** `scripts/check-routing-receipt.ts --publish` also creates, through the Checks API, one completed check run named `routing-eval` at the PR head, whose title carries `eval_pass_rate=<0..1> eval_suite=<corpus hash, first 12 digits>`: the owner top-1 accuracy of the head's `templates/routing.toml` on the test split, and the corpus version. Those are the two markers `readEvalResult` (`src/hub/eval-gate.ts`) parses. The person names it with `eval_check = "routing-eval"` in `.agenthub/sdlc.toml`. It is a SECOND run because an Actions job's own check run (`routing-receipt`) has a null title and summary, so the hub could read no marker from it; the job needs `checks: write`. A rerun UPDATES the run (the hub reads exactly one completed run of that name at a head). A publish that cannot be made is a `::warning` and never changes the job's exit status, which is the receipt verdict; the hub then shows the eval gate's "pass rate or suite version not reported" gap. With no readable corpus or template the report has no markers and a failed conclusion, never a made-up number. The rate is a score of the shipped default on the test split (100% while every documented rule holds), not a score of the change: the change is judged by the receipt.
31
+ - A reader of many generated files tests the SHAPE of each before it dereferences any field (`receiptShape` in `scripts/benchmarks/routing-corpus.ts`): a block that is only a receipt by `kind` and `version` is skipped, never an exception, so one malformed old file cannot stop the check before a valid receipt beside it is considered (#321 review nit). Test malformed and valid files together, in both orders.
32
+
33
+ ## Model-routing promotion rule (#637 part 2)
34
+
35
+ A default that decides which MODEL answers is changed with a receipt too. Scope: the model-routing keys of `templates/routing.toml`, read as one summary (`modelRoutingSummary` in `scripts/benchmarks/model-routing-corpus.ts`): `[local]`, `[hub_routes.*]`, top-level `stay_switch` and `max_switch_prefill_tokens`, each class's `route`, `fixed_model` and `pi_backend`, and the `[pi]` windows. Peers, `local_allowed`, `[signals]` and `[constraints]` are part 1's. Part 1's receipt hashes and rebuilds the WHOLE template (its `policySummary` is every parsed key), so ANY change to `templates/routing.toml` still needs part 1's receipt, and a change to a model-routing key needs this one as well: two receipts, one per gate. A change that touches no model-routing key (peer order, signals, constraints) needs only part 1's, and this check says so and passes. Narrowing part 1 to task-routing keys would end the double receipt; it is left unchanged here (out of this task's paths) and is the upgrade path.
36
+
37
+ - **The frozen corpus** is `scripts/benchmarks/model-routing-corpus.json`, built from `build-model-routing-corpus.ts` (readable fixtures) and frozen with `model-routing-gate.ts --freeze`. Three case kinds. `selection` cases give the route and alias a class gets on the local worker. `route` cases drive the REAL `HubRouteRuntime` (stage, plan_execute, advisor, escalation) with a scripted host: the "model" is a fake that records which alias was asked and replays the case's judge replies, so what is scored is the routing code and the routing keys, never a model. `judge` cases are conversations whose verdict class (approve or redo, escalate or not) a sound judge returns, written from the judge prompts' own rules; they are scored ONLY by the hand-run live receipt, never in CI.
38
+ - **Expectations are written from the documented rules** (each `note` names one), never copied from a run. Both splits carry a deterministic PII guard and a deterministic hard-override guard, the test split carries a prompt-injection judge guard, and validation refuses a corpus without them. A PII case has an ordinary twin (the PII trace carries no prefill size, the ordinary one does), so it cannot pass on an empty trace.
39
+ - **The gate** is `bun scripts/benchmarks/model-routing-gate.ts`: no flag prints the shipped default's numbers (100% on both splits); `--receipt [--base <ref>] [--out <file>] [--live]` writes `docs/verification/<date>-modelrouting-<sha8>.md` (the name differs from part 1's `-routing-` so neither check reads the other's files). The sha8 is the digest of the model-routing keys, not of the whole template.
40
+ - **The rule.** The candidate is not worse on the primary metric (deterministic case pass rate on the test split) and regresses no PII or hard-override case; when the live half is present, the judge cases follow the same rule (fewer passes or a regressed guard judge case blocks). The receipt's `overall` verdict (its heading, and the gate's exit code) combines both; `evidence.verdict` is the deterministic part only, and the document prints both. A change that needs the live half but was written without it says `Incomplete` in its heading.
41
+ - **The live half is by hand.** A change to an alias, a judge or any key outside the deterministic set (`liveRequired`) moves what a model answers, so the receipt must carry a `live` block made with `--live`: it asks each policy's judge alias the judge cases through the gateway with the request THAT POLICY builds (`judgeRequestFor`: the same prompts and the advisor's route `transcript_max_chars` as `HubRouteRuntime.review` passes them; a test compares it with what the real runtime sends) and writes the model id the gateway REPORTED for each alias (OmniRoute `servedModel`). Each outcome records the sha256 of the request it was asked, and CI recomputes it, so a receipt for a changed cap that measured the default's request is refused. It needs `AGENTHUB_OMNIROUTE_URL` and `OMNIROUTE_API_KEY`, exits 2 without them before any request, and writes nothing when a call failed, the gateway named no served model, or it named the alias. A deterministic-only change may carry `served_models: live-not-run`.
42
+ - **CI** is the second step of `.github/workflows/routing-receipt.yml`, `scripts/check-model-routing-receipt.ts`. It runs no model and has no gateway or key. It REBUILDS everything the files determine (configurations, changed keys, every metric and per-case outcome, the rendered document) and compares it whole, as part 1 does. The live block is the one thing no file determines, so CI checks its shape and consistency only: exactly the test-split judge cases, the alias each policy asks, no failed call, a served id for every alias that is a real model id and not the alias, and scores recomputed from the recorded verdicts. It cannot check that a model said it: a person who reads the receipt owns that.
43
+ - **A corpus change voids every earlier receipt** and never lands in the same PR as the keys it measures. Do not edit a case's `expect` to make a candidate pass, relax a guard, or move a case between splits to flatter a candidate.
@@ -10,11 +10,12 @@ Scope: quota and native context readings, checkpoints, pause, handoff and resume
10
10
  - The status line tee must never fail or slow the render: no throw, original command run with the same stdin, 5 s cap.
11
11
 
12
12
  - Keep native context occupancy (`src/hub/context-window.ts`, readers in `src/adapters/definitions/native-context.ts`) separate from quota accounting; never use Codex's accumulated `total` as occupancy or estimate Pi counters. Host code reads quota and context only through `readNativeQuota` (`budget.ts`) and `readNativeContext` on the resolved definition's declared `budget.reader` / `budget.context`; no declared reader is unknown, never an empty reading (rule: `adapters.md`).
13
- - Context checkpoint completion requires its request id and the current attached peer/native session. Invalidate its transport generation synchronously at a new peer claim, before asynchronous recall or attachment. Never pause, reassign or replace a session on context pressure. Refuse private-turn, PII-pattern and all associated open PII-task summaries (owner or reviewer, including in_review, with the current routing policy; use `isClosedTaskState` to exclude approved and cancelled work) before local persistence or cloud memory; never broadcast checkpoint text.
13
+ - Context checkpoint completion requires its request id and the current attached peer/native session. Invalidate its transport generation synchronously at a new peer claim, before asynchronous recall or attachment. Never pause, reassign or replace a session on context pressure; `ContextWindows.admission` (#802) only copies the reading for new-offer routing and never changes the crossing latch. Refuse private-turn, PII-pattern and all associated open PII-task summaries (owner or reviewer, including in_review, with the current routing policy; use `isClosedTaskState` to exclude approved and cancelled work) before local persistence or cloud memory; never broadcast checkpoint text.
14
14
  - Invalid/stale context readings expose unknown and do not rearm a crossing. Latch a crossing only after its event is accepted; reconsider paused/recovery-held crossings after release with fresh current-session readings. Tee context fields contain finite numeric values or null only, bound to daemon instance, launcher and native session. Context and session go to the per-peer file `claudeStateFile(base, peer)` (the builtin keeps the unsuffixed name); `claude-usage.json` stays one shared file per account.
15
15
 
16
16
  - A provider hard refusal ends a reading override and supersedes a soft evaluation still awaiting its checkpoint; never let the obsolete checkpoint overwrite that pause. An ACP provider quota refusal reports a hard budget reading and confirms the live pause before the adapter requeues work, so the pause fences retries and the existing relay handles assignments. Use the observed window length for an unknown reset; repeated quota refusals while paused do not consume poison-envelope retries.
17
17
  - Kimi's ACP `{used, size}` occupancy is a context reading (`acpContext`, source `acp_usage_update`) bound to the adapter's own session id: never a `tokens` event or a budget window, and an invalid shape reports unknown, not the previous value. A gate crossing sends a checkpoint request only to definitions with `budget.checkpoint` (Claude and Codex); an ACP peer gets the event and the notice (#285).
18
+ - Jcode (#801): its ACP `usage_update` `{used, size}` is a context reading (`jcodeContextValue`, source `jcode_usage_update`, session-bound); its prompt result `usage` is per-turn consumption recorded as one `jcode_acp` `usage` event (`acpTurnUsage`, `onTurnUsage`) and never reaches `addTokens`, a token window or a budget window. Never read Jcode's `totalTokens`, never infer a quota window from either record, and keep a malformed, wrong-session or stale value unknown. Never read `jcode usage --json`. A later Jcode version repeats `docs/verification/2026-10-28-jcode-0.99.1-usage.md` before its readings are claimed.
18
19
  - A quota resume notice replaces the peer's unread resume notices: carry their moves into the new one (`refs.budgetResumeMoves`, or the whole text of a notice that has none), publish it, and only then withdraw the ones it replaces, so a failed publish keeps them (#306).
19
20
  - Quota handoff context reads `QUOTA_HANDOFF_PLATFORMS` (`src/adapters/registry.ts`, each definition's `hooks.recallPlatform`: claude, codex, kimi, qwen, jcode). It is independent of `hooks.recallInjected`: a handoff platform never joins the injected-platform exclusion in recall (`test/adapter-handoff-platforms.test.ts`).
20
21
  - `Budget.report` resolves the reporter and its account through `hooks.definitionOf` (the daemon's startup-bound project catalog; builtin-only when absent) and skips `user`, `hub` and a registered kind, configured and external module kinds included, with no `budget` block or with `budget.quotaExempt` (only local); an unregistered id keeps quota accounting. `budget.instrumented` (execution budget) never exempts quota.
@@ -10,12 +10,12 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
10
10
  - `WsPeer.steer` exists only for a push channel session: set at attach, cleared on detach, roster detach and a pull-only attach, and it refuses anything but important work in the current busy turn (#493).
11
11
  - `replyParent()` decides what a reply answers (highest hop, never the `hub` preface). Use it for deliveries and steers alike, or the hop cap can be reset.
12
12
  - Report a send's outcome from `Bus.publishResult`, never from `publish()`'s bare target list: a hop-capped send answers `ok: true, dropped: "hop"` with the hop-cap notice, and the channel tool and `ahub say` print that notice, never "no peers attached" (#546). A native peer's `onMessage` returns that `PublishResult` on a hop drop, its send tool (Pi, the local worker) reports it as not delivered, and no off-roster retention note follows a hop drop (#550).
13
- - What a peer is handed (`out`) and what it stands for (`originals`) differ once a delivery is condensed: the bus keeps both, registers `out` so `reply_to` resolves, puts the originals back on a `failed_safe` receipt and holds any other failure (a thrown `deliver`, a later `onFailed`, a `needs_review` receipt) as `needs_review` with its originals, for `ahub queue resolve`, and counts no attempt when the peer merely got busy while the delivery was prepared. A delivery with an `important` envelope is never condensed.
13
+ - What a peer is handed (`out`) and what it stands for (`originals`) differ once a delivery is condensed: the bus keeps both, registers `out` so `reply_to` resolves, puts the originals back on a `failed_safe` receipt and holds any other failure (a thrown `deliver`, a later `onFailed`, a `needs_review` receipt) as `needs_review` with its originals, for `ahub queue resolve`, and counts no attempt when the peer merely got busy while the delivery was prepared. A delivery with an `important` envelope or a remote sender is never condensed; a remote recipient (`remoteId`, rooms #941) gets one envelope per delivery with its queue deadline, never a preface, batching delay, condensation or steer.
14
14
  - A condensed digest is sent by `digest`, not `hub`: `replyParent()` skips only the hub's `presence` item, and a reply to a digest must keep the highest hop of what it replaced. On a `failed_safe` delivery or a `retry` resolution the bus puts back the originals, never the digest.
15
15
  - Limits admit what is sent: the envelope `newEnvelope` builds (a reply inherits its parent's sender, `digest` resolves to the originals, `capPriority` applies), never the raw `to` and priority. Admit first and build later, and implicit replies all count as broadcasts.
16
- - A reply is addressed, never broadcast. An adapter that answers a delivery passes `to: replyAudience(envs)`; anything else with an `inReplyTo` inherits that envelope's sender. Leaving `to` empty fans the message out to every peer.
16
+ - A reply is addressed, never broadcast. An adapter that answers a delivery passes `to: replyAudience(envs)`; anything else with an `inReplyTo` inherits that envelope's sender. Leaving `to` out or empty fans the message out to every peer. The control `send` takes a nonempty string `to` as that one peer and refuses any other non-array shape or bad item before publishing (#963); never turn a malformed `to` into a broadcast.
17
17
  - `digest` is not a peer: addressing a reply at the envelopes the peer was handed sends it nowhere once a delivery was condensed. The bus resolves `digest` back through `lastDelivery.originals`; anything else that reads a reply's `to` has to do the same.
18
- - A pull-only peer (a Claude session without channel pushes, Kimi in its own TUI, or agy) is never drained by a push; `Bus.pull` hands it its queue oldest first up to `PULL_BUDGET_BYTES` (64 KiB of `frame` text, #713; `takeBudget`), the first item always going so one huge envelope cannot stall it, and a poison one still alone. A push peer, and an attached channel session that is not pull-only, keep `take`'s `DIGEST_MAX`: never raise that for a pull. The daemon's `inbox` reply carries `waiting` and the plugin says `N more waiting: call hub_inbox again before you start work`. An attached channel session may also pull its own queue through `hub_inbox` while busy (#493); the same holds and journal completion apply. Drain and pull share `held()` (recovery, an uncertain delivery, any pause, the lifecycle `deliveryHeld` fence) and `remember()` (so `reply_to` resolves), and a drain rechecks `deliverable()` after condensing because such a session can take the peer meanwhile. A pull is journaled `completed` in the same transaction as the bus snapshot, never `accepted`; a failed one puts back only that peer's queue and preface and never reloads the snapshot.
18
+ - A pull-only peer (a Claude session without channel pushes, Kimi in its own TUI, or agy) is never drained by a push; `Bus.pull` hands it its queue oldest first up to `bus.pull_budget_bytes` (#489 H3; unset is `PULL_BUDGET_BYTES`, 64 KiB of `frame` text, #713; `takeBudget`), the first item always going so one huge envelope cannot stall it, and a poison one still alone. A push peer, and an attached channel session that is not pull-only, keep `take`'s `bus.digest_max` (at most `DIGEST_MAX`): never raise that for a pull. The daemon's `inbox` reply carries `waiting` and the plugin says `N more waiting: call hub_inbox again before you start work`. An attached channel session may also pull its own queue through `hub_inbox` while busy (#493); the same holds and journal completion apply. Drain and pull share `held()` (recovery, an uncertain delivery, any pause, the lifecycle `deliveryHeld` fence) and `remember()` (so `reply_to` resolves), and a drain rechecks `deliverable()` after condensing because such a session can take the peer meanwhile. A pull is journaled `completed` in the same transaction as the bus snapshot, never `accepted`; a failed one puts back only that peer's queue and preface and never reloads the snapshot.
19
19
  - Record exact task-offer reads only through `DeliveryJournal.completeDelivered` on an active-fenced adapter completion, correlated reply or actual pull, atomically with completion. Capture original offers before terminal pruning and record their ids, never condensed ids; accepted receipts, generic transitions, operator/hub dispositions and restored completed rows are not read evidence.
20
20
  - The `onImportantQueued` nudge for a pull-only peer (#398) is advisory: it never dequeues, steers, journals or settles, and a throw from it never stops an enqueue.
21
21
  - Merge recall and restored prefaces through `mergePrefaces`/`uniqueMemoryLines`; preserve distinct context and new notes, deduplicate memory identities, and keep colliding short prefixes distinct. Use `recallDisplayText`/`recallMemoryLines` before display caps: suppress only matching automatic approval outcomes, retain nonempty rejection details, preserve id headers and complete-word excerpts, and keep token trimming at line boundaries. Keep these changes display-only.