@staix/agent-hub 0.12.22 → 0.12.23

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 (78) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +16 -8
  3. package/docs/agent-notes/adapters.md +10 -5
  4. package/docs/agent-notes/benchmarks.md +3 -0
  5. package/docs/agent-notes/budget.md +4 -0
  6. package/docs/agent-notes/daemon.md +8 -1
  7. package/docs/agent-notes/local-worker.md +3 -2
  8. package/docs/agent-notes/tasks.md +2 -1
  9. package/docs/agent-notes/tests.md +2 -0
  10. package/docs/events.md +7 -3
  11. package/docs/operations.md +232 -26
  12. package/docs/quickstart.md +9 -4
  13. package/docs/security.md +15 -6
  14. package/docs/smoke.md +72 -5
  15. package/docs/specs/2026-09-19-agent-hub-design.md +77 -10
  16. package/docs/verification/2026-10-10-cli-output-phase1.md +309 -0
  17. package/docs/verification/2026-10-10-cli-output-phase2.md +672 -0
  18. package/docs/verification/2026-10-11-0.12.23.md +90 -0
  19. package/docs/verified.json +25 -16
  20. package/package.json +1 -1
  21. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  22. package/plugins/agent-hub/server.js +14 -3
  23. package/src/adapters/acp-quota.ts +13 -0
  24. package/src/adapters/acp.ts +95 -13
  25. package/src/adapters/codex-appserver.ts +43 -13
  26. package/src/cli/bench.ts +2 -1
  27. package/src/cli/console-state.ts +28 -22
  28. package/src/cli/console.ts +20 -10
  29. package/src/cli/help.ts +19 -14
  30. package/src/cli/identity.ts +2 -2
  31. package/src/cli/init.ts +1 -1
  32. package/src/cli/main.ts +272 -159
  33. package/src/cli/output.ts +363 -0
  34. package/src/cli/preview.ts +2 -1
  35. package/src/cli/reset.ts +3 -0
  36. package/src/cli/status-lines.ts +0 -28
  37. package/src/cli/terminal-recovery.ts +27 -1
  38. package/src/cli/upgrade-runtime.ts +24 -7
  39. package/src/cli/upgrade.ts +23 -5
  40. package/src/hub/board.ts +2 -0
  41. package/src/hub/budget.ts +39 -7
  42. package/src/hub/bus.ts +10 -11
  43. package/src/hub/child-process.ts +11 -0
  44. package/src/hub/conductor.ts +23 -11
  45. package/src/hub/config-trust.ts +16 -5
  46. package/src/hub/conflicts.ts +2 -0
  47. package/src/hub/context-window.ts +8 -1
  48. package/src/hub/crash.ts +2 -1
  49. package/src/hub/daemon.ts +338 -23
  50. package/src/hub/db-timeout.ts +3 -0
  51. package/src/hub/delivery-journal.ts +2 -1
  52. package/src/hub/envelope.ts +1 -1
  53. package/src/hub/events.ts +2 -0
  54. package/src/hub/execution-budget.ts +2 -0
  55. package/src/hub/facts.ts +5 -4
  56. package/src/hub/git.ts +32 -0
  57. package/src/hub/hub-tools.ts +1 -1
  58. package/src/hub/lifecycle.ts +10 -4
  59. package/src/hub/manager.ts +18 -5
  60. package/src/hub/peers.ts +5 -3
  61. package/src/hub/permission-mode.ts +14 -0
  62. package/src/hub/project.ts +2 -1
  63. package/src/hub/recovery-error.ts +31 -0
  64. package/src/hub/recovery-store.ts +6 -5
  65. package/src/hub/registry.ts +2 -1
  66. package/src/hub/routing.ts +114 -13
  67. package/src/hub/settings.ts +392 -0
  68. package/src/hub/snapshots.ts +4 -2
  69. package/src/hub/start-mode.ts +48 -0
  70. package/src/hub/tasks.ts +129 -31
  71. package/src/hub/terminal-open.ts +64 -0
  72. package/src/hub/ui.ts +25 -13
  73. package/src/local/deny.ts +29 -7
  74. package/src/local/sandbox.ts +44 -6
  75. package/src/memory/recall.ts +2 -2
  76. package/src/models/mlx.ts +2 -1
  77. package/src/pi/tool-receipts.ts +2 -1
  78. package/src/ui/index.html +157 -10
package/CHANGELOG.md CHANGED
@@ -4,6 +4,25 @@ 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.23
8
+
9
+ - 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).
10
+ - 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).
11
+ - 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)
12
+ - Bound SQLite contention on every shared database connection with a per-file busy timeout and a source guard. (#303)
13
+ - 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).
14
+ - 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).
15
+ - 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).
16
+ - 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)
17
+ - 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).
18
+ - 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).
19
+ - 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).
20
+ - 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).
21
+ - 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).
22
+ - 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).
23
+ - 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).
24
+ - 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.
25
+
7
26
  ## 0.12.22
8
27
 
9
28
  - 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
@@ -4,7 +4,7 @@ Native multi-agent hub for one developer's machine: Claude Code, Codex, Kimi Cod
4
4
  hub-owned local-LLM worker 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.23, 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
 
@@ -28,7 +28,7 @@ cd <your project> && ahub init && ahub up
28
28
  Or install the same version from GitHub:
29
29
 
30
30
  ```bash
31
- bun add -g github:STAIxBWLB/agent-hub#v0.12.22 && ahub setup
31
+ bun add -g github:STAIxBWLB/agent-hub#v0.12.23 && ahub setup
32
32
  ```
33
33
 
34
34
  The installed commands remain `ahub` and `agent-hub`.
@@ -83,17 +83,19 @@ an N-peer message bus and attaches each agent through its native control surface
83
83
  ## Pi with DGX and MLX
84
84
 
85
85
  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;
86
+ `.agenthub/config.json`. Set `pi.auto_start` to `true` to start it with the hub.
87
+ Pi starts in its TUI by default; `ahub pi --headless`, or the start mode
88
+ setting (`ahub settings set peers.pi.start_mode headless`), asks for the
89
+ background peer instead. The project uses its existing OmniRoute connection for DGX;
88
90
  `pi.dgx_coding` and `pi.dgx_fast` select the gateway's configured model IDs.
89
91
 
90
92
  ```bash
91
93
  ahub models setup # prepare bounded Qwen3.5 4B MLX in an existing Ollama service
92
94
  ahub models start # verify loopback Ollama/model readiness (no daemon spawn)
93
95
  ahub models status
94
- ahub pi --mode headless --backend auto
96
+ ahub pi --headless --backend auto
95
97
  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
98
+ ahub pi # its TUI in this terminal; an idle headless session moves into it
97
99
  ```
98
100
 
99
101
  The default routing template assigns implementation, bulk edits and tests to
@@ -151,7 +153,12 @@ browser cannot be opened.
151
153
  Open the link within 60 seconds; the browser session lasts one hour.
152
154
 
153
155
  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.
156
+ peers, answer approvals, start a peer in its start mode and show the settings.
157
+ An ordinary session changes the safe switches and lowers a mode to `ask`; the
158
+ rest needs `ahub ui --settings` or `ahub settings` in a terminal. See
159
+ [Settings](docs/operations.md#settings-from-the-dashboard-and-the-terminal) and
160
+ [Start modes](docs/operations.md#start-modes-tui-by-default-headless-as-opt-in).
161
+ Budget pauses still require the terminal override.
155
162
  Private envelopes and PII tasks stay redacted. Local-worker and Pi tool details and
156
163
  allow decisions stay in `ahub tail` / `ahub permit`; the page can deny them.
157
164
  No dashboard port is opened until requested. See [the session security model](docs/security.md#local-dashboard-sessions-issue-6).
@@ -176,7 +183,8 @@ ahub ui --all
176
183
 
177
184
  The unified dashboard lists registered roots and opens their boards, streams,
178
185
  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
186
+ confirmation. Its Start control opens a peer's TUI only through a terminal provider
187
+ (`terminal.open` or Orca); without one, launch Claude/Codex TUIs from a terminal. The manager stays available
180
188
  when a project stops; `ahub ui --all --stop` stops only the manager. Existing
181
189
  `ahub ui` still opens the current project's dashboard.
182
190
 
@@ -1,8 +1,9 @@
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
5
  - 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.
6
+ - 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
7
  - 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
8
  - Codex 0.154.0 `agentMessage` items carry `text` and `phase` (not `content[]`); only the last non-`commentary` message of a turn is shared.
8
9
  - 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,7 +12,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
11
12
  - 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
13
  - 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
14
  - 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
+ - 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.
15
16
  - 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
17
  - 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
18
  - 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.
@@ -19,13 +20,17 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
19
20
  - 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
21
  - 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
22
  - 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.
23
+ - 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).
24
+ - 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
25
  - 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
26
  - 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
27
  - 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
28
  - 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
29
  - 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.
30
+ - `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.
31
+ - 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
32
  - 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
33
  - 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.
34
+
35
+ - 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.
36
+ - 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.
@@ -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).
@@ -1,9 +1,11 @@
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
+ Scope: the 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/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
4
 
5
5
  - 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
6
  - `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.
7
+ - 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`.
8
+ - 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
9
  - `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
10
  - 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
11
  - 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 +25,8 @@ Scope: the control WS and its protocol, state files, the Claude channel's reconn
23
25
  - 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
26
  - 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
27
  - `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.
28
+
29
+ - 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.
30
+ - 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.
31
+ - 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).
32
+ - 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).
@@ -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. 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.
@@ -2,9 +2,10 @@
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.
@@ -8,5 +8,7 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
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
9
  - A free-port probe binds `hostname: "127.0.0.1"`, matching the loopback server that takes the port (#232).
10
10
  - `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.
11
+ - 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.
12
+ - 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
13
  - 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
14
  - 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`.
package/docs/events.md CHANGED
@@ -41,17 +41,21 @@ marked `private: true`, and PII tasks `pii: true`.
41
41
  | `task` | `id`, `event` (the board history event, e.g. `proposed`, `assigned`, `done`, `check failed`, `blocked`, `ready`), `by`, `state`, `owner`, `reviewer`, `class`, `pii`, and `reason` when the board recorded a move reason (`manual`, `budget`, `offline`, `idle`, `rejections`, `declined`, `inference_failed`, `delivery_failed`) |
42
42
  | `overlap` | `task`, `owner`, `others` (`task`, `owner`, `paths`, and `symbols` when plans name the same symbol; a name that matches a PII pattern is left out, so either list can be empty), the structured twin of the console notice |
43
43
  | `permission_mode` | `peer`, `from`, `to` (`ask`, `ask-when-needed`, `never-ask`): a person set a peer's permission mode (`from` equals `to` when it was already in force) or confirmed a never-ask default (issue #242) |
44
+ | `settings` | `key` (a registry key such as `permission.kimi` or `routing.classes.implement.peers`), `from`, `to` (the value before and after: the one in force for a setting that applies at once, the stored one for a setting read at a start; a closed word, a boolean, a list of peer ids, a route id, or null for none), `source` (`dashboard`, `terminal`), and `undo: true` for an undo: a person changed a setting from the dashboard or `ahub settings` (issue #269). A mode change also writes its `permission_mode` event |
44
45
  | `permission` | `id`, `peer`, `event` (`requested`, then one of `answered`, `expired`, `cancelled`) and, on the closing event, `latencyMs`, `surface` (`console`, `dashboard`, `terminal`: where a person answered; absent when the hub closed the request) and `option` (the kind of the option chosen: `allow_once`, `allow_always`, `reject_once`, `reject_always`): one approval request relayed to the console and how it ended. `expired` is `approvals.timeout_s` without an answer. `cancelled` with a `surface` is a person's deny in the console or by `ahub permit <id> deny` (a deny on the dashboard is `answered` with a reject option); without one, the hub withdrew the request (its call or turn ended, the peer was stopped, or the hub is stopping). Never the title or the tool |
45
46
  | `pii_screen` | `task` (absent for a budget hand-off or a note not about a task), `item` (`task`, `summary`, `review note`, `handoff`, `note`), `label` (`pii`, `clear`), `source` (`regex`: a pattern matched and no call was made; `screen`; `unknown`: no verdict, handled as PII), `category` (`name`, `student_id`, `phone`, `address`, `grade`, `health`, `other`), `miss` (why there was no verdict: `too long`, `off campus`, `timeout`, `unreadable`, `failed`), `ms` (absent for `regex`): one PII screen verdict while `signals.pii_screen = "local"` (issue #198); never the text |
46
- | `context_pressure` | `peer`, `source` (`claude_statusline`, `codex_token_usage`), `measuredAt` (epoch ms of the reading), `used` (the fraction of the context window in use, 0 to 1), `window` (its size in tokens, or null): a fresh reading of a peer's native context reached `context.gate`. Recorded once per crossing, until a reading below the gate or a new native session rearms it, and not while the peer is paused or offline or the hub is in recovery or stopping |
47
+ | `context_pressure` | `peer`, `source` (`claude_statusline`, `codex_token_usage`, `acp_usage_update`), `measuredAt` (epoch ms of the reading), `used` (the fraction of the context window in use, 0 to 1), `window` (its size in tokens, or null): a fresh reading of a peer's native context reached `context.gate`. Recorded once per crossing, until a reading below the gate or a new native session rearms it, and not while the peer is paused or offline or the hub is in recovery or stopping |
47
48
  | `quota` | `peer`, `windows` (`id`, `used`, `resetsAt`), `hard`, `measuredAt` (when the reading was taken, if not when it arrived: Claude's numbers come through a file) |
48
49
  | `conflict` | `peer`, `task` (its task in progress, when it had one), `other` (the other owner's open task), `owner`, `paths` (names that match a PII pattern left out, so it can be empty), `concurrent` (another peer worked during the turn), `turns` (the two turn ids, when two overlapping turns' snapshots both hold the change) |
49
50
 
50
51
  Token usage by adapter:
51
52
 
52
53
  - Kimi (ACP `usage_update`, and the usage a `session/prompt` result carries): recorded when it holds a checked
53
- total or an input/output pair. The `{used, size}` shape Kimi sends is context occupancy and is not recorded
54
- (issue #285 tracks showing Kimi's usage).
54
+ total or an input/output pair. The `{used, size}` shape Kimi sends is context occupancy: it is shown as the peer's
55
+ native context reading (source `acp_usage_update`, #285 phase 1) and no `tokens` event is written from it; Kimi's
56
+ consumption is not recorded yet (phase 2). The adapter accepts the update whenever it names the current session;
57
+ Kimi 2.1.1's source text emits it after the prompt resolves, when the peer may already be idle, and that order is
58
+ not yet observed live through the hub (docs/smoke.md).
55
59
  - Codex (app-server `thread/tokenUsage/updated`): recorded as the growth of the thread's running total, so
56
60
  compaction estimates, usage-limit refreshes and the replay to a reattaching connection add nothing. A thread
57
61
  started under the hub counts from zero; a resumed thread's first update is its history and only sets the