@staix/agent-hub 0.12.22 → 0.12.24

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 +117 -0
  2. package/README.md +22 -9
  3. package/docs/addons/claude-mem.md +24 -0
  4. package/docs/addons/codex-plugin.md +8 -0
  5. package/docs/addons/dot-admit.md +8 -0
  6. package/docs/addons/open-code-review.md +8 -0
  7. package/docs/addons/ponytail.md +23 -0
  8. package/docs/addons/ripwire.md +22 -0
  9. package/docs/agent-notes/README.md +5 -0
  10. package/docs/agent-notes/adapters.md +18 -6
  11. package/docs/agent-notes/benchmarks.md +3 -0
  12. package/docs/agent-notes/budget.md +4 -0
  13. package/docs/agent-notes/bus.md +3 -1
  14. package/docs/agent-notes/daemon.md +14 -1
  15. package/docs/agent-notes/local-worker.md +6 -3
  16. package/docs/agent-notes/tasks.md +6 -1
  17. package/docs/agent-notes/tests.md +4 -1
  18. package/docs/bench.md +28 -3
  19. package/docs/cooperbench.md +4 -0
  20. package/docs/data-policy.md +36 -0
  21. package/docs/events.md +10 -3
  22. package/docs/gsd-collaboration-analysis.md +48 -0
  23. package/docs/memory.md +231 -0
  24. package/docs/operations.md +873 -30
  25. package/docs/plans/2026-10-11-issues-109-sequential.md +81 -0
  26. package/docs/plans/2026-10-11-routing-evaluation-proposal.md +30 -0
  27. package/docs/profiles.md +57 -0
  28. package/docs/quickstart.md +9 -4
  29. package/docs/research.md +31 -1
  30. package/docs/roles.md +85 -0
  31. package/docs/sdlc.md +170 -0
  32. package/docs/security.md +124 -8
  33. package/docs/smoke.md +354 -5
  34. package/docs/specs/2026-09-19-agent-hub-design.md +676 -15
  35. package/docs/specs/2026-10-12-backlog-implementation-plan.md +186 -0
  36. package/docs/specs/2026-10-12-office-view-design.md +212 -0
  37. package/docs/text-projection.md +19 -0
  38. package/docs/verification/2026-10-10-cli-output-phase1.md +309 -0
  39. package/docs/verification/2026-10-10-cli-output-phase2.md +672 -0
  40. package/docs/verification/2026-10-11-0.12.23.md +90 -0
  41. package/docs/verification/2026-10-11-acp-watch-t0.md +33 -0
  42. package/docs/verification/2026-10-11-agent-config-inventory.md +20 -0
  43. package/docs/verification/2026-10-11-jcode-peer-contract.md +27 -0
  44. package/docs/verification/2026-10-13-0.12.24.md +94 -0
  45. package/docs/verification/2026-10-13-board-phone.md +31 -0
  46. package/docs/verification/2026-10-13-fsmonitor-fixture-attribution.md +19 -0
  47. package/docs/verified.json +34 -16
  48. package/package.json +2 -1
  49. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  50. package/plugins/agent-hub/ROLE-NOTICE.md +26 -0
  51. package/plugins/agent-hub/server.js +1108 -261
  52. package/scripts/check-docs.mjs +129 -0
  53. package/src/adapters/acp-quota.ts +13 -0
  54. package/src/adapters/acp.ts +201 -20
  55. package/src/adapters/claude-channel.ts +48 -11
  56. package/src/adapters/codex-appserver.ts +54 -13
  57. package/src/adapters/jcode.ts +110 -0
  58. package/src/adapters/local-worker.ts +10 -7
  59. package/src/adapters/pi.ts +94 -20
  60. package/src/cli/attention.ts +60 -0
  61. package/src/cli/bench.ts +26 -6
  62. package/src/cli/board-read.ts +12 -0
  63. package/src/cli/console-state.ts +131 -39
  64. package/src/cli/console.ts +73 -17
  65. package/src/cli/facts-hook.ts +12 -4
  66. package/src/cli/help.ts +52 -21
  67. package/src/cli/identity.ts +19 -8
  68. package/src/cli/init.ts +48 -10
  69. package/src/cli/jcode-mcp.ts +58 -0
  70. package/src/cli/kimi-mcp.ts +53 -0
  71. package/src/cli/launch.ts +16 -1
  72. package/src/cli/main.ts +641 -193
  73. package/src/cli/memory.ts +193 -0
  74. package/src/cli/output.ts +404 -0
  75. package/src/cli/pet-config.ts +1 -0
  76. package/src/cli/pet.ts +83 -0
  77. package/src/cli/preview.ts +40 -6
  78. package/src/cli/reset.ts +3 -0
  79. package/src/cli/status-lines.ts +1 -29
  80. package/src/cli/tail-render.ts +1 -0
  81. package/src/cli/terminal-recovery.ts +27 -1
  82. package/src/cli/upgrade-runtime.ts +56 -10
  83. package/src/cli/upgrade.ts +23 -5
  84. package/src/cli/watch.ts +31 -0
  85. package/src/hub/activity.ts +53 -0
  86. package/src/hub/ask.ts +10 -5
  87. package/src/hub/bench.ts +10 -3
  88. package/src/hub/board.ts +224 -16
  89. package/src/hub/budget.ts +39 -7
  90. package/src/hub/bus.ts +58 -20
  91. package/src/hub/child-process.ts +11 -0
  92. package/src/hub/citations.ts +7 -0
  93. package/src/hub/conductor.ts +34 -17
  94. package/src/hub/config-trust.ts +20 -7
  95. package/src/hub/conflicts.ts +2 -0
  96. package/src/hub/context-window.ts +8 -1
  97. package/src/hub/crash.ts +11 -1
  98. package/src/hub/daemon.ts +1218 -113
  99. package/src/hub/db-timeout.ts +3 -0
  100. package/src/hub/delivery-journal.ts +7 -1
  101. package/src/hub/envelope.ts +1 -1
  102. package/src/hub/events.ts +14 -2
  103. package/src/hub/execution-budget.ts +2 -0
  104. package/src/hub/facts.ts +28 -8
  105. package/src/hub/git.ts +38 -0
  106. package/src/hub/github-import.ts +28 -0
  107. package/src/hub/hub-tools.ts +49 -10
  108. package/src/hub/inference.ts +1 -1
  109. package/src/hub/lifecycle-schema.ts +17 -0
  110. package/src/hub/lifecycle.ts +10 -4
  111. package/src/hub/listen.ts +15 -0
  112. package/src/hub/manager.ts +26 -6
  113. package/src/hub/path-policy.ts +96 -0
  114. package/src/hub/peer-catalog.ts +28 -0
  115. package/src/hub/peers.ts +5 -3
  116. package/src/hub/permission-mode.ts +33 -5
  117. package/src/hub/planning.ts +70 -0
  118. package/src/hub/profiles.ts +74 -0
  119. package/src/hub/project.ts +2 -1
  120. package/src/hub/projected-diff.ts +73 -0
  121. package/src/hub/projection-exec.ts +24 -0
  122. package/src/hub/recovery-error.ts +31 -0
  123. package/src/hub/recovery-store.ts +6 -5
  124. package/src/hub/registry.ts +2 -1
  125. package/src/hub/research.ts +144 -21
  126. package/src/hub/roles.ts +123 -0
  127. package/src/hub/roster.ts +23 -0
  128. package/src/hub/routing.ts +132 -18
  129. package/src/hub/sdlc-queue.ts +112 -0
  130. package/src/hub/sdlc-release.ts +125 -0
  131. package/src/hub/sdlc.ts +250 -0
  132. package/src/hub/settings.ts +411 -0
  133. package/src/hub/shipped-roles.ts +25 -0
  134. package/src/hub/snapshots.ts +92 -16
  135. package/src/hub/start-mode.ts +50 -0
  136. package/src/hub/supervision.ts +1 -0
  137. package/src/hub/task-sweep.ts +1 -1
  138. package/src/hub/tasks.ts +1014 -111
  139. package/src/hub/terminal-open.ts +64 -0
  140. package/src/hub/text-projection.ts +126 -0
  141. package/src/hub/tooling.ts +159 -0
  142. package/src/hub/ui.ts +40 -14
  143. package/src/hub/workflow.ts +71 -0
  144. package/src/local/deny.ts +29 -7
  145. package/src/local/sandbox.ts +50 -10
  146. package/src/local/tools.ts +11 -6
  147. package/src/memory/brief.ts +28 -14
  148. package/src/memory/import.ts +55 -0
  149. package/src/memory/recall.ts +86 -2
  150. package/src/memory/store.ts +750 -0
  151. package/src/memory/sync.ts +145 -0
  152. package/src/models/mlx.ts +2 -1
  153. package/src/pet/NOTICE.md +10 -0
  154. package/src/pet/characters.ts +1438 -0
  155. package/src/pet/config.ts +31 -0
  156. package/src/pet/frame.ts +17 -0
  157. package/src/pet/pixels.ts +28 -0
  158. package/src/pi/tool-receipts.ts +2 -1
  159. package/src/role-text.d.ts +1 -0
  160. package/src/ui/index.html +983 -40
  161. package/src/ui/needs.ts +58 -0
  162. package/src/ui/office-model.ts +440 -0
  163. package/src/ui/peer-palette.ts +16 -0
  164. package/src/ui/task-board.ts +49 -0
  165. package/src/ui/task-progress.ts +5 -1
  166. package/templates/AGENTS.block.md +3 -2
  167. package/templates/config.json +5 -1
  168. package/templates/profiles/dev.json +22 -0
  169. package/templates/profiles/general.json +26 -0
  170. package/templates/profiles/research.json +57 -0
  171. package/templates/profiles/writing.json +28 -0
  172. package/templates/roles/NOTICE.md +26 -0
  173. package/templates/roles/dev/backend-engineer.md +26 -0
  174. package/templates/roles/dev/code-reviewer.md +26 -0
  175. package/templates/roles/dev/frontend-engineer.md +26 -0
  176. package/templates/roles/dev/security-reviewer.md +26 -0
  177. package/templates/roles/dev/software-architect.md +26 -0
  178. package/templates/roles/dev/test-engineer.md +26 -0
  179. package/templates/roles/research/analyst.md +26 -0
  180. package/templates/roles/research/fact-checker.md +26 -0
  181. package/templates/roles/research/research-lead.md +26 -0
  182. package/templates/roles/research/researcher.md +26 -0
  183. package/templates/roles/research/statistician.md +26 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,123 @@ Issue and pull request numbers in the entries for 0.7.7 and earlier refer to the
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.12.24
8
+
9
+ - Preserve planner notices across queued/uncertain deliveries, stale recipient copies, queue pressure and temporary roster absence (#263).
10
+ - Isolate fixture-owned automatic Git maintenance without weakening document readback security assertions (#369).
11
+ - Keep automatic control listeners clear of requested native peer ports while preserving ownership refusals (#375).
12
+
13
+ - Restore dashboard peer role metadata and show bounded task guidance ids on board cards (#295).
14
+
15
+ - Complete terminal board column/release projections and structured late-cut/unknown approval flags (#263).
16
+
17
+ - Add bounded one-way GitHub issue intake into persistent deduplicated private-safe Backlog (#263).
18
+
19
+ - Add atomic backlog decomposition and bounded unused-child undo with staged DAGs and tool/CLI/dashboard wiring (#263).
20
+
21
+ - Add planner WIP slot admission and bounded pending-pull undo, configured capacity through existing settings, and tool/CLI/dashboard/status wiring (#263).
22
+
23
+ - Extend backlog planning with bounded acyclic dependencies and rostered reservations, retaining revision-fenced undo and unstarted-work floors (#263).
24
+
25
+ - Project shared body-free attention into the dashboard header with built-in SVG characters, accessible counts and navigation-only activation (#280).
26
+
27
+ - Add owner-approved STAIx and four alternative built-in characters, bounded mini/full footers, glyph fallback and session character cycling (#280).
28
+
29
+ - Exclude person/hub-owned work from offline-agent attention and read structural board status facts without decoding task payloads (#280).
30
+
31
+ - Add global body-free attention status, person review/offline facts, read-only recovery observation and token-free pet/prompt output (#280). Native account verification remains unverified.
32
+
33
+ - Add structural console attention counts, an ASCII STAI rule line, read-only `n` navigation, session `P`/`--no-pet`, presentation configuration and plain-mode updates (#280). Native visual checks remain unverified.
34
+
35
+ - Add revision-fenced task planning, person-owned phased release controls, actual-history dates and optional research metadata across CLI, tools and dashboard (#263).
36
+
37
+ - Import the hub's notes from claude-mem into hub memory, add optional model-suggested edges that stay out of evidence until a person confirms them, and measure memory through `memory_brief` counts, a by-memory-brief research split and `ahub bench run --memory` arms (#258, phase 3).
38
+
39
+ - Sync hub memory between a person's machines through a private git remote (`ahub memory sync`), and add long-term tasks with advisory leases and conflict display (`ahub goal`, `hub_task_propose` `goal`, status, recall and the dashboard's Long-term tasks tab) (#258, phase 2).
40
+
41
+ - Add the hub's own memory (#258, phase 1): append-only short- and long-term logs in `~/.agenthub/memory/` with a derived graph index, promotion of decision, contract and fail notes on approval, `hub_remember` `horizon` and `scope`, graph-first task briefs, recall and `ahub ask` evidence, `ahub memory`, and a read-only dashboard Memory section. PII task text and secret-looking notes never enter it.
42
+
43
+ - Add five-column dashboard task cards, ready/waiting split, shared peer colors, agent legend/lanes and scoped drag/button assignment and Backlog staging controls (#263).
44
+
45
+ - Add the Isometric projection to the dashboard Office view: the same model through the same code path with projected grid coordinates, upright labels, identical focus order and accessible names, and jump-on-switch — plus an art polish pass (outline glyphs, drawn raised hand, per-desk avatars, extruded iso desks with legs and name plates, wireframe iso rooms, Done-shelf cabinet/stacks, two-column room cards with dividers, arc arrowheads) guided by generated reference concepts kept outside the repo (#264, phase 4 of 4).
46
+
47
+ - Add optional turn/checking/links snapshot fields for the dashboard office view (exact task bubble, finishing-a-turn, check-running mark, overlap/conflict/cohort warning lines), ids-only and PII-safe with PROTOCOL and EVENTS_SCHEMA unchanged (#264, phase 3 of 4).
48
+
49
+ - Add snapshot-driven motion to the dashboard Office view: card moves by CSS transform transition, message arcs, delivery warning marks and the busy latch from event rows with timer-free keyframe fades, and the settle rule, with reduced-motion support (#264, phase 2 of 4).
50
+
51
+ - Add a static flat Office view to the dashboard: stable desks, state rooms, capped cards, an Off/Flat view control persisted in a second host-scoped cookie, and scene/list highlighting, with no daemon change (#264, phase 1 of 4).
52
+
53
+ - Record each peer's add-ons (claude-mem, ponytail, ripwire, open-code-review, the Codex plugin, dot admission) by name and version in a `tooling` event and the research records, compare tasks with each on and off (`ahub research addons`), declare and check add-on arms in `ahub bench run --addon`, and record a decision per add-on in `docs/addons/` (#248).
54
+
55
+ - Add person-only bounded merge-order and release-readiness evidence through CLI, console and both dashboard modes; retain unknown/native gates and never merge or publish from a report (#257).
56
+
57
+ - Stage unoffered Backlog tasks, reserve suggested owners, and pull/return through person-owned Tasks transitions; hide backlog from ordinary agent and conductor task reads (#263).
58
+
59
+ - Support bounded custom lifecycle stage ids, actors and entry/exit gates across Tasks, projections and managed peer schemas, preserving the approval floor (#257).
60
+
61
+ - Support bounded private/read-only data globs, real wildcard aliases and conservative discovery limits; withhold Claude scoped write grants on read-only data (#259).
62
+
63
+ - Add isolated project profile data overrides across task aliases/plans, schemas, native guidance and init while preserving flow/check authority (#259).
64
+
65
+ - Add explicit non-git shadow repositories for hub facts, snapshots and conservative undo without creating .git (#259).
66
+
67
+ - Add bounded snapshot-based rich-document review and reverse undo text previews with persistent task baselines and owned converter cleanup (#259).
68
+
69
+ - Record immutable attributed-turn catalog-role preparations in opt-in research JSONL, preserving unknown external and legacy evidence (#295).
70
+
71
+ - Add person-only catalog role assignment/clear, task role edits and Roles settings controls with pending guidance metadata (#295).
72
+
73
+ - Add validated persistent task catalog roles with bounded assignment/review guidance and identity-only records (#295).
74
+
75
+ - Add machine-local bounded read-only text projections for rich-document facts while preserving raw attribution (#259).
76
+
77
+ - Add declared private-data routing and managed local/Pi read-only data policy with hardlink-safe command bounds (#259).
78
+
79
+ - Add data-driven research/writing/general project profiles, base-class aliases, document plans and guidance while preserving dev defaults (#259).
80
+
81
+ - Add optional bounded dev/research role catalog, shipped/project hashes and MIT notices, read-only CLI diagnostics and existing session guidance paths; preserve default contracts and authority (#295).
82
+
83
+ - Add opt-in gsd-lite phase checklist guidance over existing board dependencies, reviewer-owned hashed receipts, head-bound approval blockers and read-only state/config exports (#257).
84
+
85
+ - Add person-only `ahub watch kimi|qwen` for bounded, transient headless ACP activity, private-turn stubs and safe terminal rendering (#286). Native TUI verification remains pending.
86
+
87
+ - Add optional owned Jcode 0.94.0 daemon/ACP and project MCP bridge, with unmanaged native policy, default-off roster and explicit native recovery/tool proof limits (#332).
88
+
89
+ - Add initial opt-in SDLC artifact receipts, exact-head approval gates, bounded readbacks and stage/blocker projections; retain off defaults and mark custom/release/live work pending (#257).
90
+ - Add optional Qwen Code 0.25.0 ACP, off roster by default, with its own verified permission table, headless CLI/preview/stop and recovery command; keep unknown quota and native proof explicit (#256).
91
+
92
+ - Default Kimi to its own pull-only TUI, retain selectable headless ACP, exclude automatic routing to pull-only peers, and let people/conductors name reviewers with withdrawal notices. Keep project MCP changes in the person launcher and refuse unsupported TUI recovery (#316).
93
+ - Exclude shared .agents skills and .claude.json from scoped grants; add the person-controlled, live instruction-file protection option, off by default (#323).
94
+
95
+ - Refuse release publication on stale or nonancestor document stamps; require the release merge commit even when the tree matches green CI (#318).
96
+ - Cancel offline ACP permission requests and withdraw pending approvals on watchdog, offline transition and stop (#311).
97
+ - Check original and resolved write/edit paths before scoped Pi/local grants, including symlinked agent configuration directories (#322).
98
+ - Measure console fitting after tab normalization and header sanitization, preventing Events body rows from overflowing (#319).
99
+ - Make the pending-approval watchdog regression use controlled renewals instead of a real-time response margin (#310).
100
+
101
+ - Exclude Kimi Code project configuration under .kimi-code from scoped automatic write/edit grants, alongside the legacy .kimi policy directory (#316).
102
+
103
+ - A person can change the live peer roster through settings, the dashboard or ahub roster: removed peers stop receiving new work while their queued messages, uncertain deliveries and pauses remain available, and all open owner/reviewer roles are offered to another eligible peer; explicit off-roster messages are retained with a public notice and removal reports both moves and work that stays (#315).
104
+
105
+ ## 0.12.23
106
+
107
+ - Console panels: Left and Right move between the five tabs with the digit keys' guards and resets, wrapping at both ends (a read holding only arrow sequences is that many key presses, never a paste), and the Events tab colours each row's header through the stream's own tokenizer on the same tab-flattened header, so an event reads the same in both places (body lines and command output stay plain; with colour off the panel is byte-identical); the tail no longer repeats an unchanged context reading: a context line prints when the printed percentage, freshness, source or known-ness changes for that peer, not at every report (#309).
108
+ - A restart blocked by SQLite contention now records retryable `database_busy` with its offered next action and bounded throw site, settles the first busy inspection without a second probe, keeps a committed source inside its shutdown wait, clears stale failure on disposition, and preserves bounded redacted CI logs and receipt fields for the next occurrence (#303).
109
+ - Pause Kimi on its observed ACP 5-hour quota refusal, requeue untouched work only after a confirmed pause and automatically relay open tasks through the existing budget policy; hard refusals end reading overrides and supersede pending soft checkpoints. (#306)
110
+ - Bound SQLite contention on every shared database connection with a per-file busy timeout and a source guard. (#303)
111
+ - Make status, board, budget, doctor, projects, queue, turns, models and reports readable with shared tables, labelled fields, color policy, complete wrapped text and JSON output; size and sanitize console child output to its available width. `queue show`, `models status` and `budget execution status` now default to text; scripts that parsed their JSON add `--json`; a file name split across lines keeps whole characters, combining marks and joined emoji included (#284).
112
+ - Let unified dashboard peer starts wait 35 seconds for the terminal provider cap plus a response margin; slower headless starts are reported as unconfirmed. Block duplicate in-flight starts, unlock after confirmed answers, and bound an unconfirmed manager start lock by the hub's shared 30-second retry guard or earlier attachment; keep project-local confirmed-start behavior (#298).
113
+ - Keep durable Codex steer replies pending through their native turn and bound unanswered replies from its end so late definitive refusals requeue safely; make coordination and recovery test waits assert their milestones and await ACP cancellation transport before the next prompt (#296).
114
+ - Wait for an ACP cancelled prompt to settle before dispatching the next message on a bus without a journal; bound the wait and retain queued work when the provider cannot confirm cancellation, discard and log cancelled prompt errors, and preserve in-flight work across a legacy peer stop/restart (#304)
115
+ - Kimi's ACP `usage_update` (`{used, size}`, context occupancy) becomes the peer's native context reading with the new source `acp_usage_update`, so `ahub status`, the console and the dashboard show `context N%` for Kimi like Claude's and Codex's; the adapter accepts the update whenever it names the current session (2.1.1's source text emits it after the turn settles, so the peer may already be idle; that order is read from source, not yet observed live), an invalid update reports unknown instead of keeping the previous reading, occupancy still writes no `tokens` event and no budget window, and a `context.gate` crossing records the event and notice for an ACP peer without sending it a checkpoint request yet (#285).
116
+ - The seatbelt profile now also refuses the `config` and `hooks` of submodule git directories below the discovered modules root of the project's own `.git`, at the depth submodules nest and immune to ancestor directories named `refs`, `config` or `hooks` (refs named `config` or `hooks` stay writable through the `/refs/` and `/logs/` exclusion, matched on whole components only), and the discovered git directories, the modules root and the components in between are refused as directory-entry targets, closing the rename, replace and symlink routes; a nested bare-layout repository is recorded as a tested exclusion in `docs/security.md` (no hub git call descends into it, whatever the project root is); and every git call the hub makes outside the sandbox goes through the one helper in `src/hub/git.ts`, which disables the programs a repository's configuration can name (file-system monitor, hooks, external diff, pager, credential and askpass helpers), with a grep guard that fails on a new direct `spawn("git"` under `src/` outside the helper and the sandboxed tool (#281).
117
+ - A repeated `changes_requested` whose escalation saved its move returns the saved task when the assignment's delivery fails, with a console notice that says the delivery is not confirmed and to look for a queued or needs_review delivery (`ahub queue list --peer`, then `ahub queue show <id>`; if the journal is down, `ahub task show <id>` shows whether the owner accepted) before re-sending with `ahub task assign <id> <owner>`, instead of failing the reviewer's tool call; every escalation, a manual one included, now also runs its guarded tail on a saved move (the old owner is told to stop, the decision note is saved) before it throws, and a failure before the escalation's write still throws (#276).
118
+ - The saved-but-undelivered rule now holds on every path through `assignOwner`: a failure after the board write prints exactly one console notice where it is thrown (the move is saved, the delivery is not confirmed, how to check with `ahub queue list --peer` / `ahub queue show` / `ahub task show`, and the `ahub task assign` resend command) and throws `AssignmentUndeliveredError` carrying the saved task; callers decide control flow by the error type and print nothing more (the daemon's `delivery_failed` and `inference_failed` escalations no longer contradict the escalation's own tail, the ready offer's consumed decision reads the error instead of the owner readback, the budget relay records the move and later ticks skip it by its owner, the offline relay records the move and still tells the gone owner to stop, and the idle sweep and rescreen go on). A claim's own accepted write and line stay out of the delivery rule (its overlap and routing announcements to earlier owners share it), a move superseded while the assignment was prepared rethrows the original error with no notice (a later entry that moves or settles it; quiet bookkeeping, reviewer changes and check results do not supersede it), and the pre-write, passed-over and reserved-owner notices go through the same guard, so those notices cannot fail their caller. No automatic resend anywhere (#297).
119
+ - Start modes: a peer with a TUI starts in it by default and headless is opt-in. `ahub pi` opens Pi's TUI (`--headless` or `--mode headless` for the background peer), and a start the hub makes itself (the dashboard's new Start control, a console command, `hub_peer_start`, `pi.auto_start`) follows `peers.<id>.start_mode` (`tui` by default, settable for pi and codex in Settings and `ahub settings`): for `tui` it opens a terminal running the fixed `ahub <peer>` command through `terminal.open` (a machine-local command template) or Orca, and with no provider it is refused with the command to run by hand, never downgraded. A conductor cannot choose the mode. **Migration:** Pi's default changes from headless to TUI; a project with `pi.auto_start` keeps its headless Pi with `ahub settings set peers.pi.start_mode headless`, and scripts add `--headless`. After an unplanned stop a Pi that ran in a terminal is no longer replaced by a headless one unless headless is its start mode. Control protocol stays 16 (`peer_start` is a new console-role request; `ui_snapshot` gains `starts`) (#269).
120
+ - Settings in the dashboard and `ahub settings list|get|set|undo`, from one registry: permission modes now and at hub start, `pi.auto_start`, per-class routing (`peers`, `escalate_to`, `route`, `pi_backend`), `stay_switch` and the safe switches. Writes go only to `.agenthub/config.local.json` and the new machine-local `.agenthub/routing.local.toml` (merged over `routing.toml` key by key and named in `ahub route explain`), read by the hub's own loaders first, with one-step undo, a preview (`route explain` for the open tasks before and after) and a `settings` event. A dashboard session reads, lowers a mode to ask and changes safe switches; `ahub ui --settings` opens a 15-minute settings session for the rest, and never-ask needs the peer id typed. Commands, credentials and sandbox reach are never editable there. `ahub init` ignores `routing.local.toml` in git. Control protocol stays 16 (`settings_get|set|undo|preview` are new console-role requests) and events schema stays 1 (#269).
121
+ - Fix macOS timing-sensitive test fixtures by waiting for channel welcomes and releasing PII responses at the fallback decision and separating ACP receipt checks from watchdog timing; add opt-in repeated macOS verification (#287).
122
+ - Control protocol stays 16: `settings_get`, `settings_set`, `settings_undo`, `settings_preview` and `peer_start` are new console-role requests that an older hub answers as unknown, the `ui` request takes an optional `settings` field, `ui_snapshot` gains `settings` and `starts`, and a refused recovery or control request can carry `code: "database_busy"` with `retryable`. Events schema stays 1: the `settings` event and the `acp_usage_update` context source are additions. A live upgrade of a running 0.12.22 hub to this release has not been run; `docs/smoke.md` has no record of one.
123
+
7
124
  ## 0.12.22
8
125
 
9
126
  - Restore the captured Codex approval policy once on a resumed thread after detached ask, show unknown-policy guidance in the CLI, and record direct native Claude child identity so verified crash cleanup can retire its settings file (#270).
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # agent-hub
2
2
 
3
3
  Native multi-agent hub for one developer's machine: Claude Code, Codex, Kimi Code, Pi and a
4
- hub-owned local-LLM worker collaborate as peers in independent project directories, with
4
+ hub-owned local-LLM worker, with optional Qwen Code and Jcode support, collaborate as peers in independent project directories, with
5
5
  task-aware model routing (Switchyard) in front of a self-hosted gateway (OmniRoute).
6
6
 
7
- Status: 0.12.22, control protocol 16. Durable delivery records distinguish queued
7
+ Status: 0.12.24, control protocol 16. Durable delivery records distinguish queued
8
8
  work from uncertain execution. The [smoke checklist](docs/smoke.md) records
9
9
  verified paths and remaining prerequisites.
10
10
 
@@ -13,6 +13,11 @@ reconciliation, and the staged upgrade command for supported source protocols.
13
13
  Controlled recovery preserves work; uncertain effects are never automatically
14
14
  replayed and may require operator review.
15
15
 
16
+ [Hub memory](docs/memory.md) keeps optional local short- and long-term notes with
17
+ a rebuildable graph index. It runs alongside native claude-mem capture/recall,
18
+ which remains a separate service. Machine sync requires a configured remote;
19
+ no remote is created by startup.
20
+
16
21
  ## Start here
17
22
 
18
23
  Requires [Bun](https://bun.sh) >=1.3.0 on PATH; Node alone cannot run the CLI.
@@ -28,7 +33,7 @@ cd <your project> && ahub init && ahub up
28
33
  Or install the same version from GitHub:
29
34
 
30
35
  ```bash
31
- bun add -g github:STAIxBWLB/agent-hub#v0.12.22 && ahub setup
36
+ bun add -g github:STAIxBWLB/agent-hub#v0.12.24 && ahub setup
32
37
  ```
33
38
 
34
39
  The installed commands remain `ahub` and `agent-hub`.
@@ -83,17 +88,19 @@ an N-peer message bus and attaches each agent through its native control surface
83
88
  ## Pi with DGX and MLX
84
89
 
85
90
  Pi is optional. Install Pi separately and set `pi.enabled` to `true` in
86
- `.agenthub/config.json`. Set `pi.auto_start` to `true` to start its background
87
- peer with the hub. The project uses its existing OmniRoute connection for DGX;
91
+ `.agenthub/config.json`. Set `pi.auto_start` to `true` to start it with the hub.
92
+ Pi starts in its TUI by default; `ahub pi --headless`, or the start mode
93
+ setting (`ahub settings set peers.pi.start_mode headless`), asks for the
94
+ background peer instead. The project uses its existing OmniRoute connection for DGX;
88
95
  `pi.dgx_coding` and `pi.dgx_fast` select the gateway's configured model IDs.
89
96
 
90
97
  ```bash
91
98
  ahub models setup # prepare bounded Qwen3.5 4B MLX in an existing Ollama service
92
99
  ahub models start # verify loopback Ollama/model readiness (no daemon spawn)
93
100
  ahub models status
94
- ahub pi --mode headless --backend auto
101
+ ahub pi --headless --backend auto
95
102
  ahub say @pi "Read the project and summarize the next small implementation task"
96
- ahub pi --mode tui # switch the idle session to its native terminal
103
+ ahub pi # its TUI in this terminal; an idle headless session moves into it
97
104
  ```
98
105
 
99
106
  The default routing template assigns implementation, bulk edits and tests to
@@ -151,7 +158,12 @@ browser cannot be opened.
151
158
  Open the link within 60 seconds; the browser session lasts one hour.
152
159
 
153
160
  The dashboard can send console messages, propose and assign tasks, pause or resume
154
- peers, and answer approvals. Budget pauses still require the terminal override.
161
+ peers, answer approvals, start a peer in its start mode and show the settings.
162
+ An ordinary session changes the safe switches and lowers a mode to `ask`; the
163
+ rest needs `ahub ui --settings` or `ahub settings` in a terminal. See
164
+ [Settings](docs/operations.md#settings-from-the-dashboard-and-the-terminal) and
165
+ [Start modes](docs/operations.md#start-modes-tui-by-default-headless-as-opt-in).
166
+ Budget pauses still require the terminal override.
155
167
  Private envelopes and PII tasks stay redacted. Local-worker and Pi tool details and
156
168
  allow decisions stay in `ahub tail` / `ahub permit`; the page can deny them.
157
169
  No dashboard port is opened until requested. See [the session security model](docs/security.md#local-dashboard-sessions-issue-6).
@@ -176,7 +188,8 @@ ahub ui --all
176
188
 
177
189
  The unified dashboard lists registered roots and opens their boards, streams,
178
190
  messages, approvals and budgets. It can start hubs and stop a selected hub after
179
- confirmation. Launch Claude/Codex TUIs from a terminal. The manager stays available
191
+ confirmation. Its Start control opens a peer's TUI only through a terminal provider
192
+ (`terminal.open` or Orca); without one, launch Claude/Codex TUIs from a terminal. The manager stays available
180
193
  when a project stops; `ahub ui --all --stop` stops only the manager. Existing
181
194
  `ahub ui` still opens the current project's dashboard.
182
195
 
@@ -0,0 +1,24 @@
1
+ # claude-mem: decision note (#248)
2
+
3
+ Status 2026-10-12: keep optional. No measured effect yet.
4
+
5
+ ## What it does to collaboration
6
+
7
+ - Recall: `context/inject` at session start (native hooks for Claude and Codex, the hub's `recallFor` for the other peers) and the task brief on handoff (`search` plus `timeline`).
8
+ - Capture: native hooks record tool use; the hub captures the local worker's (`src/memory/capture.ts`).
9
+ - Shared notes: `hub_remember` and the hub's own notes on done, review, escalation and checkpoint.
10
+
11
+ ## Evidence
12
+
13
+ - Field data: no research records on the development machine yet (`~/.agenthub/research` empty); records written from now on carry the `tooling` fingerprint (`ahub research addons`).
14
+ - Controlled arms: none run. Owed: `all-on`, `claude-mem-off`, `all-off` (docs/bench.md, Add-on arms), listed in docs/smoke.md.
15
+
16
+ ## Decision
17
+
18
+ | Part | Decision | Basis |
19
+ | --- | --- | --- |
20
+ | Recall at session start | keep optional | no measurement; the hub already reaches every peer through `recallFor` |
21
+ | Capture of tool use | keep optional | no measurement; capture runs through a cloud observer, so PII rules keep it out of PII work regardless |
22
+ | Long-term store, tiers, graph of notes | adopted natively in #258 | the user's request for cross-project and cross-machine memory, not a measured effect; #258 AC11 measures it (memory off, claude-mem only, native graph) |
23
+
24
+ Revisit when an arm comparison has at least 5 attempts per arm and measure, or field data has 5 tasks on each side.
@@ -0,0 +1,8 @@
1
+ # Codex plugin for Claude Code: decision note (#248)
2
+
3
+ Status 2026-10-12: keep optional. No measured effect yet.
4
+
5
+ - What it does: lets a Claude session hand diagnosis, a second implementation or an adversarial review to Codex outside the hub's bus.
6
+ - Expected effects to measure: tokens and wall time per approved task, first-pass rate, and work that bypasses the task board.
7
+ - Evidence: none collected; the fingerprint records it for Claude only. Arms owed: `all-on`, `codex-plugin-off`, `all-off`.
8
+ - Decision: keep optional. The hub's own Codex peer and task board already carry delegation natively; nothing is adopted without data.
@@ -0,0 +1,8 @@
1
+ # dot admission: decision note (#248)
2
+
3
+ Status 2026-10-12: keep optional. No measured effect yet.
4
+
5
+ - What it does: admits heavy local work (builds, full test runs, installs) one per repository and defers under host pressure.
6
+ - Expected effects to measure: check failures and wall time per approved task, and checks deferred rather than run.
7
+ - Evidence: none collected; the fingerprint records it for every peer whose first `dot` on PATH is the dotfiles CLI. Arms owed: `all-on`, `dot-admit-off`, `all-off`.
8
+ - Decision: keep optional. The hub runs one completion check at a time already; admission across sessions stays the workspace's tool.
@@ -0,0 +1,8 @@
1
+ # open-code-review: decision note (#248)
2
+
3
+ Status 2026-10-12: keep optional. No measured effect yet.
4
+
5
+ - What it does: the OCR delegation gate selects files and rules for a review that the host agent then performs; in this workspace it gates merges.
6
+ - Expected effects to measure: review rounds and reviewer unmet items per approved task, `regressed` and `reverted` labels after approval.
7
+ - Evidence: none collected; the fingerprint records it for Claude (plugin) only. Arms owed: `all-on`, `open-code-review-off`, `all-off`.
8
+ - Decision: keep optional. The hub's reviewer role and review checklists (#35) are the native counterpart; no part is adopted without data.
@@ -0,0 +1,23 @@
1
+ # ponytail: decision note (#248)
2
+
3
+ Status 2026-10-12: keep optional. No measured effect yet.
4
+
5
+ ## What it does to collaboration
6
+
7
+ - A Claude Code (and optionally Codex) plugin that steers the agent toward the smallest change that works: reuse before writing, stdlib before dependencies, root cause in the shared function, `ponytail:` comments on deliberate shortcuts.
8
+ - Expected effects to measure: diff size and files changed per task, rework (changes requested), review rounds, and whether a "smallest diff" misses sibling callers (later `regressed` labels).
9
+
10
+ ## Evidence
11
+
12
+ - Field data: none yet; the fingerprint now records it for Claude and Codex.
13
+ - Controlled arms: none run. Owed: `all-on`, `ponytail-off`, `all-off`.
14
+
15
+ ## Decision
16
+
17
+ | Part | Decision | Basis |
18
+ | --- | --- | --- |
19
+ | Diff-size rule | keep optional | no measurement |
20
+ | Root-cause-in-the-shared-function rule | keep optional | no measurement; candidate for a reviewer-role rubric item if the arms show fewer `regressed` labels |
21
+ | `ponytail:` comments on shortcuts | already native | the repository's own convention (AGENTS.md), independent of the plugin |
22
+
23
+ No native adoption without data (backlog plan R10). A follow-up issue is opened only for a part an arm shows helps.
@@ -0,0 +1,22 @@
1
+ # ripwire: decision note (#248)
2
+
3
+ Status 2026-10-12: keep optional. No measured effect yet.
4
+
5
+ ## What it does to collaboration
6
+
7
+ - A call-graph CLI agents query before editing: orientation, callers and uses, impact (blast radius with test reachability) and the tests to run for a change.
8
+ - Where it could matter between agents: plans that name the right symbols (the hub's overlap detection compares plan paths and symbols), fewer conflicts between owners, and reviewers finding sibling callers.
9
+
10
+ ## Evidence
11
+
12
+ - Field data: none yet; the fingerprint records it for every peer that has it on PATH.
13
+ - Controlled arms: none run. Owed: `all-on`, `ripwire-off`, `all-off`.
14
+
15
+ ## Decision
16
+
17
+ | Part | Decision | Basis |
18
+ | --- | --- | --- |
19
+ | Impact queries before a plan | keep optional | no measurement; candidate native use is symbol-level impact for overlap detection, if `ripwire-off` shows more collisions |
20
+ | Caller lookups during review | keep optional | no measurement |
21
+
22
+ No native adoption without data (backlog plan R10). A follow-up issue is opened only for a part an arm shows helps.
@@ -0,0 +1,5 @@
1
+ # Agent note background
2
+
3
+ Human explanation. Agent instructions: the area notes in this directory.
4
+
5
+ On 2026-10-13, two PR #368 reviews found that a missing delivery id or a missing visible row could select another Queue row. The console navigation rule in daemon.md records the exact-row contract. The missing-row and ID-less secondary-delivery regression tests demonstrate both cases.
@@ -1,8 +1,10 @@
1
1
  # Native adapters and spawned agents
2
2
 
3
- Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` and `activity` events), peer turn state, approvals, and the agent processes the hub spawns and stops. Read before editing `src/adapters/`, `src/pi/`, `src/hub/peers.ts`, `src/hub/permission-mode.ts`, `src/hub/tool-refusal.ts`, `src/hub/child-process.ts`, `src/hub/lifecycle.ts` (shutdown deadline), `src/memory/capture.ts` (session end) or the process-tree stop in `scripts/benchmarks/teardown.ts`, or before adding a native peer.
3
+ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` and `activity` events), peer turn state, approvals, and the agent processes the hub spawns and stops. Read before editing `src/adapters/`, `src/pi/`, `src/hub/peers.ts`, `src/hub/permission-mode.ts`, `src/hub/tool-refusal.ts`, `src/hub/child-process.ts`, `src/hub/lifecycle.ts` (shutdown deadline), `src/hub/start-mode.ts`, `src/hub/terminal-open.ts`, `src/memory/capture.ts` (session end) or the process-tree stop in `scripts/benchmarks/teardown.ts`, or before adding a native peer.
4
4
 
5
+ - Kimi TUI is a pull-only WsPeer under the one kimi id, with no hub busy/watchdog/permission/quota machinery. Only the person's launcher merges or removes its project MCP entry; a daemon headless start refuses a stale entry and never edits the file. Scrub mode/channel markers from the native child; managed AGENTS instructions and tool descriptions carry the pull rules, not MCP initialize instructions.
5
6
  - 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.
7
+ - 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).
6
8
  - Codex reports each thread it adopts, started or resumed (`onThread`, the `native_thread` event), before any turn on it starts: the upgrade planner (`codexZeroTurn` 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.
7
9
  - Codex 0.154.0 `agentMessage` items carry `text` and `phase` (not `content[]`); only the last non-`commentary` message of a turn is shared.
8
10
  - Approval titles are agent-written text shown to a person: the daemon escapes control characters, and write/edit/bash show what will be written or run.
@@ -11,21 +13,31 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
11
13
  - Report each Pi owner exit once, with code/signal (unknown for an unobserved TUI exit), turn/tool activity and only the last tool name. Auto-restart only a current, started, unexpectedly idle headless owner on its verified persisted session, once per 60 s; preserve shutdown/requested-stop/recovery/startup/active-tool fences and never fall back fresh.
12
14
  - Pi's `always` grant (#209) is keyed by `executeTool`'s tool name, never parsed from the approval title, and lives only in that Pi start's closure. Never persist it, share it with the local worker, or let the dashboard give it.
13
15
  - 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. 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 in `src/hub/permission-mode.ts`.
14
- - 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, conductor tool or dashboard action sets one. Never write a runtime choice to a restart snapshot.
15
- - A peer whose mode lives in the native agent (Kimi, Codex) 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.
16
+ - 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 a second path that writes `permissionModes`. Never write a runtime choice to a restart snapshot.
17
+ - 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.
16
18
  - 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`.
17
19
  - 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.
18
20
  - 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`).
19
21
  - Every agent the hub spawns (the Codex app-server, ACP agents such as Kimi, Pi) runs in its own process group and is stopped as one (`stopOwnedProcess(proc, { group: true })`, `trackGroup` after spawn): `codex` is a node launcher, and mid-turn its native app-server does not exit on SIGTERM within the grace period, so a SIGKILL to the launcher alone left the app-server running under init and holding the daemon alive (#113, #115). A new adapter that spawns an agent does the same.
20
22
  - To stop a process tree, freeze it (SIGSTOP) before reading what it contains, then SIGKILL, and call it done only when the table shows none of it (with no table at all, the stop can only ask the group itself, which cannot see groups below it). A process a read after the freeze shows for the first time is frozen and read again before anything is killed. A snapshot taken while the tree runs misses what it starts next: three review rounds of #113 found such a gap (groups of its own, an unreadable first read, a child started during the grace period).
21
23
  - Work that must survive `ahub kill` (claude-mem `session-end`) is awaited in `stop()`; fire-and-forget dies with the process.
22
- - A peer must set `busy` synchronously inside `deliver()`, otherwise the bus drains the next envelope into a running turn (Kimi answers `turn.agent_busy`).
23
- - A watchdog-cancelled turn still reports later. Anything a turn does on completion must check it is still the current turn (`turn` generation in `acp.ts`, `activeTurns` in `codex-appserver.ts`). A rejected prompt or abnormal prompt end reports through the adapter's `onTurnFailure` (same shape in `acp.ts` and `pi.ts`), even when ACP streamed partial text; a stale cancelled turn never reports it.
24
+ - A peer must set `busy` synchronously inside `deliver()`, otherwise the bus drains the next envelope into a running turn (Kimi answers `turn.agent_busy`). Outside a turn the adapter accepts an occupancy diagnostic (a `context-used` `usage_update` naming its current session) and nothing else: no token total, no chunk, nothing unnamed or for an offline peer (#285).
25
+ - A watchdog-cancelled turn still reports later. Anything a turn does on completion must check it is still the current turn (`turn` generation in `acp.ts`, `activeTurns` in `codex-appserver.ts`). A rejected prompt other than a recognised quota refusal, or an abnormal prompt end, reports through the adapter's `onTurnFailure` (same shape in `acp.ts` and `pi.ts`), even when ACP streamed partial text; a stale cancelled turn never reports it; a rejected watchdog-cancelled ACP prompt during its journal-less wait is discarded with one log line, not requeued as on the earlier adapter.
24
26
  - Native owner teardown must handle a lost shutdown acknowledgement using verified process identity. Pending launches must be revocable, and active tools/streaming output must count as watchdog activity.
25
27
  - Pi owner identity is one shared contract, `processSignature` in `src/pi/process-signature.ts`; the startup claim check, the extension's claim, the TUI owner monitor, `recoveryReady` and the verified teardown all go through it. Its `ps` read pins LC_ALL=C and TZ=UTC (the `processTable` contract) because `lstart` follows the reader's timezone and locale, and a hub on UTC with a Pi child on system time otherwise hashes different strings for the same process and refuses the valid owner (#174). Strict pid + start + command identity stays; never accept PID-only or a fuzzy command match.
26
28
  - Return the shared `toolResultFailed` verdict as `/tool`'s `failed`; map only `failed === true` to Pi's `isError`. Preserve text and the older bridge's absent-flag behavior. The one exception is a person's `!command` whose process exit was observed: its `failed` is `exitCode !== 0` and the reply carries `exitCode`, from `onProcessExit` through `packPiShellResult`, never parsed from the output (#253).
27
29
  - Pi's bridge executes `/tool` only for the verified owner (`verifiedOwner()`: the claimed pid with its `processSignature`; a headless owner is pinned from the spawned process once `get_state` proved its session) with the current `sessionId` and turn `generation`, and admits `/budget` only for that owner and generation, checked again after the awaited admission. Refuse anything else before execution. `tool_abort` aborts that pending call, or fences its id when it arrives before the call (64 ids a turn, then every further call of that turn). A person's `!command` (`purpose: "idle_user_bash"`) is taken only in TUI mode from an idle owner on its one-use reservation, and it alone skips `piPermit`: never widen that to a model's bash call (#253).
28
30
  - 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.
29
- - `stop(reason?)` (`src/hub/peers.ts`): a reason marks a person's requested stop (`peer_stop`, rule: `daemon.md`). Fence before the first await (the adapter's stopping flag, or `offline`) so `deliver()` and buffered native callbacks do nothing more, hold the in-flight delivery as `needs_review` with that reason, and end `offline`. A requested stop never completes, escalates or reports the turn it cut (`AcpPeer` bumps `turn`; `PiPeer.fail` skips the failure message and `onTurnFailure` while `requestedStopReason` is set). `PiPeer.stop()` waits for aborted tools only up to `stopGraceMs`: a tool past it never blocks the teardown, and the stop throws `PiToolSettlementFailure` once the owner is down.
31
+ - `stop(reason?)` (`src/hub/peers.ts`): a reason marks a person's requested stop (`peer_stop`, rule: `daemon.md`). Fence before the first await (the adapter's stopping flag, or `offline`) so `deliver()` and buffered native callbacks do nothing more, hold the in-flight delivery as `needs_review` with that reason, and end `offline`. For ACP without a journal, return ordinary in-flight envelopes once through `onFailed` before a requested stop invalidates the turn; never return the watchdog-cancelled original prompt. A requested stop never completes, escalates or reports the turn it cut (`AcpPeer` bumps `turn`; `PiPeer.fail` skips the failure message and `onTurnFailure` while `requestedStopReason` is set). `PiPeer.stop()` waits for aborted tools only up to `stopGraceMs`: a tool past it never blocks the teardown, and the stop throws `PiToolSettlementFailure` once the owner is down.
32
+ - A terminal the hub opens for a person (`src/hub/terminal-open.ts`, #269) gets `personEnv`, which strips every agent marker; never `peerChildEnv`, which sets one. Adding a native peer with a TUI means adding it to `STARTABLE_PEERS` and, if it also has a headless form, to `START_MODE_PEERS` (`src/hub/start-mode.ts`), with its fixed words in `startWords`.
30
33
  - Spawn a native peer with `peerChildEnv(peer, source)` so its own `AGENTHUB_PEER_ID` replaces inherited agent markers. Sandbox tool environments carry their executing peer too. Preserve the native shell environment policy; Codex's `CODEX_THREAD_ID` supplies the fallback after filtering. Before changing marker detection or launcher environments, read `docs/verification/2026-10-09-agent-shell-t0.md` for pinned source and live-probe limits.
31
34
  - Emit Pi's ceiling event at the pre-effect rejection boundary (`src/pi/ceiling.ts`), before `agent_end`. Accept only a valid current-session/current-generation signal during a running turn; retain the first signal and attach it to that turn's failure. Never classify a ceiling from free text.
35
+
36
+ - After an ACP watchdog cancels a prompt on a bus without a journal, keep the peer busy with its inactivity watchdog cleared until that prompt settles; suppress its turn-scoped updates and permissions while still accepting session-bound occupancy diagnostics. Bound the wait by the existing 30 s ACP handshake deadline, go offline, never idle, with queued work retained if it expires, and revoke the wait on stop or transport loss; a durable delivery keeps its existing offline/needs_review path.
37
+ - ACP quota refusals use the pinned agent contract in `src/adapters/acp-quota.ts`, fenced by native identity and current turn generation. Report hard quota and require the hub's explicit pause confirmation before settling a pre-acceptance refusal as `failed_safe`; retain `needs_review` when work already started, and keep unknown refusals on the ordinary failure path. Sanitize agent refusal text before hub-written output.
38
+
39
+ - Refuse ACP permissions while offline or waiting for watchdog cancellation before consulting automatic or human approvers. Abort pending permission signals on watchdog, offline transition and stop; the daemon withdraws their registered console approvals before any late person answer. Restart creates new requests without reviving withdrawn ones (#311).
40
+
41
+ - Apply QWEN_MODE_IDS only to a production qwen025 adapter with verified qwen-code 0.25.0 native identity and advertised ids; legacy benchmark ACP stays unmanaged. Await the native acknowledgement before bus admission, preserve existing approval fences, and never map auto or plan. No Qwen quota classification exists until a pinned native refusal is observed.
42
+
43
+ - 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.
@@ -10,3 +10,6 @@ Scope: the CooperBench runners, their fixtures, teardown and recovery. Read befo
10
10
  - Read terminal coverage only through bounded `terminal_coverage` metadata reads, rebinding the audit, ledger and manifest to the seal index. Require complete sealed state, all audit checks, plan/cell/count reconciliation and zero recorded live owners. Report recorded verification separately from fresh audit; missing or inconsistent evidence is unavailable.
11
11
  - Publish Pi benchmark descriptors from `benchPiToolProblem`; keep production schemas unchanged and omit solo peer messaging. Attest every bootstrap tool publication against declared native/schema/source pins before scored generation. Fail/defer missing, mixed, malformed, truncated or changed publications; retain only bounded names/counts/hashes. Declare a new study condition before any changed collection; never rewrite historical artifacts or select retries by score. The driver's Qwen profile and Pi tool sandbox are `profile()` from `src/local/sandbox.ts`: a change to that profile (#270's refusal of the `.git` name is one) is such a changed condition.
12
12
  - The benchmark runner's Claude trust entry: a write still `pending` in the runner's own process never landed, so it is `not_written` on every path (contained or not, its temp file removed or left), and neither the runner nor the recovery touches an entry then. Only a dead runner's `pending` leaves that question to the recovery, which takes back only an entry exactly as the runner would have written it, and none while the runner's temp file is still there (the rename never happened). `changed_concurrently` is the user's entry and is never taken back. The runner's settlement is `settleTrust` in `scripts/benchmarks/teardown.ts`, tested as a table: change it there, never inline.
13
+
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
+ - 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.
@@ -12,3 +12,7 @@ Scope: quota and native context readings, checkpoints, pause, handoff and resume
12
12
  - Keep native context occupancy in `src/hub/context-window.ts` separate from quota accounting; never use Codex's accumulated `total` as occupancy or estimate Pi counters.
13
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 non-approved associated PII-task summaries (owner or reviewer, including in_review, with the current routing policy) 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.
15
+
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
+ - 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 to Claude and Codex only; an ACP peer gets the event and the notice (#285).
18
+ - 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).
@@ -13,8 +13,10 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
13
13
  - 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.
14
14
  - 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.
15
15
  - `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.
16
- - A pull-only peer (a Claude session without channel pushes, #205) is never drained; `Bus.pull` hands it what one push would take. 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.
16
+ - A pull-only peer (a Claude session without channel pushes or Kimi in its own TUI) is never drained; `Bus.pull` hands it what one push would take. 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.
17
17
  - `resolveDelivery` mutates queues in place; a refused or failed resolution puts back only that peer's queue and preface and keeps every pause. Never reload a snapshot on a live bus.
18
18
  - `restore` takes queued work from the journal, never from the committed snapshot once the journal has state (an empty journal, revision 0, is seeded from the commit): from the commit only an empty queue key is carried over, for an offline peer whose key no delivery ever persisted (#234). Never resurrect a non-empty queue from it.
19
19
  - The journal's owner check (`rejectLiveOwner` in `delivery-journal.ts`) judges the manifest pid with `processLiveness` and its `pidSignature`, never a bare probe (rule: `daemon.md`).
20
20
  - A hub-native peer (Pi, the local worker) does not get to call its own message `important`: `capPriority` caps it unless the delivery it answers held an `important` envelope addressed to it. Check the delivery, not `replyParent`, which ties on hop and takes the later item. Do not bypass it by setting `priority` in the adapter.
21
+
22
+ - Routing never chooses a pull-only owner or reviewer automatically. Pass live pull-only ids and role-specific explicit naming to the pure assign function; an automatic singleton is not named. Only a honored persisted owner reservation, explicit owner nomination/claim, or authorized reviewer nomination bypasses that eligibility refusal, never roster, PII, pause or capability guards.
@@ -1,9 +1,13 @@
1
1
  # Daemon, control protocol and recovery
2
2
 
3
- Scope: the control WS and its protocol, state files, the Claude channel's reconnect, `ahub setup`, git snapshots, upgrade and recovery. Read before editing `src/hub/daemon.ts`, `src/hub/control-client.ts`, `src/adapters/claude-channel.ts`, `src/hub/restart.ts`, `src/hub/recovery-store.ts`, `src/hub/snapshots.ts`, `src/hub/manager.ts`, `src/hub/lifecycle.ts`, `src/hub/crash.ts`, `src/cli/setup.ts`, `src/cli/upgrade*.ts`, `src/cli/terminal-recovery.ts`, `src/cli/recovery-package.ts` or `src/cli/facts-hook.ts`, or before adding a native peer.
3
+ - Console attention navigation selects a delivery only by an exact visible row id. A missing id or row prints read-only guidance; never select by peer or fall back to the first Queue row.
4
+
5
+ Scope: the operator console, control WS and its protocol, state files, the dashboard listener and settings, the Claude channel's reconnect, `ahub setup`, git snapshots, upgrade and recovery. Read before editing `src/cli/console.ts`, `src/cli/console-state.ts`, `src/cli/attention.ts`, `src/ui/needs.ts`, `src/hub/daemon.ts`, `src/hub/ui.ts`, `src/hub/settings.ts`, `src/hub/control-client.ts`, `src/adapters/claude-channel.ts`, `src/hub/restart.ts`, `src/hub/recovery-store.ts`, `src/hub/snapshots.ts`, `src/hub/manager.ts`, `src/hub/lifecycle.ts`, `src/hub/crash.ts`, `src/hub/start-mode.ts`, `src/hub/terminal-open.ts`, `src/hub/recovery-error.ts`, `src/hub/db-timeout.ts`, `src/cli/setup.ts`, `src/cli/upgrade*.ts`, `src/cli/terminal-recovery.ts`, `src/cli/recovery-package.ts` or `src/cli/facts-hook.ts`, or before adding a native peer.
4
6
 
5
7
  - The plugin bundle is installed apart from the daemon. Breaking a control WS message contract bumps `PROTOCOL` in `control-client.ts`; a new request answered as unknown or an optional field preserves it, as does additive optional public task metadata in `ui_snapshot` that older clients ignore (such as `deps` in #246).
6
8
  - `peer_stop` is console-only and stops only a hub-owned headless ACP, Pi or local runtime through its adapter. Refuse native-terminal peers and recovery-held mutations; serialize with starts and permission changes, and refuse a recovery `prepare` while a stop runs. An admitted stop holds delivery through `BusOptions.deliveryHeld` before any teardown await (never a console pause that resume can undo, or a global recovery hold that blocks unrelated peers) and withdraws the peer's pending approvals before the adapter stops. A requested stop holds an in-flight delivery as `needs_review` with an ids-only stop reason, suppresses restart/escalation, and never resolves that delivery automatically.
9
+ - Settings (#269): the dashboard's `setting`, `setting_undo` and `setting_preview` actions and the console-role `settings_get|set|undo|preview` requests go through `settingsSet`, `settingsUndo` and `settingsPreview` (`src/hub/daemon.ts`) and the registry in `src/hub/settings.ts`. A new editable setting is a registry entry, never a new write path; a key that chooses what the hub runs or where secrets or task text go joins `NEVER_EDITABLE` instead. Writes go only to `.agenthub/config.local.json` and `.agenthub/routing.local.toml` through `store()`: the loader's own validation first, temp file and rename, never a file git tracks. Authority comes from the dashboard session (`DashboardSession`, `src/hub/ui.ts`) or the console role, never from the request body; `ui_action` from the manager is an ordinary session. Never return a setting's value in an event or notice except through `recordSetting`.
10
+ - Start modes (#269): every start the hub makes itself goes through `hubStart` (`src/hub/daemon.ts`): the dashboard's `start_peer`, the console-role `peer_start`, the conductor's `start` hook and `pi.auto_start`. It takes the mode from `startMode(peer)` and nothing else; never add a caller-chosen mode, flag or command to it, and never fall back to headless when a terminal cannot be opened (refuse with the manual command). The words a terminal runs are assembled there from `process.execPath`, the CLI entry, the project root and `startWords(peer)`. Recovery and `startPeer` callers that pass a recorded mode keep it. The real terminal providers are wired only in `src/hub/lifecycle.ts` (`DaemonOptions.terminal`): a daemon built without them, as in every test, opens nothing; a test injects a recording provider and never the real one.
7
11
  - `ahub setup` reads Claude Code's state from `--json` listings and takes one step at a time, re-reading after each; never match paths or names by substring.
8
12
  - A copy of the git index keeps the original's mtime (`snapshot()`): git's racy-entry check compares entries with the index file's time in whole seconds, and a fresh copy makes a same-size edit look clean.
9
13
  - State files that clients read (`status.json`, `control-token`) are written after the port is bound, and `status.json` via temp file + rename.
@@ -23,3 +27,12 @@ Scope: the control WS and its protocol, state files, the Claude channel's reconn
23
27
  - Recovery judges the registry claim as it is now (`inspectRecovery` re-reads `registeredProjects()`), never the plan's frozen copy; a registry that cannot be read is not inspected, never stopped (#226).
24
28
  - The managed Claude hook (`src/cli/facts-hook.ts`) may only add `permissionDecision: "allow"` on `PreToolUse`, and for `ask-when-needed` only what `projectFileTool` accepts: a hub that is down, slow or refuses decides nothing. The daemon puts `permission` in a `facts` reply only for the hook of this instance's recorded `ahub claude` launch (the same `launchId`, not `--unattended`), and a permission-only hook (`hookPurpose`) never drives busy state (#242).
25
29
  - `ahub claude` (`src/cli/launch.ts`, #270) publishes `claude-launch.json` for every launch against a running hub, atomically and without a lock: hooks, permission status, session observation and recovery read its `launchId`, so never make it wait for native-proof state. The native child's pid and signature go only into the private `claude-native-<binding hash>.json`, keyed by the exact launch record, written once under `claude-launch.lock` and removed at that child's own exit; never put them in `claude-launch.json`, where a late callback of an earlier launch could replace a newer record. Crash cleanup of a previous launch's settings file (`cleanupStaleClaudeSettings`) needs the launcher gone by `processLiveness` and that native proof, and for a `direct-child` proof the native pid absent from an available `processTable`: a direct child may exec in place, so its signature certifies nothing. A script or interpreter gets no proof and keeps its settings file.
30
+
31
+ - Keep SQLite recovery contention in the existing blocked receipt as retryable `database_busy`, retaining bounded throw-site frames and the ordinary `ok: false` rid reply. Use a neutral control error; the receipt's next actions name resume or the partway disposition. Clear a superseded failure on abort, disposal, fresh-session choice or a refused resume of a disposition. After a commit request, treat busy inspection as unconfirmed and keep polling only inside the existing shutdown bound.
32
+ - Recovery inspection uses its existing 30 s control cap for the first status read and consumes a structured busy reply without a second status probe; ordinary lifecycle probes keep their default. Treat `busy_timeout` as a ceiling for eligible waits, not a guarantee that every write waits: a WAL snapshot conflict may fail immediately. Account for synchronous 3 s hub-database waits and serial timer callbacks when choosing a timeout; several waits may consume the 30 s control margin. Never multiply the busy wait through retries or infer stopped from a shorter probe's timeout.
33
+ - Every `new Database(` under `src/` is followed at once by its own `PRAGMA busy_timeout` from a named `*_BUSY_TIMEOUT_MS` constant (`HUB_DB_BUSY_TIMEOUT_MS` in `src/hub/db-timeout.ts` for `hub.db`), and nothing sets `busy_timeout` again later: `test/db-busy-timeout.test.ts` reads the source and fails on any other opening idiom, so a new one extends that guard first (#303).
34
+ - The manager forwards a dashboard `start_peer` with the terminal provider cap (`MAX_COMMAND_MS`) plus 5 s and reports a missed deadline as `unconfirmed`, never as a failure; it recognises the deadline by the exact text `ControlClient.request` resolves with, so change the two together. The hub's terminal reopen guard and the dashboard's retry hold share `PEER_START_RETRY_MS` (`src/hub/start-mode.ts`): never give either its own number (#298).
35
+
36
+ - Use `KNOWN_PEERS` for the native catalog and `rosterAllows` for participation. Change live membership only at the authorized roster settings write or undo, applying exactly that key. Keep external ids and projects without roster settings compatible with the existing admission and lazy status rules. Fence starts and attachment before detaching or stopping a removed runtime; retain its queue, uncertain deliveries and independent pauses. Refuse removal of the conductor holder and require explicit confirmation and `--now` for an active turn.
37
+
38
+ - A Kimi one-launch TUI admission ends on its owning detach, roster removal, matched launcher cleanup or the unclaimed 30 s deadline. An old socket or launcher never clears a newer admission. Pin --mode tui in the hub-opened terminal argv.
@@ -1,10 +1,11 @@
1
1
  # Local worker and sandbox
2
2
 
3
- Scope: the hub-native local worker, its tools, path guard, denylist and seatbelt sandbox. Read before editing `src/adapters/local-worker.ts`, `src/local/`, `src/memory/capture.ts` or `src/hub/facts.ts` (both read the denylist), or the grant filter in `src/hub/permission-mode.ts`.
3
+ Scope: the hub-native local worker, its tools, path guard, denylist and seatbelt sandbox. Read before editing `src/adapters/local-worker.ts`, `src/local/`, `src/memory/capture.ts` or `src/hub/facts.ts` (both read the denylist), `src/hub/git.ts`, or the grant filter in `src/hub/permission-mode.ts`.
4
4
 
5
5
  - Nothing the local worker executes may run outside `sandboxedExec`; a new tool that spawns a process goes through it, and a tool that touches a path goes through `guardPath`.
6
6
  - The sandbox denies home reads by default (toolchain dirs, the project and its real git dir excepted) and all network, loopback included: claude-mem and the Codex app-server listen on loopback without auth. Tests that bind a local port therefore fail when the worker runs them, also with `local.bash_network: true` (the egress proxy keeps loopback closed, and the proxy variables send a test's plain-HTTP `fetch` to the proxy, which refuses it); that is the intended trade-off, and `"direct"` is the switch until 0.13.0 removes it.
7
- - The seatbelt profile refuses the `.git` name itself at any depth (creation, rename, symlink, gitfile), `config` and `hooks` directly under it, `commondir` at any depth below it or below an external git dir, and `.agenthub` anywhere in the project, every name folded against the 16 code points HFS+ ignores (#270). An external git dir's `config`/`hooks` rules cover only its own children plus the hooks directory's contents: at any depth they would deny refs named `config` or `hooks` in a worktree or submodule project. The fold is a group alternation, never a character class (the seatbelt regex engine is byte-oriented and a quantifier after a multi-byte class member binds to its last byte), and every pattern keeps its `^` anchor (an unanchored starred alternation does not match). Folded literals never contain the project root: an SBPL string literal dies at 1024 bytes. The cost: `git init`, `git clone` and `git worktree add` do not run inside the sandbox, and neither do `git worktree remove`, `git worktree prune` of a stale record, `git clean -ffd` with a nested repository, or `rm -rf` of a directory holding a `.git`.
7
+ - The seatbelt profile refuses the `.git` name itself at any depth (creation, rename, symlink, gitfile), `config` and `hooks` directly under it, `commondir` at any depth below it or below an external git dir, and `.agenthub` anywhere in the project, every name folded against the 16 code points HFS+ ignores (#270). A submodule's git dir under the common dir's `modules/` tree gets the same `config`/`hooks` refusal at the depth submodules nest, every term anchored below the discovered modules root so ancestor directories named `refs`/`config`/`hooks` neither disable nor trip the rule, and a `/refs/` or `/logs/` segment below it excludes the path so refs named `config` or `hooks` stay writable (#281); the discovered git dirs, the modules root and the components in between are also refused as directory-entry targets (rename, replace, symlink, recreate). Known residuals: a submodule whose own path holds a whole `refs` or `logs` segment (a substring, `catalogs` included, keeps the refusal — the exclusion matches whole components only), a nested repository in bare layout (the hub never runs git there; docs/security.md), folded spellings of the components between `.git` and a submodule's git dir on a volume that folds names (the 1024-byte SBPL literal cannot hold a folded path; external git dirs carry the same residual since #270), and the submodules of a repository nested inside the project (only the project's own common dir is discovered). An external git dir's `config`/`hooks` rules cover only its own children plus the hooks directory's contents: at any depth they would deny refs named `config` or `hooks` in a worktree or submodule project. The fold is a group alternation, never a character class (the seatbelt regex engine is byte-oriented and a quantifier after a multi-byte class member binds to its last byte), and every pattern keeps its `^` anchor (an unanchored starred alternation does not match). Folded literals never contain the project root: an SBPL string literal dies at 1024 bytes. The cost: `git init`, `git clone` and `git worktree add` do not run inside the sandbox, and neither do `git worktree remove`, `git worktree prune` of a stale record, `git clean -ffd` with a nested repository, `git submodule update --init` (its git dir's `config` write is refused), or `rm -rf` of a directory holding a `.git`.
8
+ - Every git call the hub makes outside the sandbox goes through `src/hub/git.ts` (`hubGitArgv`/`hubGitEnv`/`hubGitSync`), which keeps the repository's configuration from naming a program the hub runs (#281); a test walks all of `src/` (src/local/ included) for a direct `spawn("git"` and fails on one outside `src/hub/git.ts` and the sandboxed tool's `src/local/tools.ts`. The sandboxed `git` tool keeps its own behavior: it runs inside the sandbox. `hubGitArgv(args, { credentials: true })` keeps the person's credential helper and is only for the repository the hub creates itself (`~/.agenthub/memory`, #258), never a project's. The submodule `config`/`hooks` refusal costs a submodule whose path has `hooks` as a whole component all git-dir writes (docs/security.md).
8
9
  - SBPL strings go through `sbplString` (the plain `"..."` form). In the raw `#"..."` form a backslash escapes nothing, so a `"` in a path ends the literal and the whole profile fails to parse.
9
10
  - One denylist, `src/local/deny.ts`: the path guard, the seatbelt profile and the memory filter all read it. Seatbelt sees absolute paths, so `local.deny` entries are anchored under the project root (a bare `private/` once denied all of `/private/var`).
10
11
  - `guardPath` walks with `lstat`: `existsSync` follows symlinks, so a dangling link looked like a new file and the write landed at its target.
@@ -13,4 +14,6 @@ Scope: the hub-native local worker, its tools, path guard, denylist and seatbelt
13
14
  - After a tool with side effects ran, a failed turn is reported, never redelivered.
14
15
  - `ctx.permit` answers a `ToolApproval` (`true`, `false`, `"expired"`, `"aborted"`, #253): only `true` permits an effect, so never test it for truthiness. A tool returns `approvalError`'s text for the rest: a denial, the expiry (which tells the model not to retry) or the withdrawal. A turn that two unanswered approvals ended (`ApprovalWaitStop`; counting rule: `adapters.md`) completes its missing tool results, settles its delivery `completed` with the reason and is never handed back for replay, also when no tool had a side effect.
15
16
  - Hub tool calls (`hub_send`, `TASK_TOOL_NAMES`, `CONDUCTOR_TOOL_NAMES`) are never passed to `capture.observe`; `Tasks` alone decides what hub tool text reaches memory (#230).
16
- - A tool that asks for approval passes its name to `ctx.permit`, and `write`/`edit` also the `guardPath` result, never the model's path string: `ask-when-needed` grants on that real path through `grantablePath` (`src/hub/permission-mode.ts`). `bash` and mutating `git` pass no path. After the awaited approval `write`/`edit` run `guardPath` again and refuse a target that changed (#242).
17
+ - A tool that asks for approval passes its name to `ctx.permit`, and `write`/`edit` also the `guardPath` result, never the model's path string: also pass the original path separately through every permit wrapper; `ask-when-needed` grants only when both the original project-relative spelling and the real path avoid protected names through `grantablePath` (`src/hub/permission-mode.ts`). `bash` and mutating `git` pass no path. After the awaited approval `write`/`edit` run `guardPath` again and refuse a target that changed (#242).
18
+
19
+ - Honor the live `protect_instruction_files` policy in scoped grants for the three instruction names; keep reads, explicit approval and never-ask unchanged. Only the settings registry writes this machine-local policy (#323).
@@ -2,11 +2,16 @@
2
2
 
3
3
  Scope: the task flow, assignment, the state machine and the hub's MCP tools. Read before editing `src/hub/tasks.ts`, `src/hub/board.ts`, `src/hub/routing.ts`, `src/hub/task-sweep.ts`, `src/hub/conductor.ts`, `src/hub/supervision.ts`, `src/hub/tool-refusal.ts` or `src/hub/hub-tools.ts`.
4
4
 
5
- - Tool callers are models: MCP `inputSchema` is not enforced on the way in. Normalize at the boundary (`cleanRefs`) before anything reaches the board, and never throw after a board write: a publish or notify that follows a `review()` or `done()` board write goes through the one `releaseNotice` guard, which turns a failed publish into a console notice, drops a failed notify and never retries (#231, #243). Of the escalation a repeated `changes_requested` triggers, only `escalate()`'s own notice and "moved" message are guarded: the assignment inside it (`assignOwner`) still throws after its board write, and must not be wrapped before #276 decides, because the #231 ready-offer retry and the #254 uncertain-receipt test depend on that throw.
5
+ - Tool callers are models: MCP `inputSchema` is not enforced on the way in. Normalize at the boundary (`cleanRefs`) before anything reaches the board, and never throw after a board write: a publish or notify that follows a `review()` or `done()` board write goes through the one `releaseNotice` guard, which turns a failed publish into a console notice, drops a failed notify and never retries (#231, #243). An assignment's delivery once its board write is saved (#276, #297): the move is fact, the failed publish is not retried in place (the journal's own recovery covers a storage error, and a retry may duplicate a publish that actually left), and a person looks for a queued or needs_review delivery with `ahub queue list --peer <owner>` (then `ahub queue show <id>`, or the accepted entry in `ahub task show <id>` if the journal is down) before re-sending with `ahub task assign <id> <owner>`. The distinction is made once, in `assignOwner`: a failure after the write prints the one console notice from `undeliveredNotice` right there, once, and throws `AssignmentUndeliveredError` carrying the saved task; callers (`review()`'s repeated rejection, `escalate` and its callers, the ready offer, both relays, the idle sweep, rescreen) decide control flow by the type and print nothing more, and `propose`, `assignTo` and `decline` additionally let the error reach the caller in its turn, which is a person or peer that can act. Two exceptions get no notice, by construction: a row superseded meanwhile, meaning a later history entry that moves or settles the task (another assignment, an unassign, the owner's accept, done, approval, close), while entries that do neither (the hub's quiet bookkeeping: answers, reviewer changes, idle sweeps, check results) leave the assignment undelivered and the notice stands, so the original error goes to the caller as before only on supersession; and a claim's own accepted write and "claimed by" line stay outside the delivery rule (a claim is its own hand-over; its overlap and routing announcements to earlier owners are deliveries and share the wrapper, so a failure there does read as one). A failure before the write stays a plain `Error` (#254), a failed write leaves the ready offer unconsumed (#231), and the notices before the write (waits, no-owner, the reserved-owner line) go through `releaseNotice` too, so those pre-write notices cannot fail their caller.
6
6
  - `assign()` never defaults to the task's current owner, or a decline can only come back to the decliner. A reservation (#207) is the same risk: the reserved owner goes first only through `blocked()`, and a decline or an escalation for reason `rejections` or `manual` records the owner it left in the history entry's `from`, which `excluded()` reads, so no later reroute hands the task back through its reservation.
7
7
  - The state machine allows `in_progress -> approved` only for classes without a reviewer; `review()` checks `in_review` itself.
8
+ - `.agenthub/routing.local.toml` (#269) may hold only `stay_switch` and per-class `peers`, `escalate_to`, `route` and `pi_backend` (`checkOverlay` in `src/hub/routing.ts`): never widen it to targets, routes, signals or constraints, which stay in the hand-written `routing.toml`. `parseRouting` is the one merge, and `assign()` reads `routing.sources` only for its trace line.
8
9
  - Conductor authority requires the explicit unique role before capability checks. The only exceptions run before that check in `Conductor.execute` and stay task-scoped: a task's current owner and reviewer read it with `hub_task_show` (#208), and its proposer redirects it with `hub_task_assign` while nobody ever accepted it and no person moved it (#207). Removing them strands reviewers that missed a review request. Assignments preserve the acting peer. A peer releases only its own conductor hold; operator release and manual, quota and recovery holds remain separate paths.
9
10
  - The PII screen (#198) runs in `propose` before anything else reads the task (no board row, envelope, triage or brief before its verdict), and `unknown` is PII. A call that screens free text (`done`, `review`, `remember`) screens only after its own checks pass, and `done` and `review` run them again after the awaited screen, because the task can change meanwhile. A history entry marked `withheld` stays withheld in public views, completed notices and review requests, whatever `nameable()` says; `ahub ask` shows it only on campus, like a note that matches a PII pattern.
10
11
  - `taskOp` (`src/hub/daemon.ts`) lists full PII rows only when `onPrem` holds: the in-process local worker inside a PII turn (#230). Every other caller, the local worker's ordinary turns included, gets stubs.
11
12
  - A guard in `tasks.ts` or `conductor.ts` passes `preEffect` (throws `PreEffectToolRefusal`, `src/hub/tool-refusal.ts`) only before the operation's first board or hold write; a throw after a write, a readback failure or a wrapped error stays a plain `Error` (#254). The Pi side of the rule is in `adapters.md`.
12
13
  - Supervision reasons use structured enums, never history notes, check output or approval titles. Round signatures survive restart; later summaries count only newly joined tasks.
14
+
15
+ - Pass off-roster ids as data into `assign()` so its trace and every owner/reviewer choice share the membership filter. Refuse a manual off-roster assignment before a board write, including a reservation. Use `reassignForRoster` through the existing assignment delivery path on removal, including in-review ownership while keeping its review state; keep the old owner or reviewer when no eligible replacement exists and emit the guarded public-title notice. Preserve budget-relay behavior when sharing that implementation.
16
+
17
+ - Phase checklists are written only through Tasks.recordLifecycle by that task's reviewer/person. Validate exact current Tasks.after edges and privacy before saving; never change dependencies, accepted owners or reservations from an import. Keep verified checklist claims separate from externally read CI, and bind them to the current reviewed head. Export only derived sanitized board/config projections.
@@ -6,7 +6,10 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
6
6
  - In Bun 1.3.14 a test timeout that fires while `Bun.spawnSync` runs can start the next test inside spawnSync's own event loop, where another spawnSync then spins at full CPU for good (trigger not pinned down; see README). Bun 1.4.2 still runs the next test inside the outer spawnSync's event loop. `scripts/check.sh` runs tests with a 20 s timeout and under `scripts/hang-watch.sh`; a test that spawns and reads the process table gets a timeout of its own.
7
7
  - The Switchyard sidecar, sandboxed commands and the MLX server get a fixed environment (`PATH`, `HOME` and their own keys), so test knobs for their fakes travel in a wrapper script or argv, not in `process.env`; native peer children inherit the hub's environment minus the agent markers (`peerChildEnv`).
8
8
  - Bind agent identity markers explicitly in native/CLI fixture children. The full gate starts the test runner without inherited agent markers and without an `ahub claude` session's hub variables (`AGENTHUB_PROJECT_DIR`, `AGENTHUB_STATE_DIR`, `AGENTHUB_INSTANCE_ID`, `AGENTHUB_LAUNCH_ID`); a new variable a native session exports and a fixture child reads joins that `env -u` list in `scripts/check.sh`. Never weaken production marker detection to make them pass.
9
+ - `bunfig.toml` preloads `test/hub-home.ts`, which gives every test run (the gate and `bun test <file>` alike) a temp `AGENTHUB_HOME` unless one is set: hub memory (#258) is on with any project config, and no test may write the person's `~/.agenthub`. Do not remove it; a test that needs its own home sets and restores the variable.
9
10
  - A free-port probe binds `hostname: "127.0.0.1"`, matching the loopback server that takes the port (#232).
10
11
  - `ahub upgrade` without `--to` asks the registry (`npm view`) and hands a target newer than the CLI to `bun x` (#272): a test that reaches either puts a fake `npm` first on `PATH` and a recording stand-in for `process.execPath`, as `test/upgrade-cli.test.ts` does. The interactive screens are tested through a scripted `ScreenIO`; a CLI test that needs them marks `process.stdin` and `process.stdout` as terminals in a wrapper script.
12
+ - Cross-process fixtures wait for the consuming client to process its handshake (the channel connected diagnostic), not merely the daemon's attached/idle state. Deadline-routing fixtures hold and release responses at the fallback decision; do not place a fixed sleep between deadline fractions and assume scheduler headroom (#287). Receipt-order fixtures use the normal watchdog for ordinary I/O, trigger expiry after observed native acceptance, and await a consumed transport barrier before asserting on late results.
13
+ - A bounded polling wait must assert its condition after the last attempt, or document why giving up is intentional. Append-only event histories wait for a correlated closing event, not removal of the opening event. Before changing a slow wait, identify the unreached state and repair its cause (#296).
11
14
  - A request that a test expects a stop or shutdown to cut gets its rejection handler where the request is created, never after the awaited teardown: a late `catch` races the reset and the run fails on an unhandled rejection.
12
- - Run `bun scripts/seeded-check.ts` before reporting the seeded-guard gate complete. Keep the six `scripts/seeds.json` cases sequential; require the same named test green before its seeded assertion fails. Seed rot, survivors, setup/compiler failures, timeout/watchdog termination and process leaks fail the gate. Never run the full seed runner from a unit test or mutate the source checkout; inspect preserved failed fixtures before removing them. CI runs this in the required Linux `seeded guards` job after the ordinary check, separately from `scripts/check.sh`.
15
+ - Run `bun scripts/seeded-check.ts` before reporting the seeded-guard gate complete. Keep the `scripts/seeds.json` cases sequential; require the same named test green before its seeded assertion fails. Seed rot, survivors, setup/compiler failures, timeout/watchdog termination and process leaks fail the gate. Never run the full seed runner from a unit test or mutate the source checkout; inspect preserved failed fixtures before removing them. CI runs this in the required Linux `seeded guards` job after the ordinary check, separately from `scripts/check.sh`.