@staix/agent-hub 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +2 -2
  3. package/docs/agent-notes/adapters.md +2 -1
  4. package/docs/agent-notes/budget.md +1 -1
  5. package/docs/agent-notes/bus.md +5 -2
  6. package/docs/agent-notes/daemon.md +5 -4
  7. package/docs/agent-notes/local-worker.md +1 -1
  8. package/docs/agent-notes/models.md +2 -1
  9. package/docs/agent-notes/tasks.md +2 -1
  10. package/docs/agent-notes/tests.md +2 -2
  11. package/docs/dogfood.md +10 -2
  12. package/docs/operations.md +192 -6
  13. package/docs/profiles.md +29 -4
  14. package/docs/quickstart.md +9 -1
  15. package/docs/security.md +7 -1
  16. package/docs/smoke.md +6 -0
  17. package/docs/specs/2026-09-19-agent-hub-design.md +94 -1
  18. package/docs/verification/2026-10-30-0.22.0.md +49 -0
  19. package/docs/verification/2026-10-30-0.23.0.md +70 -0
  20. package/docs/verification/2026-10-30-445-capability-probes.md +172 -0
  21. package/docs/verification/2026-10-30-adapter-operational-settings.md +261 -0
  22. package/docs/verification/2026-10-30-bus-delivery-operational-settings.md +77 -0
  23. package/docs/verification/2026-10-30-jcode-ac4.md +24 -0
  24. package/docs/verification/2026-10-30-jcode-ac6.md +52 -0
  25. package/docs/verification/2026-10-30-model-operational-settings.md +211 -0
  26. package/docs/verification/2026-10-30-operational-settings-closure.md +1217 -0
  27. package/docs/verification/2026-10-30-recovery-wait-settings.md +141 -0
  28. package/docs/verification/2026-10-30-ui-console-operational-settings.md +1845 -0
  29. package/docs/verified.json +15 -15
  30. package/package.json +1 -1
  31. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  32. package/plugins/agent-hub/ROLE-NOTICE.md +1 -1
  33. package/plugins/agent-hub/server.js +559 -115
  34. package/src/adapters/acp.ts +48 -2
  35. package/src/adapters/claude-channel.ts +6 -4
  36. package/src/adapters/codex-appserver.ts +19 -0
  37. package/src/adapters/jcode.ts +1 -1
  38. package/src/adapters/local-worker.ts +3 -3
  39. package/src/adapters/native-capabilities.ts +50 -0
  40. package/src/adapters/registry-types.ts +3 -10
  41. package/src/cli/capabilities-output.ts +30 -0
  42. package/src/cli/console-state.ts +8 -5
  43. package/src/cli/console.ts +27 -4
  44. package/src/cli/help.ts +3 -1
  45. package/src/cli/identity.ts +4 -1
  46. package/src/cli/learn-output.ts +38 -0
  47. package/src/cli/main.ts +28 -2
  48. package/src/cli/recovery-waits.ts +26 -0
  49. package/src/cli/smoke-pty-bridge.ts +67 -18
  50. package/src/cli/smoke-pty.ts +10 -2
  51. package/src/cli/upgrade-runtime.ts +16 -14
  52. package/src/cli/upgrade.ts +2 -1
  53. package/src/hub/board.ts +8 -2
  54. package/src/hub/bus.ts +117 -17
  55. package/src/hub/capabilities.ts +215 -0
  56. package/src/hub/config-apply.ts +8 -4
  57. package/src/hub/config-trust.ts +1 -0
  58. package/src/hub/control-client.ts +41 -8
  59. package/src/hub/daemon.ts +436 -33
  60. package/src/hub/deferred-hub.ts +2 -2
  61. package/src/hub/delivery-hints.ts +2 -2
  62. package/src/hub/delivery-journal.ts +80 -61
  63. package/src/hub/envelope.ts +12 -2
  64. package/src/hub/events.ts +2 -0
  65. package/src/hub/hub-tools.ts +16 -1
  66. package/src/hub/inference.ts +10 -5
  67. package/src/hub/invocations.ts +316 -0
  68. package/src/hub/learning.ts +33 -1
  69. package/src/hub/operational-settings.ts +180 -1
  70. package/src/hub/peers.ts +3 -0
  71. package/src/hub/restart.ts +2 -2
  72. package/src/hub/room-invocations.ts +145 -0
  73. package/src/hub/settings.ts +22 -2
  74. package/src/hub/shipped-packs.ts +5 -2
  75. package/src/hub/shipped-roles.ts +13 -0
  76. package/src/hub/tasks.ts +92 -1
  77. package/src/memory/capture.ts +2 -2
  78. package/src/models/relay.ts +20 -9
  79. package/src/rooms/application.ts +67 -2
  80. package/src/rooms/boundary.ts +43 -11
  81. package/src/rooms/service.ts +40 -2
  82. package/templates/checks/analysis.mjs +59 -0
  83. package/templates/checks/fixtures/analysis-expected.json +1 -0
  84. package/templates/checks/fixtures/analysis-input.json +1 -0
  85. package/templates/checks/fixtures/analysis.mjs +7 -0
  86. package/templates/checks/fixtures/ops.json +4 -0
  87. package/templates/checks/fixtures/writing-rules.json +4 -0
  88. package/templates/checks/fixtures/writing.json +5 -0
  89. package/templates/checks/ops.mjs +40 -0
  90. package/templates/checks/writing.mjs +40 -0
  91. package/templates/packs/analysis.pack.json +14 -0
  92. package/templates/packs/ops.pack.json +14 -0
  93. package/templates/packs/writing-pack.pack.json +14 -0
  94. package/templates/roles/NOTICE.md +1 -1
  95. package/templates/roles/ops/meeting-actions-owner.md +26 -0
  96. package/templates/roles/ops/project-manager.md +26 -0
  97. package/templates/roles/writing/editor.md +26 -0
  98. package/templates/roles/writing/style-checker.md +26 -0
  99. package/templates/roles/writing/terminology-checker.md +26 -0
  100. package/templates/roles/writing/translator.md +26 -0
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.23.0
8
+
9
+ - Write and external invocations (#445 C3, task 180, #991): a shared `write` or `external` capability runs only for a caller's open non-PII task (`hub_invoke` `task`) and only after a person allows that one call; unattended hubs refuse before prompting, and `destructive` still never runs. The approval shows caller, target, capability, class, digest, task and the screened, sanitized arguments (200 characters per value, marked when cut); the previews stay on person permission surfaces and never reach audit, events or logs. Every admission gate is rechecked after the approval. The task history records an invocation's queued and terminal metadata without changing the task's state. Only a `read` may replay, at most once after a safe failure; an uncertain write or external outcome is held as `needs_review`, never retried. `hub_invoke` waits up to the approval window instead of the 30 s tool timeout, and a closed or cancelled call is withdrawn before it can be granted. Not live-verified (C4).
10
+ - Read invocations across hubs (#446 X6, task 187, #992): `hub_invoke` can name an exact remote `peer@label`. Both persons must pin the same capability, digest and class `read` (`invocation.share` on the source under the `peer@label` key, and the target's own local entry); there is no remote discovery, and write, external and destructive never cross. Only bounded flat scalar arguments travel; the target re-checks its schema and every #445 gate. Task and approval refs stay local. A remote call cannot start another one, duplicates run once, and an uncertain handoff or result is `needs_review`, never replayed. Remote results keep the remote-turn exclusions from memory, history and telemetry. Not live-verified across two machines (X7).
11
+ - Writing, analysis and ops domain packs (#513b, task 322, #993): `writing-pack` (base `writing`; editor, translator, style and terminology roles), `analysis` (base `research`, analyst) and `ops` (base `general`; project-manager and meeting-actions-owner roles), each with a criterion-id rubric and one advisory check. The checks are standalone scripts in `templates/checks/` that a person runs by hand; packs and `ahub init` never execute them. The role catalog now ships 17 roles.
12
+ - Adapter timing settings (#489 H5, task 446, #982): `adapter_timing.acp_tool_ceiling_min` (1-30, default 30), `adapter_timing.acp_activity_batch_ms` (10-50, default 50) and `adapter_timing.jcode_steer_timeout_s` (5-60, default 5), applied at the next start of a Kimi, generic ACP or Jcode peer. Running peers keep their values.
13
+ - Model relay and inference settings (#489 H6, task 447, #984): `model_relay.cooldown_failures`, `model_relay.cooldown_s`, `model_relay.cooldown_cap_s`, `model_relay.terminal_history_limit`, `inference.backoff_min`, `inference.digest_min_items` and `inference.digest_min_chars`. All seven need a daemon restart; unset behaves as before.
14
+ - Console presentation settings (#489 H7, #987): `console.poll_ms` (2000-60000, default 4000), `console.notice_ms` (raise only, default 10000), `console.history_limit`, `console.pending_stream_limit` and `console.attention_limit` (each 1 to its old default), read once at the next console launch. The three limits only trim the console's own display state, never queues, deliveries or approvals.
15
+ - An ACP peer whose stdout carried another session's command list in the same chunk as its own no longer adopts the foreign list and lists nothing (#567, #988).
16
+ - Read invocations (#445 C2, task 179): `hub_invoke { peer, capability, args?, task? }` asks another peer to run one capability the person shared from it and returns an invocation id at once; the result arrives later as one hub-headed, sanitized message of at most 8 KiB, addressed to the caller only. A `read` capability runs without approval (write and external need a task and a person's approval, C3 above; destructive never runs). The target gets one hub-written request and answers under its own permissions; `[REFUSED]` declines. The answer is taken over inside the bus before anything is built from it, so it reaches no one but the caller's one result; an answer the PII or secret checks do not clear, or a callee whose turn became private, is `withheld` with fixed text and no body kept. Every refusal names its reason before any envelope exists: PII, nesting, target offline, paused, held, off roster or pull-only, not shared or changed, class, task, arguments, screen, and per-caller or per-target caps. `invocation.timeout_s` (default 600) expires a queued request and times out a delivered one. A row in `hub.db` and an `invocation` event hold ids only. New envelope kinds `invocation` and `invocation_result` are delivered alone and never condensed; `invocation` is a daemon feature and recovery refuses a target without it while one is open. Not live-verified (C4).
17
+ - Capability discovery (#445 C1, task 178): `ahub capabilities [--json]` (person only) lists what each attached peer offers, and agents read what you share with `hub_capabilities { peer? }`. Kimi 2.3.2 and Qwen 0.26.1 give their ACP `available_commands_update` skills (builtins never), Codex 0.166.0 gives its enabled `skills/list` entries; any other version, an offline peer and an unanswered list give nothing. Sharing is `invocation.share` in `.agenthub/config.local.json` only, pinned by a digest of kind, name, schema, class, source and invoke form, with an optional person `class`; an entry with no class anywhere counts as `destructive`. `hub_capabilities_publish` lets a session the hub cannot read publish its own list (forced to `invoke: prompt`, bound to its connection). Neither tool runs in a PII turn or reaches memory. This slice only lists; calling a shared capability is C2 and C3 above. The `exec` side-effect class is gone: arbitrary execution is `destructive`.
18
+
19
+ ## 0.22.0
20
+
21
+ - Five bus and delivery limits are settings (#489 H3, task 444, #975): `bus.max_attempts` (1-100, default 3), `bus.digest_max` (1-10, default 10), `bus.pull_budget_bytes` (1-65536, default 65536), `bus.notes_kept` (1-10, default 10) and `bus.terminal_history_limit` (1-2048, default 2048), live from `config.local.json` at their next use. Unset behaves as before; only `bus.max_attempts` may rise above what shipped, and a reload never trims queues, resets attempts or prunes history.
22
+ - Five upgrade-recovery waits are settings (#489 H4, task 445, Refs #489): `recovery.reconnect_wait_s`, `recovery.unmanaged_reconnect_wait_s`, `recovery.control_timeout_s`, `recovery.shutdown_wait_s` and `recovery.terminal_idle_wait_s`. Unset behaves as before; every person-visible wait can be raised and never cut below what shipped, a bad or unreadable file is the default, and a wait already running keeps its deadline.
23
+ - `ahub learn list [--all] | accept <id> | reject <id> [--json]` (#514b, person only, refused for agent shells and the configured conductor): the verdict on the recurring-unmet-criterion cards #514a files. `accept` adds that card's one `review_checklist` line to your current checklist in `.agenthub/config.local.json` through the settings writer (this machine only, never a tracked file, `ahub settings undo` puts it back, applies after a restart) and closes the card; `reject` only closes it. The line and its task count are rebuilt from the card's key line and the board, never parsed from the card's text. The #514a card sentence "nothing in the hub writes it" is amended to say the hub writes it only on `accept` (earlier cards keep the old sentence). The settings event and undo reply for this key carry a line count, not your lines.
24
+ - The macOS PTY bridge no longer hangs in `Bun.spawn` under load, and no longer outlives its caller (#560, Refs #559). Bun up to 1.4.3 can block for good in `Bun.spawn({ terminal })` when the child writes and exits before the call returns (oven-sh/bun#45300, fixed in 1.4.4); the bridge now starts its child behind a bounded gate it opens after the call returns, and stops the child when its caller is gone. The test retry helper `runStarted` is removed.
25
+
7
26
  ## 0.21.0
8
27
 
9
28
  - Tasks and routing operational settings (#489 H2, task 441): six more numbers use the H1 mechanism (fixed rows in `src/hub/operational-settings.ts`, the closed `config-apply.ts` schema, the settings table), all live and with the default equal to the constant they replace: `tasks.demote_at` (1.5), `tasks.demotion_half_life_h` (24), `tasks.contradiction_window_days` (7), `tasks.rescreen_max` (10), `tasks.split_min_observations` (5) and `tasks.split_max_failure_share` (0.3). Each is read where its constant was used, so a change applies at the next demotion read, verdict, re-screen or prediction without a restart, and unset behaves exactly as before. The split thresholds only feed the shadow prediction, which never changes assignment. Nothing here is a safety floor or a contract. `docs/operations.md` lists the other 35 H0 operational items of `tasks.ts`, `routing.ts` and `task-sweep.ts` that stay as they are, and why.
package/README.md CHANGED
@@ -5,7 +5,7 @@ Code, Codex, Kimi Code, Pi and a hub-owned local worker (plus Qwen Code, Jcode a
5
5
  Antigravity if you want them) share a message bus, a task board and a memory, each
6
6
  connected through its own native interface.
7
7
 
8
- Status: 0.21.0, control protocol 16. Bun + TypeScript, one loopback daemon per
8
+ Status: 0.23.0, control protocol 16. Bun + TypeScript, one loopback daemon per
9
9
  project. Site: [agent-hub.staix.net](https://agent-hub.staix.net).
10
10
 
11
11
  ## Quick start
@@ -94,7 +94,7 @@ uses the home must run a version that understands the new layout.
94
94
  [#258](https://github.com/STAIxBWLB/agent-hub/issues/258)).
95
95
 
96
96
  Live checks that still need a real account or a person are listed in the
97
- [release verification report](docs/verification/2026-10-29-0.21.0.md) and the
97
+ [release verification report](docs/verification/2026-10-30-0.23.0.md) and the
98
98
  [smoke checklist](docs/smoke.md).
99
99
 
100
100
  ## Documentation
@@ -44,7 +44,8 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
44
44
  - 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).
45
45
  - 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.
46
46
  - 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.
47
- - ACP tool ceiling (#542): a tool call announced (`tool_call`) and not yet `completed` or `failed` in the current turn keeps a silent busy turn alive past the watchdog, up to `AcpOptions.toolCeilingMs` (never below the watchdog) counted from the last protocol message. Re-arm with `touch(<remaining ceiling, at most the watchdog>)`, never a full extra watchdog, and clear the open calls at each `deliver()`. Past the ceiling, cancel as an ordinary watchdog; its log line names the open tools only on a turn that is not private (latch `isPrivateTurn`, fail closed when it throws), each title as one sanitized, quoted, cut line.
47
+ - ACP tool ceiling (#542): a tool call announced (`tool_call`) and not yet `completed` or `failed` in the current turn keeps a silent busy turn alive past the watchdog, up to `AcpOptions.toolCeilingMs` (never below the watchdog) counted from the last protocol message. Re-arm with `touch(<remaining ceiling, at most the watchdog>)`, never a full extra watchdog, and clear the open calls at each `deliver()`. Past the ceiling, cancel as an ordinary watchdog; its log line names the open tools only on a turn that is not private (latch `isPrivateTurn`, fail closed when it throws), each title as one sanitized, quoted, cut line. The daemon passes `toolCeilingMs`, `activityBatchMs` and Jcode's `steerTimeoutMs` from `adapterOperationalReaders` (`adapter_timing.*`, #982) only when it constructs an adapter; an adapter never reads config itself, and an accepted edit never retimes a running launch.
48
+ - Native capability listings (#445 C1, #985): keep vendor identities and listing shapes in `src/adapters/native-capabilities.ts`, never in `src/hub/capabilities.ts`. `capabilityManifest` lists only on a `NATIVE_PINS` version; any other version returns an empty manifest, never a guess, and a transport failure rejects rather than showing a stale list. Codex `skills/list` uses a negative hub id, and only `enabled: true` skills are listed. ACP keeps `available_commands_update` lists that arrive during `binding` keyed by session (`earlyCommands`, at most `EARLY_COMMANDS_MAX`), and the bind adopts only the bound session's list (#567).
48
49
  - Retire a cancelled Codex native turn until its exact completion or session teardown. Late start acceptance, answer frames and completion must not revive its audience or relabel uncertainty as `failed_safe`. If cancellation has no provable native id, keep the peer offline until verified reattachment.
49
50
  - 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.
50
51
  - 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).
@@ -13,7 +13,7 @@ Scope: quota and native context readings, checkpoints, pause, handoff and resume
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; `ContextWindows.admission` (#802) only copies the reading for new-offer routing and never changes the crossing latch. Refuse private-turn, PII-pattern and all associated open PII-task summaries (owner or reviewer, including in_review, with the current routing policy; use `isClosedTaskState` to exclude approved and cancelled work) before local persistence or cloud memory; never broadcast checkpoint text.
14
14
  - Invalid/stale context readings expose unknown and do not rearm a crossing. Latch a crossing only after its event is accepted; reconsider paused/recovery-held crossings after release with fresh current-session readings. Tee context fields contain finite numeric values or null only, bound to daemon instance, launcher and native session. Context and session go to the per-peer file `claudeStateFile(base, peer)` (the builtin keeps the unsuffixed name); `claude-usage.json` stays one shared file per account.
15
15
 
16
- - 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.
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, except for an `invocation` request: it gets at most one automatic retry, only when `canRetryInvocation` says so, paused or not (rule: `bus.md`).
17
17
  - Kimi's ACP `{used, size}` occupancy is a context reading (`acpContext`, source `acp_usage_update`) bound to the adapter's own session id: never a `tokens` event or a budget window, and an invalid shape reports unknown, not the previous value. A gate crossing sends a checkpoint request only to definitions with `budget.checkpoint` (Claude and Codex); an ACP peer gets the event and the notice (#285).
18
18
  - Jcode (#801): its ACP `usage_update` `{used, size}` is a context reading (`jcodeContextValue`, source `jcode_usage_update`, session-bound); its prompt result `usage` is per-turn consumption recorded as one `jcode_acp` `usage` event (`acpTurnUsage`, `onTurnUsage`) and never reaches `addTokens`, a token window or a budget window. Never read Jcode's `totalTokens`, never infer a quota window from either record, and keep a malformed, wrong-session or stale value unknown. Never read `jcode usage --json`. A later Jcode version repeats `docs/verification/2026-10-28-jcode-0.99.1-usage.md` before its readings are claimed.
19
19
  - A quota resume notice replaces the peer's unread resume notices: carry their moves into the new one (`refs.budgetResumeMoves`, or the whole text of a notice that has none), publish it, and only then withdraw the ones it replaces, so a failed publish keeps them (#306).
@@ -10,12 +10,15 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
10
10
  - `WsPeer.steer` exists only for a push channel session: set at attach, cleared on detach, roster detach and a pull-only attach, and it refuses anything but important work in the current busy turn (#493).
11
11
  - `replyParent()` decides what a reply answers (highest hop, never the `hub` preface). Use it for deliveries and steers alike, or the hop cap can be reset.
12
12
  - Report a send's outcome from `Bus.publishResult`, never from `publish()`'s bare target list: a hop-capped send answers `ok: true, dropped: "hop"` with the hop-cap notice, and the channel tool and `ahub say` print that notice, never "no peers attached" (#546). A native peer's `onMessage` returns that `PublishResult` on a hop drop, its send tool (Pi, the local worker) reports it as not delivered, and no off-roster retention note follows a hop drop (#550).
13
- - What a peer is handed (`out`) and what it stands for (`originals`) differ once a delivery is condensed: the bus keeps both, registers `out` so `reply_to` resolves, puts the originals back on a `failed_safe` receipt and holds any other failure (a thrown `deliver`, a later `onFailed`, a `needs_review` receipt) as `needs_review` with its originals, for `ahub queue resolve`, and counts no attempt when the peer merely got busy while the delivery was prepared. A delivery with an `important` envelope or a remote sender is never condensed; a remote recipient (`remoteId`, rooms #941) gets one envelope per delivery with its queue deadline, never a preface, batching delay, condensation or steer.
13
+ - What a peer is handed (`out`) and what it stands for (`originals`) differ once a delivery is condensed: the bus keeps both, registers `out` so `reply_to` resolves, puts the originals back on a `failed_safe` receipt and holds any other failure (a thrown `deliver`, a later `onFailed`, a `needs_review` receipt) as `needs_review` with its originals, for `ahub queue resolve`, and counts no attempt when the peer merely got busy while the delivery was prepared. A delivery with an `important` envelope, an invocation kind or a remote sender is never condensed; a remote recipient (`remoteId`, rooms #941) gets one envelope per delivery with its queue deadline, never a preface, batching delay, condensation or steer.
14
+ - Invocations (#445 C2, #986): an `invocation` or `invocation_result` (`isInvocationKind`) goes alone in `take` and `takeBudget`, is never condensed, and is never steered into: both steer sites and `steerQueued` ask `invocationTurnOpen` first. In `onMessage`, a peer's answer goes to `onAnswer` before `parseMarker` and before any publish, when it names the request (`inReplyTo` in the handed delivery) or names nothing while the invocation turn is open. A taken answer, or a throwing hook, publishes, stores and emits nothing.
15
+ - An invocation is never replayed through ordinary retry (#991). `retryDelivery` refuses a row that holds one. An uncertain or thrown delivery holds the peer (`recoveryHeldPeers`) and calls `onInvocationUncertain` instead of `failed()`. `failed()` allows at most one automatic retry, and only when `canRetryInvocation` returns true; unknown or a throw refuses.
16
+ - Add an envelope kind only to `KINDS` (`envelope.ts`); the journal, restart reader, deferred-hub check and held hints read that one list. Test room provenance with `remoteEnvelope(env)`, never `from.includes("@")`: a relayed room invocation arrives `from: hub` with `refs.invocationRemote` (#992).
14
17
  - A condensed digest is sent by `digest`, not `hub`: `replyParent()` skips only the hub's `presence` item, and a reply to a digest must keep the highest hop of what it replaced. On a `failed_safe` delivery or a `retry` resolution the bus puts back the originals, never the digest.
15
18
  - Limits admit what is sent: the envelope `newEnvelope` builds (a reply inherits its parent's sender, `digest` resolves to the originals, `capPriority` applies), never the raw `to` and priority. Admit first and build later, and implicit replies all count as broadcasts.
16
19
  - A reply is addressed, never broadcast. An adapter that answers a delivery passes `to: replyAudience(envs)`; anything else with an `inReplyTo` inherits that envelope's sender. Leaving `to` out or empty fans the message out to every peer. The control `send` takes a nonempty string `to` as that one peer and refuses any other non-array shape or bad item before publishing (#963); never turn a malformed `to` into a broadcast.
17
20
  - `digest` is not a peer: addressing a reply at the envelopes the peer was handed sends it nowhere once a delivery was condensed. The bus resolves `digest` back through `lastDelivery.originals`; anything else that reads a reply's `to` has to do the same.
18
- - A pull-only peer (a Claude session without channel pushes, Kimi in its own TUI, or agy) is never drained by a push; `Bus.pull` hands it its queue oldest first up to `PULL_BUDGET_BYTES` (64 KiB of `frame` text, #713; `takeBudget`), the first item always going so one huge envelope cannot stall it, and a poison one still alone. A push peer, and an attached channel session that is not pull-only, keep `take`'s `DIGEST_MAX`: never raise that for a pull. The daemon's `inbox` reply carries `waiting` and the plugin says `N more waiting: call hub_inbox again before you start work`. An attached channel session may also pull its own queue through `hub_inbox` while busy (#493); the same holds and journal completion apply. Drain and pull share `held()` (recovery, an uncertain delivery, any pause, the lifecycle `deliveryHeld` fence) and `remember()` (so `reply_to` resolves), and a drain rechecks `deliverable()` after condensing because such a session can take the peer meanwhile. A pull is journaled `completed` in the same transaction as the bus snapshot, never `accepted`; a failed one puts back only that peer's queue and preface and never reloads the snapshot.
21
+ - A pull-only peer (a Claude session without channel pushes, Kimi in its own TUI, or agy) is never drained by a push; `Bus.pull` hands it its queue oldest first up to `bus.pull_budget_bytes` (#489 H3; unset is `PULL_BUDGET_BYTES`, 64 KiB of `frame` text, #713; `takeBudget`), the first item always going so one huge envelope cannot stall it, and a poison one still alone. A push peer, and an attached channel session that is not pull-only, keep `take`'s `bus.digest_max` (at most `DIGEST_MAX`): never raise that for a pull. The daemon's `inbox` reply carries `waiting` and the plugin says `N more waiting: call hub_inbox again before you start work`. An attached channel session may also pull its own queue through `hub_inbox` while busy (#493); the same holds and journal completion apply. Drain and pull share `held()` (recovery, an uncertain delivery, any pause, the lifecycle `deliveryHeld` fence) and `remember()` (so `reply_to` resolves), and a drain rechecks `deliverable()` after condensing because such a session can take the peer meanwhile. A pull is journaled `completed` in the same transaction as the bus snapshot, never `accepted`; a failed one puts back only that peer's queue and preface and never reloads the snapshot.
19
22
  - Record exact task-offer reads only through `DeliveryJournal.completeDelivered` on an active-fenced adapter completion, correlated reply or actual pull, atomically with completion. Capture original offers before terminal pruning and record their ids, never condensed ids; accepted receipts, generic transitions, operator/hub dispositions and restored completed rows are not read evidence.
20
23
  - The `onImportantQueued` nudge for a pull-only peer (#398) is advisory: it never dequeues, steers, journals or settles, and a throw from it never stops an enqueue.
21
24
  - Merge recall and restored prefaces through `mergePrefaces`/`uniqueMemoryLines`; preserve distinct context and new notes, deduplicate memory identities, and keep colliding short prefixes distinct. Use `recallDisplayText`/`recallMemoryLines` before display caps: suppress only matching automatic approval outcomes, retain nonempty rejection details, preserve id headers and complete-word excerpts, and keep token trimming at line boundaries. Keep these changes display-only.
@@ -21,6 +21,7 @@
21
21
  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/adapters/definitions/native-recovery*.ts`, `src/adapters/definitions/native-policy-aux.ts`, `src/adapters/definitions/native-config-*.ts`, `src/adapters/definitions/native-cli-*.ts`, `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/hub/config-reload.ts`, `src/hub/config-apply.ts`, `src/hub/config-tier.ts`, `src/hub/config-edit.ts`, `src/hub/config-edit-host.ts`, `src/hub/claude-launch-proof.ts`, `src/hub/recovery-continuity.ts`, `src/cli/recovery-presentation-options.ts`, `src/cli/setup.ts`, `src/cli/upgrade*.ts`, `src/cli/terminal-recovery.ts`, `src/cli/recovery-package.ts`, `src/cli/facts-hook.ts`, `src/hub/usage.ts`, `src/hub/memory-profile.ts`, the memory code modules (`src/hub/memory-symbols.ts`, `src/hub/memory-code-overlay.ts`, `src/hub/memory-code-head.ts`, `src/hub/memory-stale-anchors.ts`, `src/cli/memory.ts`), the task brief host (`src/memory/brief.ts`, `src/hub/memory-plan-guard.ts`), `src/hub/native-hook-launch.ts`, `src/cli/output.ts`, `src/ui/index.html`, `src/hub/task-code-group.ts` (dashboard Code group), the agent drift sweep (`src/hub/agent-drift.ts`, `src/cli/agents.ts`) or the instance modules (`src/hub/instances.ts`, `src/hub/instance-config.ts`, `src/hub/instance-execution.ts`), or before adding a native peer.
22
22
 
23
23
  - 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).
24
+ - `hub_invoke` over the control WS (#445, #991): the daemon binds each admission to its socket and `rid` with an `AbortController` (`invocationRequests`, at most 64 per connection; a duplicate `rid` refuses). `request_cancel` and socket close abort it, and every check after an await confirms the socket is open and still maps that `rid` to the same controller before any effect. Clients go through `ControlClient.request`, whose `controlRequestTimeout` raises a `hub_invoke` wait to `INVOCATION_CONTROL_TIMEOUT_MS`; never call it with a shorter bound.
24
25
  - `peer_stop` is the console's or an admitted conductor's (`admitOperational(sock, "stop")`) and stops, through its adapter, only an owner whose adapter reports `hubOwned === true` and whose `form` is not `tui` (today headless ACP, Pi, local, print-reviewer and Claude implementer); a present `undefined` form refuses. 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.
25
26
  - Console review guidance (#711): `r` on a task in review opens `reviewChoice` (`src/cli/console-review.ts`, modelled on `modeChoice`) from the row or the open task detail, never a bare prefill; `a` and `c` prefill `review N approved|changes_requested ` with a guide line, a `changes_requested` line with no note is refused in `reduceConsole` before any effect, and the result of a review command is shown as a notice in panels mode (`reviewResult`). The console only prefills and runs `ahub review` as the person: never grant it extra authority, and keep a private task's title out of the choice line (`[pii]`). The CLI cause line (`agentShellCause`, `src/cli/identity.ts`) is printed for the `tools` identity and a person-only hub refusal only, never for the console identity, and never replaces the refusal.
26
27
  - Console changed-symbols block (#684 2b): `symbolFields` (`src/cli/console-symbols.ts`) adds `symbols` after `plan` for a task_show-shaped detail only, from the public `codeSymbols`; never from a raw `Task`, never for a private task, never from a stored receipt (one with `repositoryKey` reads as unknown: only the screened public receipt is shown), never as a second copy of the match rule (`planSymbolMatches` in `review-brief.ts`). Unknown is `unknown (no code facts)`, never an empty list.
@@ -33,7 +34,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
33
34
  - SDLC band timer (#481 S7): the daemon's band interval (`bandsEveryMs`, default 5 min) is the only caller of `tasks.proposeFromBands`. It skips while stopping or in recovery, when `tasks.bandsState()` is not ready or `bandsHeld` holds, and when `board.statusRevision()` with the band signature is unchanged since the last pass that ran; it records that key after the write. A failure is one sanitized `hub.log` line. Writer rules: `tasks.md`.
34
35
  - `ahub board` (#724): `renderBoard` prints one line per task only with a terminal width and without `--full`; `--full`, `ahub task show` and output without a width keep the full table and SDLC advice. `STAGE_CODES` (`src/cli/output.ts`) keys on blocker-text prefixes from `src/hub/sdlc.ts`: rewording a blocker there updates its code in the same PR. TITLE is cut, never wrapped; STAGE is never dropped (it falls back to the meter and the code); a PII row keeps its `(ahub task show N)` hint while 8 title columns remain.
35
36
  - CLI output default (#814): a command prints for a person unless `--json` is given. The recovery and upgrade commands follow it too (`src/cli/upgrade-output.ts` composes `planLines`, `renderPlan`, `renderOperation`, `renderBlocked` and `choiceInstruction`, so the non-interactive text never says what the screens would not): a pipe and `--dry-run` print the plan with blockers, reconnect-only and fresh-start peers in stdout, `--json` keeps the document and the stderr lines (#206), and `recoveryDisplay` takes a pipe's width from `COLUMNS`. Hub tool text (`task show`, the backlog and release commands) is parsed on the CLI side by `showToolText` and rendered by `src/cli/task-output.ts`; `--json` prints the text byte for byte, unparseable text prints as it came, and a private task is rendered as the stub the hub sent, never asked for more. A command that takes free text reads `--json`, `--full` and `--color=` through `splitPresentation` (`edges`): only a leading or trailing run is a flag, and a value flag's value never is, so a reason keeps its words. `test/cli-json-guard.test.ts` also scans every file in `src/cli/` for a print of a hub tool's text (`console.log(await taskOp(<op>))`, or `console.log(x)` where `x` is bound from one) and passes only when the print is inside a `--json` branch, the op is in the named `SENTENCE_OPS` (ops whose daemon case returns prose, each checked against the daemon), or it is named debt with a reason (`TEXT_DEBT` for JSON ops a later task converts, `CONDUCTOR_DEBT` for the conductor-role commands, `REFUSAL_FALLBACK` for the refusal sentence printed after `--json` and the table were handled); a new JSON-returning op printed bare fails it, and an entry that matches no print fails too. Convert a command by rendering it through `showToolText`, never by adding it to a list. A renderer is a pure function in `src/cli/` (`sdlc-output.ts` for the `sdlc` read commands) and the call site keeps its own visible `if (options.json) console.log(JSON.stringify(...))`, so `test/cli-json-guard.test.ts` can see the branch. Never print JSON by default: add the command's `--json` branch, or name the site in the guard's `DATA_FORMAT` (JSON by purpose: `export`, `sdlc export`, `bench export`, the PreToolUse hook reply) with its reason. The guard accepts a `--json` branch only when the condition PROVES the flag (`selectsJson`): `X.json` on an option holder (`options`, `display`, `present`), `args|flags.includes("--json")`, a known alias, `&&` when either side does, `||` only when both do, `=== true`. Anything else (`!x`, `=== false`, `|| other`, `?? x`, a comparison, an unknown name) is not a guard, so a new shape fails until it is written one of these ways. A renderer table never exceeds the terminal: `grid` (`sdlc-output.ts`) wraps cells, then falls back to one labelled block per row, and keeps every identifier whole. `NOT_YET_CONVERTED` is debt the later #814 tasks delete; an entry that matches no site fails the test, so the list only shrinks. `sdlc` drops `--json`, `--full` and `--color=` from its operands only for its read subcommands (`sdlcOperands`): the others keep their argv exactly. A hub tool's JSON text that does not parse prints as it came, never as an empty report.
36
- - Settings (#269): the dashboard's `setting`, `setting_undo`, `setting_preview` and `setting_reload` actions and the console-role `settings_get|set|undo|preview|reload` requests go through `settingsSet`, `settingsUndo`, `settingsPreview` (`settingsUndoPreview` for `undo: true`) and `reloadConfig` (`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 numeric operational tunable (#489) is a fixed row in `OPERATIONAL_NUMBERS` (`src/hub/operational-settings.ts`) plus its explicit `config-apply.ts` leaf (`operationalNumberCheck`), its `DEFAULT_CONFIG` default and a reader asked at each use, never registered from a file, request or environment variable; 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 (for `settings_set` and `settings_preview` of an `operationalSetting` or `operationalRosterSetting` key, also an admitted conductor), 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`.
37
+ - Settings (#269): the dashboard's `setting`, `setting_undo`, `setting_preview` and `setting_reload` actions and the console-role `settings_get|set|undo|preview|reload` requests go through `settingsSet`, `settingsUndo`, `settingsPreview` (`settingsUndoPreview` for `undo: true`) and `reloadConfig` (`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 numeric operational tunable (#489) is a fixed row in `OPERATIONAL_NUMBERS` (`src/hub/operational-settings.ts`) plus its explicit `config-apply.ts` leaf (`operationalNumberCheck`), its `DEFAULT_CONFIG` default and a reader asked at each use, or, for a row with `applies: "next use"` or `"restart"`, captured once by the consumer it configures (a new adapter for `adapter_timing.*`, a new model relay for `model_relay.*`, `Inference` at daemon start for `inference.*`, a console launch for `console.*` through one `settings_get` after its first draw and tail subscription, 3 s bound, defaults on failure); never retime or trim a running consumer's state with it, never registered from a file, request or environment variable; 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 (for `settings_set` and `settings_preview` of an `operationalSetting` or `operationalRosterSetting` key, also an admitted conductor), 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`.
37
38
  - Structural config edits (#689): `src/hub/config-tier.ts` is the one tier rule (editor, dashboard refusal and `structuralLeaf` in `authorizeConfigLeaf`), `src/hub/config-edit.ts` plans and writes (loaders first, revision checked twice, bounded backups, atomic), `src/hub/config-edit-host.ts` owns authority, the typed tier 3 confirmation, the `config_edit` event and the scoped publish. The daemon only supplies ports (`createConfigEditor` in `daemon.ts`); never validate, write or classify in `daemon.ts`. A new editable structural key is an entry in `config-tier.ts` and nothing else; a key that chooses what the hub runs or where secrets go is tier 3 and is never offered to the dashboard (the hub refuses it, the page also hides it). The audit event and every notice carry key paths and value hashes, never a value. `structural` reaches `authorizeConfigLeaf` only through `structuralScopes` for the one scoped reload. `sdlc.toml` is read once at start, so it joins `pendingRestart` through `allPendingRestart()`, never through `reloadConfig`. A new ahub settings subcommand that writes is classified `console` by default; keep it that way.
38
39
  - Live reload (#439): `reloadConfig` is the only publication path. Settings writes and undo, `ahub settings reload`, the dashboard reload and the debounced watcher of the four `CONFIG_SOURCE_FILES` (`src/hub/config-reload.ts`) all reach it; `prepareConfiguration` validates the complete candidate before anything is published, and recovery or shutdown holds it. A write or undo passes its own key as `scope` and publishes only that path, and `acknowledgeConfiguration` records only the accepted paths, so other edits stay pending. Every changed key, inside the registry or not, is decided by `authorizeConfigLeaf` (`src/hub/config-apply.ts`): a refused key keeps its accepted value and is reported by name while the others apply, `roster.*` applies only through its own scoped roster write, and a change of the settled planner or conductor holders (`roleHolders` over `resolveSettledRoles`) is guarded even when a role list shrinks. Outside a named write, only the console's `settings_reload` (authority `terminal`) adopts guarded file edits, by sending back the `confirmation` token bound to the candidate `fingerprint` and `configRevision`; changed bytes or accepted state refuse it, and a watcher, dashboard or settings session never confirms. An undo that names `key` and `undoDigest` writes only while `pendingUndoIdentity` still matches both. Keep config-source snapshots plain and cloneable; classify dynamic ids with validated running/source identity scopes, never suffix discovery or a builtin fallback. Compare restart-bound declarations with the running config on every reload, never a pending desired value accepted by an earlier reload; derived defaults stay bound to that running declaration. A `restart` key (`configApply`) keeps its running value and is listed in `pendingRestart`; reload notices and events carry key names and reasons, never values. The console settings editor (the Settings panel, or the Peers panel's `w` WIP shortcut, which opens only an existing `tasks.wip_limits.<peer>` row for a rostered peer listed in status `taskWip`) stays bound to the panel that opened it (`SettingsEdit.panel`) and arms a write only from a preview reply that matches that panel and its open edit's token, key and value; pasted text never confirms. Console Tasks backlog actions (`p`, `a`, `e`, `R`, `c`) only prefill person CLI commands (`backlog pull-ready|plan|return`, `task cancel`) with the row's immutable id and its planning or flow revision; `validateLockedRow` rechecks the row's state and revision at Enter and again at `y`, and no console path writes the board directly (#532).
39
40
  - Planner setting (#755): `planner` is one id or a list of one to eight distinct eligible ids, validated only by the `planner` row of `projectSettings` (eligible declared instances join its `values`) and `checkSettingValue`, in `loadConductorPolicy` and `validateRawConfig` alike. Every `resolveSettledRoles` call (`settlePlanner`, `roleHolders`) passes the same `plannerEligible` predicate built from that row, so reload guards compare holders the way the running map settles them.
@@ -54,7 +55,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
54
55
  - Adding a native peer requires testing the command emitted by the actual recovery driver, not only its inspection or launch helpers. Never let a generic non-Codex branch treat a new peer as Claude.
55
56
  - Cross-version recovery must use the authenticated source protocol for prepare/commit/abort and the target protocol after startup. Prove the transition against a real prior release before claiming compatibility.
56
57
  - Preserve `task_cancel` target-feature preflight before source holds, capture or commit. Reuse stored target features only after verifying the same retained root and digest; legacy receipts still capable of commit must obtain feature proof. Do not apply pre-commit compatibility checks to sources already past commit. Every prepare, including the lease renewal before a terminal close and after a resume, sends the operation's stored `targetFeatures` (#478).
57
- - A recovery target-feature floor (`task_cancel`, `peer_instances`, `role_stages` while a declared Claude-kind instance holds `implementer`, `task_planners` while a list/instance planner is configured or `Board.hasPlannerState()` finds a task/release binding or plan-review authorship marker, `release_trains` while a configured train is active or frozen, `reviewer_pool` while a fresh reviewer or `review.pool` is configured, `configured_acp` while the project declares `adapters`, `external_adapter_modules` while external module definitions are loaded, `external_adapter_instances` with `external_adapter_modules` and `peer_instances` while an external module-kind instance is declared, `conductor_recovery` while `Board.hasConductorManualState()` finds open conductor manual-assignment intent) lives in `incompatibleRecoveryTarget`, which every existing check site calls, and its name in `DAEMON_FEATURES`; never check a floor at one site only.
58
+ - A recovery target-feature floor (`task_cancel`, `invocation` while the invocation ledger has an open (`queued` or `delivered`) row, `peer_instances`, `role_stages` while a declared Claude-kind instance holds `implementer`, `task_planners` while a list/instance planner is configured or `Board.hasPlannerState()` finds a task/release binding or plan-review authorship marker, `release_trains` while a configured train is active or frozen, `reviewer_pool` while a fresh reviewer or `review.pool` is configured, `configured_acp` while the project declares `adapters`, `external_adapter_modules` while external module definitions are loaded, `external_adapter_instances` with `external_adapter_modules` and `peer_instances` while an external module-kind instance is declared, `conductor_recovery` while `Board.hasConductorManualState()` finds open conductor manual-assignment intent) lives in `incompatibleRecoveryTarget`, which every existing check site calls, and its name in `DAEMON_FEATURES`; never check a floor at one site only.
58
59
  - Defer hub notices only for an authenticated recovery `prepare` or `commit` whose target features include `recovery_deferred_hub`, or for a restored daemon. Otherwise `setRecoveryHold(true)` moves the deferred notices into queues before commit records expected integrity, and a bare `fenceRecovery` defers nothing. Abort and lease expiry release the bus hold before clearing the operation; if that release fails, keep the operation and let expiry retry on its 1 s timer (#478).
59
60
  - Resume a prepared `commitSent` source that is unavailable or database-busy inside the existing bounded shutdown poll; never re-send commit, re-prepare or close terminals again on that path. Require confirmed `stopped` before target startup, refuse a replacement instance, and retain the prepared receipt on timeout.
60
61
  - Keep restoration receipts explicitly tagged as pending, waiting with origin/binding, done with binding, or failed. Project waiting origins as `waiting:pending` and `waiting:failed`; preserve the chosen binding and origin across interruption instead of inferring completion from an object.
@@ -63,7 +64,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
63
64
  - Judge every recorded owner pid (hub manifest, registry claim, recovery runner, manager owner, launcher record) with `processLiveness` in `src/pi/process-signature.ts`, never a bare `process.kill(pid, 0)`: after a reboot a recorded pid can belong to another process, even one we may not signal, and the bare probe wedged recovery and `ahub up` (#226). A writer records `processSignature(process.pid)` beside its pid; a registry claim stores it as `<instance id> <signature>` because an older daemon's claim rewrites the pid but not that column. Records without a signature keep the bare-pid answer; unknown is never gone.
64
65
  - A restored daemon accepts a changed native session id (Kimi and local aside: they restart as fresh workers) in two ways only: the #64 rule for a Claude session that never persisted a transcript, and the operation-fenced `recovery-waivers.json`, which the coordinator writes wherever it accepts a session other than the planned one: a reconnect-only Claude (#206), an operator-chosen fresh session, the plan's `freshStart` of a Codex thread the hub saw start with no turn, or a zero-turn Claude it accepted (#215). Never waive on any other evidence. Re-preparation after an expired lease keeps every effect receipt, and `restored:<peer>` = `failed` never counts as restored. Judge a restoration launcher by its `terminal-recovery.json` record and process signature (and a `terminal_handle_stale` answer), never by the terminal's exit; an unreadable record is unknown, never "none".
65
66
  - A target that stopped after it started is restarted only through `recordRestart` (`src/cli/upgrade.ts`), recorded before the new one starts and at most once per project per resume: each done or waiting `restored:<peer>` binding becomes `retired:<peer>`, whose terminal is closed (`closedRetired:<peer>`) or found absent before that peer is relaunched, and is never revalidated against the new instance. Keep repaired-session provenance across every target restart, disabling automatic fresh/zero-turn replacement. A person may explicitly accept loss of a repaired Codex/Claude conversation only on typed positive missing transcript/rollout evidence and a disposal-time recheck; refuse live-test repaired overrides after read-only receipt classification and before runner/lock or coordinator mutation, fail closed on unclassifiable scope, and repeat in core before lock and after under-claim read, preserving ordinary #408 delegation; record the one-use grant and freshOverride evidence, and consume the grant on exact attachment. Pi stays excluded: positively missing accepted Pi files get stop-and-archive plus the normal project-scoped ahub pi command; bookmark retirement never falls back fresh in the same start. Clear the repair fence only for a done or waiting binding retired from that target; an uncertain creation without a durable binding keeps its fence. Launcher evidence is read for every instance in `restarts`, and a session attached without an id is unknown, never gone (#225).
66
- - After a managed terminal is created, persist its binding before waiting up to `RECONNECT_WAIT_MS` for authenticated attachment to the expected target instance, operation and conversation (#414). Resume waits on the saved binding without creating another terminal. Count only attached, non-offline session ids; when the chosen terminal has no current launch record, a confirmed-ended row for another handle or an id-less attachment retains the bounded wait after older-launcher checks. A late current record must match the saved terminal; unknown or live replacement evidence still blocks. Timeout keeps the receipt blocked and records only terminal/hub metadata; a last native connect attempt that was never recorded is unknown, never inferred from terminal readiness or offline state. Final identity and preservation verification stays strict. `createTerminal` re-reads the launch record and Orca binding of an idle replacement every `MAP_POLL_MS` until it maps to the original peer and session, a known identity or session contradicts it, or the deadline passes; only then does it report `ambiguous-create`. Missing identity alone is never a mapping (#599).
67
+ - After a managed terminal is created, persist its binding before waiting up to `recovery.reconnect_wait_s` (#489 H4, read by `readRecoveryWaits` at the start of the wait; unset 600 s) for authenticated attachment to the expected target instance, operation and conversation (#414). Resume waits on the saved binding without creating another terminal. Count only attached, non-offline session ids; when the chosen terminal has no current launch record, a confirmed-ended row for another handle or an id-less attachment retains the bounded wait after older-launcher checks. A late current record must match the saved terminal; unknown or live replacement evidence still blocks. Timeout keeps the receipt blocked and records only terminal/hub metadata; a last native connect attempt that was never recorded is unknown, never inferred from terminal readiness or offline state. Final identity and preservation verification stays strict. `createTerminal` re-reads the launch record and Orca binding of an idle replacement every `MAP_POLL_MS` until it maps to the original peer and session, a known identity or session contradicts it, or the deadline passes; only then does it report `ambiguous-create`. Missing identity alone is never a mapping (#599).
67
68
  - An operation receipt stays `schema: 1`. Only `abandoned()` writes 2, on an operation with a recorded `disposition` whose coordinator predates #215: in the same write as the disposition, or, for a disposition 0.12.20 recorded at 1, by the next command that holds the runner claim. `validateReceipt` accepts 2 only with a disposition (#227). Never write 2 on an operation that can still be resumed. Disposal clears `error` and `failure`, so each `stop-and-archive` audit entry keeps the operation's `error` at that moment as `cause` (#776): only text the receipt already held, never task or message text.
68
69
  - Four checks read another release's source text for an exact line: `coordinatorCurrent` (`export async function disposeRecovery` in `src/cli/upgrade.ts`), `targetReadsWaivers` (`export function readRecoveryWaivers` in `src/hub/restart.ts`), `coordinatorRestarts` (`export function recordRestart` in `src/cli/upgrade.ts`) and `supportsAction` (`export async function reattachRecovery` in `src/cli/upgrade.ts`). Renaming, moving or un-exporting one makes this release read as an older one: keep those lines until a capability list replaces the checks. The reattach CLI dispatch must use the same `supportsAction` check as its printed command; never redirect a fallback command back to a coordinator without that action. Recovery also reads a target's literal `OPTIONAL_PEERS` (`src/hub/peer-catalog.ts`, through `legacyTargetSupport`) and `START_MODE_PEERS` (`src/hub/start-mode.ts`, through `legacyHeadlessArgv`), declared in `src/adapters/definitions/native-recovery.ts`; keep both literal declarations until a published capability replaces them.
69
70
  - Publish terminal ownership only after the current native owner is proven through `terminal_owner` (#482); capture and start acknowledgement are pending evidence only. Preserve unrelated and invalid rows, scope current evidence to project/instance/peer, and recheck the same native generation after asynchronous Orca reads. Keep Claude's hook record pre-spawn. Compatibility and retention ceilings are described in Operations, controlled recovery.
@@ -80,7 +81,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
80
81
  - A headless Claude implementer's tools socket (#525-4) is admitted only with the launch id of that peer's current child (`ownedTools`) and a verified worktree root, and bound as `toolsOwner`: every request and every reply rechecks `currentImplementerTools` and fails closed. Its `task` ops are limited to `IMPLEMENTER_HUB_TOOL_NAMES` and run inside `childToolAuthority`, so `Tasks.requestCheck` refuses before an effect once the child changed. Its `inbox` refuses, before `bus.pull`, a batch holding a private item or one attributed to another task. Its person `permission_request` path is `classed: true`, offers allow-once and deny only, and denies on expiry, abort or a changed child. Automatic Bash permission uses only the fixed host-wrapper contract in `adapters.md`, including the explicit client protocol capability; a raw native allow is never an automatic grant. Its facts get `permission` only for the current child's launch id, session id and this instance.
81
82
 
82
83
  - 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.
83
- - 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.
84
+ - Recovery inspection uses the recovery control cap (`recovery.control_timeout_s`, 30 s unset, never lower) 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.
84
85
  - 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).
85
86
  - 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).
86
87
  - `openManager` keeps only the manager that answered `/open`: when the answering pid is not the process this open spawned, it stops its own spawn with SIGTERM, so a losing concurrent open never leaves an unowned manager (#584).
@@ -16,7 +16,7 @@ Scope: the hub-native local worker, its tools, path guard, denylist and seatbelt
16
16
  - git arguments never pass through `guardPath`; `gitArgsProblem` refuses absolute paths, `..`, `--no-index` and denylisted `rev:path` forms.
17
17
  - A worker turn builds its messages in a local array and joins the history only as a whole. Never push to `history` mid-turn: one tool call without its result poisons every later request.
18
18
  - After a tool with side effects ran, a failed turn is reported, never redelivered.
19
- - A turn with any remote envelope (`from` containing `@`, rooms #941) never joins `history` and never reaches `capture.observe`, like a PII turn, and every failure it reports (answer, delivery reason, log line) says only `remote turn failed`. Keep all three when you touch the turn's settlement or the tool loop.
19
+ - A turn with any remote envelope (`remoteEnvelope(env)`: `from` containing `@`, or a hub `invocation`/`invocation_result` carrying `refs.invocationRemote`; rooms #941, #992; never test `from` alone) never joins `history` and never reaches `capture.observe`, like a PII turn, and every failure it reports (answer, delivery reason, log line) says only `remote turn failed`. Keep all three when you touch the turn's settlement or the tool loop.
20
20
  - `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.
21
21
  - `Facts` (`src/hub/facts.ts`, #428) names a large file whose ctime is under `RACY_MS` by its git blob id: in `load()` and `observe()` only through `racyBlob()` (per-file `RACY_HASH_MAX_BYTES`, per-store `RACY_BURST_BYTES` per event-loop turn, memo cleared at `boundary()`), past which it keeps the stat-only stamp and makes no claim; `atHead()` keeps its own `HASHED_BYTES` bound, `tree()` reads large files by stat only, and `largeBlob()` caches only settled stamps.
22
22
  - 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). Every live `capture.observe` passes a host `sourceId` (`<session>.<turn>.<step>.<n>`, #752); a new host caller supplies one too. Capture stores `paths` only for `read`/`write`/`edit` and only project-relative ones: command argv is never path metadata.
@@ -11,5 +11,6 @@ Scope: the model relay and its journal, the hub's own inference and its slots, O
11
11
  - Shared inference slots must recover after a hub process dies, without evicting a live owner. Cancellation has to reach slot acquisition from the relay caller, not just exist in the helper signature.
12
12
  - Stay/switch (#197): `hub/auto` and local stage routes apply `stay_switch` only through `stayOrSwitch` (`src/models/route/stage.ts`), never at a call site; `shadow` must not change a routing decision (the recorded-decision golden test in `test/route-relay.test.ts`). No route summarizes, trims or rewrites the forwarded conversation: a tier that cannot fit is filtered out. A PII route's planner never sees the conversation size (no prefill bound) and its event omits `prefillTokens`.
13
13
  - A change to the model-routing keys of `templates/routing.toml` (`hub_routes`, `local`, `fixed_model`, `stay_switch`, the `[pi]` windows) carries a model-routing receipt (#637 part 2): `bun scripts/benchmarks/model-routing-gate.ts --receipt`, plus `--live` by hand when an alias or a judge moves. The corpus drives the real `HubRouteRuntime`; see "Model-routing promotion rule" in `docs/agent-notes/benchmarks.md`.
14
- - Load and cooldowns (#199) only reorder a `hub/auto` request's candidates (the route's own backend, then its fallback, as on main), in one ordering step in the relay's fetch handler; a fixed alias is never reordered. Never remove a candidate. A candidate goes first only while it is neither cooling nor `failing` (any failure, an error status or a failed stream included, for 30 s or until a success; separate from the cooldown streak). Load and pin moves are opt-in (`efficientWaitMs` absent: none) until #199 AC5 measures them. Their attempt is bounded only up to its response headers (`MOVE_FIRST_BYTE_MS`, after `omni.base()`); a cut there marks `failing` without counting toward a cooldown, and a stream that fails after the headers cannot fall back, so do not claim a move never fails a request MLX would have served. None happens while the request runs under an execution budget (`underBudget`, a read that admits nothing; a throw counts as a budget). The selector only names `prefer`; it never picks a moved backend itself. They never consider PII because the relay serves only Pi, which refuses PII work before any model call (`selectModel` in `daemon.ts`); a relay caller that can carry PII must not get a reorder toward DGX without a positive `onCampus()`. Count a cooldown failure only for a transport or startup failure outside a cooldown that was neither cancelled, refused admission, a busy MLX slot (`MlxBusyError`: load, never failure), nor a deadline the relay cut itself (`journalEntry.cut`: `"budget"` for the execution budget's, `"move"` for a moved attempt's first byte, which only marks `failing`); any HTTP answer, whatever its status, ends the streak and the cooldown (`answered`).
14
+ - Load and cooldowns (#199) only reorder a `hub/auto` request's candidates (the route's own backend, then its fallback, as on main), in one ordering step in the relay's fetch handler; a fixed alias is never reordered. Never remove a candidate. A candidate goes first only while it is neither cooling nor `failing` (any failure, an error status or a failed stream included, for `model_relay.cooldown_s` (default 30 s) or until a success; separate from the cooldown streak). Load and pin moves are opt-in (`efficientWaitMs` absent: none) until #199 AC5 measures them. Their attempt is bounded only up to its response headers (`MOVE_FIRST_BYTE_MS`, after `omni.base()`); a cut there marks `failing` without counting toward a cooldown, and a stream that fails after the headers cannot fall back, so do not claim a move never fails a request MLX would have served. None happens while the request runs under an execution budget (`underBudget`, a read that admits nothing; a throw counts as a budget). The selector only names `prefer`; it never picks a moved backend itself. They never consider PII because the relay serves only Pi, which refuses PII work before any model call (`selectModel` in `daemon.ts`); a relay caller that can carry PII must not get a reorder toward DGX without a positive `onCampus()`. Count a cooldown failure only for a transport or startup failure outside a cooldown that was neither cancelled, refused admission, a busy MLX slot (`MlxBusyError`: load, never failure), nor a deadline the relay cut itself (`journalEntry.cut`: `"budget"` for the execution budget's, `"move"` for a moved attempt's first byte, which only marks `failing`); any HTTP answer, whatever its status, ends the streak and the cooldown (`answered`).
15
+ - Relay cooldown policy and journal size (`model_relay.*`, read through `modelOperationalReaders`, #984) are passed only when a relay is constructed, and `inference.backoff_min` only when `Inference` is constructed. Both are restart-bound: a reload never rewrites a running relay's cooldowns or journal, or an existing backoff deadline.
15
16
  - MLX owner and generation identity read `ps lstart` with LC_ALL=C/TZ=UTC pinned (#177): unpinned, a record written under one environment reads as a foreign process under another, and the fail-closed paths would refuse the hub's own server.
@@ -67,7 +67,7 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
67
67
  - `Tasks.review` captures `reviewAuthority` once and rechecks it in `valid()` after every await, right before each write; a headless reviewer's verdict, and its `hub_task_show` (checked before and after the read), holds only while the same child runs for that task at its bound envelope head and review revision (`Tasks.reviewBindingCurrent`, host-only: `in_review`, same reviewer, non-PII, matching `reviewRequestRevision` and review head). Never cache the answer (#223, #533).
68
68
  - Review scope (#852, `src/hub/review-scope.ts`): a reviewer reads `full` (code, affected tests, CI) or `code` (code only). The default is data on the fixed reviewer variants (`AdapterDefinition.reviewer.scope`, `code` for the sandboxed codex and kimi variants, fixed-builtin-only like `instances.modelSelect`); a fresh-reviewer instance may override it with `review_scope` from `config.local.json` only (tier 3, never from a tracked file or the dashboard). `TasksDeps.reviewScope(peer)` is the one read. `sendReview` replaces the Hub-verified, check-result and no-check lines with `CODE_SCOPE_LINE` for a `code` reviewer, which requires every code defect as a structured finding with an id. Reserve findings for defects in the reviewed code, docs or tests; missing check/CI/test-run/PR-or-branch identity evidence goes in `unmet`, never `findings` (Amendment 2). The verdict rule is structural (amended AC3): a `code` reviewer's `changes_requested` is always recorded, never refused, and never classified by reading its unmet text; never add a text classifier (background: README). If its structured `findings` carry no item with disposition `open`, the history entry is `evidenceOnly: true`: it adds nothing to `rejections` (so it cannot escalate), records no failed outcome, contradicts no earlier approval, writes no recurring-finding candidate, is not a rework round in the SDLC metrics, still returns the task to its owner as usual, and sends the configured conductor one ids-only task notice (`evidenceOnlyNotice`; with no conductor, one console notice). A verdict with an open finding counts as today. A person's and a `full` reviewer's verdict are never evidence-only. A `code` reviewer's verdict history entry carries `reviewScope: "code"` and a `full` reviewer's carries nothing, so old rows are unchanged. Merge rule: a task approved by a `code` reviewer is merged by the conductor or the person only after exact-head CI is green and no review thread is unresolved, and the merge note names the CI run.
69
69
  - Keep structured review findings in verdict history, bound to the reviewed head. Validate bounded reports and person-only accepted dispositions before effects; recheck authority, privacy and head identity after awaits. A supplied `head` that differs from the task's resolved head (`findingHead`) is refused pre-effect, and the resolved head is part of the identity rechecked after the screen (#657). Key findings by `id` alone, never by pass (`findingsForHead`): a nit or open reclassification never discharges an Important, only an explicit disposition does. Omitted IDs never settle findings; carry open Important across heads until explicitly dispositioned, while nits stay head-local. Use the shared valid-head resolver for projections. Project text through publicView, and deduplicate recurring public pass/id pairs into backlog proposals through durable `Review finding key:` markers (a pair recurs only across separate work: tasks sharing a PR, a branch in one repository or a supersession count once, `workUnits`, #736; a task counts only where a review that is not withheld recorded the finding's id, whatever its pass, as open or accepted, never where it is only recorded fixed, #328) read only from hub-proposed `plan` cards (first history entry by `hub`), without editing instruction files.
70
- - Recurring unmet criteria (#514a, `src/hub/learning.ts`): grouping is pure (rows in, groups out); `Tasks.proposeCriterionCandidates` is the only caller and runs beside `proposeRecurringFindings`. It files a backlog card with ids and counts and the `profile_overrides` fragment as text, never applies it, and counts only rows that `candidateTextAllowed` passes (no PII). Its dedupe is the `Criterion candidate key:` line; keep it first in the detail.
70
+ - Recurring unmet criteria (#514a, `src/hub/learning.ts`): grouping is pure (rows in, groups out); `Tasks.proposeCriterionCandidates` is the only caller and runs beside `proposeRecurringFindings`. It files a backlog card with ids and counts and the `profile_overrides` fragment as text, never applies it, and counts only rows that `candidateTextAllowed` passes (no PII). Its dedupe is the `Criterion candidate key:` line; keep it first in the detail. `ahub learn` (#514b) is the person's verdict on those cards: `parseCandidateCard` reads only that first line (and the hub-authored `plan` card, and the title that must match it), the task count and the line are recomputed from the board (`learnEvidence`, `overridesDiff`), and the card closes through `Tasks.cancel` only after `settingsSet` wrote the one managed key `LEARN_CHECKLIST` (never before, never by a direct board write); every `Tasks.learn*` method refuses any `by` but the person, and a new learn op never joins `CONDUCTOR_TASK_OPS`. Rewording the card's key line or title changes the parser in the same change.
71
71
  - Plan conformance (#481 S2, `src/hub/plan-conformance.ts`): compare only literal repository-relative `plan.paths` entries (file or directory prefix, no glob) with complete PR file metadata; `completePlanInventory` requires a full head, `pathsComplete` and `typesComplete` (false on any rename or copy). Root, absolute-unverified or unsafe entries, a missing plan, a truncated inventory and a failed read are `unknown`, never covered; never read patches or the filesystem. An untouched planned path is advice; a changed path outside the plan is a departure, and `planConformanceBlockers` blocks enforce approval on `unknown` and on an unaccepted departure. Plan `proof` and `risks` (32 strings of 512 characters) cover no file. `Board` records `planRevision` only while `trackPlanRevisions` (mode not off), separate from backlog `planningRevision`.
72
72
  - Departure consent is written only by `Tasks.acceptPlanDeparture` for the person (`ahub sdlc accept-departure <task> <head> <reason...>`; no peer tool) on a currently known departure at the current PR head, never on `unknown`, and never for a PII task. It binds repo, PR, head, plan revision and fingerprint, the inventory, `planScopeIdentity` and this host's `departureGeneration`, so a restart or a change to any of them invalidates it (`validPlanDepartures`). Status and merge-queue reads never write the board: they only note observations and invalidations in memory (`observedPlans`, `invalidatedPlanDepartures`), which `planObservation()` attaches to the next owner or reviewer transition write. Off captures no observation or revision; a plan or refs change still invalidates an existing acceptance.
73
73
  - The review coverage gate (#520) pins `refs.commit` and the sdlc receipt and rechecks them in `valid()`; an unavailable brief is never an empty one (enforce refuses, advise notes). Read brief attributes and rules at the merge base (`--attr-source`, `check-attr --source`, `git show <base>:`), never from the head or the checkout.
@@ -90,6 +90,7 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
90
90
  - Planner walk and reviewer independence (#525-3): `plannerWalk` and `implementers` in `src/hub/routing.ts` are pure; the planner is chosen only inside `assign()` (`planners`, `namedPlanner`, `keptPlanner` in, `Assignment.planner`/`plannerReason` out), and `Tasks` never picks one outside it. `Tasks.weights()` is the one place `planners` is built (rostered holders of `d.roles`, sorted), so every `assign()` path and `explain` agree. Pass a bound task as `keptPlanner` to that same walk: its explanation names the binding as `kept`; drafts and legacy null bindings still rotate, and explanation never writes a binding or history. Rotation is `task.id mod n` over every holder and is never renumbered by availability; a named planner is exact and never replaced. I-2 (`implemented this task`) lives in `blocked()` for the reviewer role as its last capability/independence check before queue admission, so the legacy, named and pool paths share it and it only ever removes an otherwise eligible candidate; when I-2 removed every otherwise eligible reviewer, the person reviews with `reviewBlocked`, never an implementer.
91
91
  - Owned implementer child (#747): `propose`, `acceptReviewed`, `done` (through `complete()`), `recordSdlc`, `review` (in `valid()`), `correct` and `remember` take `requestCheck(by)` (`TasksDeps.requestAuthority`) at entry and call it after every await, before the next write; a new await on those paths adds a call, and its refusal stays pre-effect. The child's tool surface is the fixed `IMPLEMENTER_HUB_TOOL_NAMES` (`src/hub/hub-tools.ts`); never widen it from project roles.
92
92
  - Shell branch binding (#845, `Tasks.bindShellBranch`): the daemon's host shell wrapper path is its only caller, and it is the only writer of the hub's `shell branch bound` entry (the branch and a sha256 of the worktree root, setting `refs.branch` in the same write). It binds only for the current owner of an `in_progress` or `changes_requested` non-PII task in this project's repository, outside a private turn, whose verified execution root (`TasksDeps.executionRoot`) is not the project root and is the binding's worktree. It refuses a branch that differs from an earlier binding or from `refs.branch`, or that another open task in the same repository holds, and rechecks `current()`, the private turn and the flow revision right before the write. Every failure returns `false` (no grant), never a throw. It records host-observed evidence, never a branch a tool call claims, and grants no execution by itself. The entry is in `QUIET_EVENTS`: it neither moves nor settles the task, so it never makes a running check's result late, supersedes an assignment or counts as a move for review admission (#880).
93
+ - Invocation audit (#991, `Tasks.recordInvocation`): the daemon's invocation admission and `finishInvocation` are its only callers; no hub tool reaches it. The `invocation` history entry carries host-owned metadata only (`InvocationHistory`: ids, caller, target, capability, digest, class, approval, `argsSha256`, outcome), never arguments or results, because `publicView` copies it unscreened. At `queued` it rechecks task scope (non-PII task and turn, open and not backlog, caller is owner or reviewer), and a failed audit refuses the invocation before it runs; never catch that and proceed. The entry is in `QUIET_EVENTS`: it neither moves nor settles the task.
93
94
  - Deliverables (#509a, `src/hub/deliverables.ts`, `src/hub/deliverable-files.ts`): the hub hashes each file itself under the owner's verified execution root (`TasksDeps.executionRoot`, the daemon's `ownerWriteRoot`; an unproven root refuses); never accept a caller's hash, size or unknown field. Vet and hash before any board write and refuse pre-effect; a `done (checking)` carries its records to `complete()`, never a later read. An approval rereads the delivered files: a reviewer's verdict is refused before any write unless every file is `unchanged`, and only the person approves moved files, which are rehashed and named in the note. A file the hub cannot read or vouch for is `unknown`, never `unchanged`. Peer-facing records go through `publicDeliverables`: for a PII task or a `pii.paths` file, id, hash and size only.
94
95
 
95
96
  - Task/release planners (#525-2): keep nullable `planner` columns additive, with no backfill. Bind new proposals and null-planner pulls only from `assign()`; install the fixed synchronous `Board.proposalPlanner` host port so proposal, split-child and generated-card bindings enter the original insert transaction before notification. Preserve explicit null callers and existing rows. Preserve existing bindings on owner moves. `Tasks.taskPlanner` keeps a bound rostered settled holder even while offline/paused; a missing, removed or off-roster holder falls back to legacy holders. Every task/release planner effect and its post-await validation uses that fresh scope; project-wide reads remain holder-wide. Naming a planner is person-only, refused for closed or PII work, admits only the holder `assign()` returns for `namedPlanner`, and records only `planner changed`, never an owner delivery event. Split children intentionally run their own pure rotation and may bind a different planner from their parent; do not inherit the parent binding. A removed/off-roster binding is preserved as legacy metadata on pull without recording a false `kept` reason.
@@ -18,9 +18,9 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
18
18
  - 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).
19
19
  - A test of turn or notice behavior that is not about the snapshot budget yields an I/O turn (`await new Promise(r => setImmediate(r))`) between captures: captures in one synchronous burst share the 250 ms budget and may correctly record unknown evidence. A daemon test whose assertion needs a turn's snapshot or end evidence passes `snapshotClock: () => 0` (`DaemonOptions.snapshotClock`, #661) so a slow runner cannot spend that budget; never lengthen the budget instead (#550). A wait on concurrent-edit evidence correlates the expected turn ids and paths (#617).
20
20
  - Holding the snapshot budget clock (`snapshotClock`, #661) holds the deadline and, since #554, the git child's wall-clock timeout (the 10 s ceiling instead of the remaining budget). The rule lives in the pure `childTimeoutMs` (`src/hub/snapshots.ts`: production keeps min(10000, remaining), only a budget carrying `now` gets the ceiling). Reproduce a slow git with a deliberately slow `git` first on PATH (`slowGit` in `test/snapshots.test.ts`), never with host-wide load, and never run a high-parallel loop while another gate shares the dgx02 host.
21
- - The macOS CI leg is four shards (#548): `check (macos-latest) shard i/4` runs `AHUB_CHECK_SHARD=i/4 scripts/check.sh` (`bun test --shard --timings=scripts/test-timings.json`), and the job named `check (macos-latest)` is only the aggregator over them; `scripts/ci-gates.mjs`, branch protection and the reuse proof read that name, so never rename it or turn it back into a matrix job without reading them. `gateJob()` in `scripts/ci-gates.mjs` counts the shard names (`check (macos-latest) shard i/N`) as gate jobs, so a run with Linux and the seeded shards done and only macOS shards running reads as a launched full gate and not as unknown scope (`test/ci-reuse.test.ts` pins every mid-run window). A new test file needs no timing entry (it gets the median), but a shard that grows past about 15 minutes means the timings are stale: regenerate them with `scripts/make-test-timings.ts`. A shard starts the suite cold, so `scripts/check.sh` execs the system git once (`AHUB_CHECK_WARM_GIT`, default `/usr/bin/git`) before the tests, because tests that give it `PATH=/usr/bin:/bin` and a 1000 ms timeout (`read-only-shell-proof`, `task-shell-default`) are killed by a cold first exec; a failing or absent warm-up never fails the shard. `check.sh` unsets `AHUB_CHECK_SHARD` after building the test arguments, so the runner and every scratch `check.sh` a test spawns (`test/check-lock.test.ts` copies `process.env`) see an unsharded environment; keep that unset. A test must not depend on running in the same process as another file (shards split files across machines), and anything that has to happen once per gate belongs behind the shard-1 condition in `scripts/check.sh`, which `test/macos-shard.test.ts` pins.
21
+ - Both OS CI legs use four shards (#548): `check (ubuntu-latest) shard i/4` and `check (macos-latest) shard i/4` runs `AHUB_CHECK_SHARD=i/4 scripts/check.sh` (`bun test --shard --timings=scripts/test-timings.json`), and each stable job name `check (ubuntu-latest)` / `check (macos-latest)` is the aggregator over its OS shards; `scripts/ci-gates.mjs`, branch protection and the reuse proof read that name, so never rename it or turn it back into a matrix job without reading them. `gateJob()` in `scripts/ci-gates.mjs` counts the shard names (`check (ubuntu-latest) shard i/N` and `check (macos-latest) shard i/N`) as gate jobs, so a run with either OS and the seeded shards done and only the other OS shards running reads as a launched full gate and not as unknown scope (`test/ci-reuse.test.ts` pins every mid-run window). A new test file needs no timing entry (it gets the median), but a shard that grows past about 15 minutes means the timings are stale: regenerate them with `scripts/make-test-timings.ts`. A shard starts the suite cold, so `scripts/check.sh` execs the system git once (`AHUB_CHECK_WARM_GIT`, default `/usr/bin/git`) before the tests, because tests that give it `PATH=/usr/bin:/bin` and a 1000 ms timeout (`read-only-shell-proof`, `task-shell-default`) are killed by a cold first exec; a failing or absent warm-up never fails the shard. `check.sh` unsets `AHUB_CHECK_SHARD` after building the test arguments, so the runner and every scratch `check.sh` a test spawns (`test/check-lock.test.ts` copies `process.env`) see an unsharded environment; keep that unset. A test must not depend on running in the same process as another file (shards split files across machines), and anything that has to happen once per gate belongs behind the shard-1 condition in `scripts/check.sh`, which `test/macos-shard.test.ts` pins.
22
22
  - A test that proves a daemon answers while work is still running asserts ordering (the answer arrived before the producer finished), never an elapsed wall-clock bound: the daemon promises no millisecond figure for `status`, so a bound measures the runner, not the product (#531); a request timeout in such a test is a hang guard only.
23
- - A tight wall-clock bound in a test must be sized for the slowest CI runner, or replaced by a barrier (#559). Hosted macOS shards run each test about 1.5 times slower than the old single leg (and git-heavy tests about 7 times), so three unchanged tests tipped over once each after #954: `test/path-policy.test.ts` (the #541 hardlink loops took 10.6 s idle for three layouts in one test and 27 s on a shard, past the 20 s limit: the loops are now five parts of 1000 iterations per layout, 5000 per layout kept, about 0.8 s per part), `test/code-facts.test.ts` (a 400 ms deadline raced the fake CLI writing its child marker, which takes about 0.3 s idle and 1.4 s at 30-way load: the test now holds the deadline timers by the configured value, waits for the marker and fires the analysis's own deadline callback, a barrier with the real deadline code; because the held deadlines never fire by themselves, its `finally` must `close()` the reader and await the analysis before the fixture is removed, and a second test pins that teardown) and `test/smoke-pty.test.ts` (the macOS PTY bridge process sometimes never gets past `Bun.spawn` with a `terminal` under load: 13 of 60 at 12-way parallel on a Mac, 0 of 60 at 4-way, never for a plain no-op bun script, and a longer timeout cannot help a process that never starts, so a run that timed out with no output is run once more in the tests whose property is not the timeout, after asserting the first attempt's owned process group was confirmed stopped; that is a bounded second attempt, `runStarted`, which only lowers the odds (two hangs in a row still fail) and was measured on local macOS only, and the real fix is in the bridge or in Bun). When a test needs a bound, measure the slow tail under 30-way local load, size the bound from it with the number in a comment, and prefer a readiness signal (a marker file the fake writes before it hangs, with the deadline fired after it) over a shorter deadline or a longer margin: a margin still depends on the runner's speed, a barrier does not.
23
+ - A tight wall-clock bound in a test must be sized for the slowest CI runner, or replaced by a barrier (#559). Hosted macOS shards run each test about 1.5 times slower than the old single leg (and git-heavy tests about 7 times), so three unchanged tests tipped over once each after #954: `test/path-policy.test.ts` (the #541 hardlink loops took 10.6 s idle for three layouts in one test and 27 s on a shard, past the 20 s limit: the loops are now five parts of 1000 iterations per layout, 5000 per layout kept, about 0.8 s per part), `test/code-facts.test.ts` (a 400 ms deadline raced the fake CLI writing its child marker, which takes about 0.3 s idle and 1.4 s at 30-way load: the test now holds the deadline timers by the configured value, waits for the marker and fires the analysis's own deadline callback, a barrier with the real deadline code; because the held deadlines never fire by themselves, its `finally` must `close()` the reader and await the analysis before the fixture is removed, and a second test pins that teardown) and `test/smoke-pty.test.ts` (the macOS PTY bridge hung inside `Bun.spawn` with a `terminal` when its child wrote and exited before the call returned: Bun up to 1.4.3, oven-sh/bun#45300, fixed in 1.4.4; 26 of 96 bare-Bun runs at 12-way parallel, none of 180 on 1.4.4. A longer timeout cannot help a process that never starts and a retry only lowers the odds, so the bridge starts its child behind a gate it opens after `Bun.spawn` returns and the retry helper is gone (#560); the bridge also stops its child when its caller dies, and a test names a hung bridge with `runPtyWith` instead of relying on load). When a test needs a bound, measure the slow tail under 30-way local load, size the bound from it with the number in a comment, and prefer a readiness signal (a marker file the fake writes before it hangs, with the deadline fired after it) over a shorter deadline or a longer margin: a margin still depends on the runner's speed, a barrier does not.
24
24
  - 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.
25
25
  - Run `bun scripts/seeded-check.ts` before reporting the seeded-guard gate complete. After changing seeded guard logic, run its affected seeds even when search-anchor counts are unchanged. Put asynchronous calls inside `resolves`/`rejects` expectations so a mutated refusal fails a named assertion. 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. A change that moves or rewrites a line a `scripts/seeds.json` case searches for updates that case's search and replacement in the same PR (seed rot fails the gate), keeping its mutant effect and named test. Never run the full seed runner from a unit test or mutate the source checkout; inspect preserved failed fixtures before removing them. CI runs it as four sequential Linux shards (`--shard i/4`) beside the ordinary check, separately from `scripts/check.sh`; the required `seeded guards` job passes only when every shard passed, or when the decision job reused a verified identical-tree full gate (`admission == 'reuse'`). Keep that job name: branch protection and `scripts/ci-gates.mjs` read it (#457). On seed rot the checker names the anchor's symbol at a base commit and where it is now (`seedRotContext`): `--base <commit>` sets that base (CI passes the PR base from a `fetch-depth: 0` checkout; keep both), and without it the base is `merge-base origin/main HEAD`. The context is evidence only: it never relaxes the one exact match, and an unavailable base says so instead of guessing.
26
26
  - `scripts/lib/ts-ast.ts` is the one TypeScript AST helper for scripts and source-scanning tests (`parseSource`, `callSites`, `symbolSpans`, `enclosingSymbol`, `findSymbol`; inline HTML scripts keep their offsets). It gives lexical locations only, never resolved call edges, and stays dev tooling: never import it from `src/`.
package/docs/dogfood.md CHANGED
@@ -33,7 +33,7 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
33
33
  | Same-family reviewer ban, `review.same_family` (#820) | `never` in this repository's `config.local.json` after the release and a hub restart: a Codex-owned task is never assigned to `codex-review`; the planner (claude) reviews, else the person | board history: tasks whose reviewer family equals the owner's (target 0), `ahub route explain <id>` showing the rule, reviews that went to the person because no planner was eligible |
34
34
  | Per-task and release planners, I-4 (#525-2) | off until this change is released and the running hub is upgraded; this PR enables no new project planner configuration | task/release planner bindings and selection reasons; wrong-planner pre-effect refusals; independent plan-review requests and `selfReview` receipts; queued notices rerouted after a planner change; `task_planners` recovery refusals |
35
35
  | Acceptance criteria ids in review (#507b) | on after the release: a task with a recorded `criteria` checklist lists its ids in the review request and `hub_review` unmet items that start with an id are recorded as `unmetCriteria`; tasks here rarely record a checklist, so most reviews are unchanged | the `Acceptance criteria` block in a review request and `unmetCriteria` in `ahub task show` history for tasks with a checklist; the share of changes-requested verdicts whose unmet items name an id; unmet items that name an id the checklist does not hold (a typo) |
36
- | Recurring unmet criteria candidates (#514a) | on after the release: three tasks (at least two separate pieces of work) of one profile and class whose reviews name the same criterion id unmet in 30 days file one backlog card with the `profile_overrides.review_checklist` line as text; the hub applies nothing. Tasks here rarely record a checklist, so cards will be rare. | cards filed and the share a person accepts into their checklist, from the backlog history |
36
+ | Recurring unmet criteria candidates (#514a) | on after the release: three tasks (at least two separate pieces of work) of one profile and class whose reviews name the same criterion id unmet in 30 days file one backlog card with the `profile_overrides.review_checklist` line as text; nothing applies it on its own; `ahub learn list|accept|reject` (#514b, person only) is the verdict: accept writes the one line to `config.local.json`, reject closes the card. Tasks here rarely record a checklist, so cards will be rare. | cards filed, and accepted versus rejected, read with `ahub learn list --all` |
37
37
  | Planner rulings in review (#835) | source/fixture coverage; active after release and hub upgrade | post-ownership planner notes in review requests and review-only views, 5 x 1000 bounds, withheld/PII exclusions, builder summary still withheld and departure accepted:false |
38
38
  | Deliverable records (#509a) | on after the release: `deliverables` on `hub_task_done` and `hub_sdlc_record`, hashed by the hub in this repository's folder; most tasks here are code with a PR and name none, so only document-shaped work (a report, a spec draft) uses it | the `Deliverables` block in a review request and `deliverables` in `ahub task show`; approvals refused with `deliverable changed since it was delivered`; records with `state: unknown` (a file the hub could not vouch for); claimed-hash refusals as a measure of agents trying to supply a hash |
39
39
  | `ahub deliverables` and the dashboard badge (#509b) | on after the release: `ahub deliverables [--check]` on this repository's board; only tasks that named deliverables appear, so it lists document-shaped work only | the person runs `ahub deliverables --check` after a document task is approved and the next time its file is edited: a `changed since review` row names the edit; `Deliverables n` chips on cards; `not measured now` in the sheet means nobody ran `--check` since the daemon started |
@@ -106,7 +106,12 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
106
106
  | Quota budgets and pauses | on | pauses, resets, time paused (#683) |
107
107
  | Token usage per peer and task | `ahub report` and, from the roll-up in `hub.db`, `ahub usage [--by peer\|task\|issue\|pr\|release\|day\|model]`, `usage limits` and `usage total` (#683 U2); console tab `7` (U3) and dashboard Usage view (U4) shipped | tokens per merged PR |
108
108
  | Operational settings mechanism (#489 H1) | available after release; this PR does not change this hub's running configuration; planner FYI interval defaults to the existing 10 minutes | shared descriptor/schema/UI/apply parity, independent legacy-default pins, real planner-notice boundary and settings inherit/undo/reload fixtures; native console/dashboard editing remains unverified |
109
+ | Recovery and upgrade wait settings (#489 H4, task 445) | available after release; this PR does not change this hub's running configuration: the five `recovery.*` settings default to the waits they replace (managed attach 600 s, unmanaged reconnect 90 s, control request 30 s, shutdown 30 s, terminal idle 120 s) and nothing is set in this repository's config | every default equals its old constant (pinned with independent literals), every person-visible wait refuses a value below what shipped, a bad or unreadable file is the default and never an error inside a recovery, the coordinator reads both layers and the file at each wait, and inherit and undo go through the local file; the waits' use inside the real recovery driver (a configured value changing an actual attachment, shutdown or control wait) is covered only as far as the tests in this PR reach, and no real upgrade was run with a configured value |
109
110
  | Tasks and routing operational settings (#489 H2, task 441) | available after release; this PR does not change this hub's running configuration: the six `tasks.*` settings default to the constants they replace (demotion threshold 1.5, half-life, contradiction window, re-screen cap 10, split minimum 5 observations, split failure share 0.3), and nothing is set in this repository's `config.local.json` | each setting's default equals its old constant (pinned independently of the descriptor), each reader is called by the code that used the constant, `ahub settings set`, inherit and undo for `tasks.demote_at`, and a reload that reaches the next demotion, re-screen and split read; 29 hand-made mutants, none surviving; inherit and undo are not yet looped over all six keys, and no live `ahub settings set tasks.*` against a running hub |
111
+ | Bus and delivery operational settings (#489 H3, task 444) | available after release; no running hub configuration is changed here; five defaults retain the previous retry/digest/pull/note/history constants | default pins, bounded H1 schema/UI/apply parity, next-operation runtime traces and real-daemon write/inherit/undo fixtures for all five keys; live native console/dashboard editing and production delivery performance remain unverified |
112
+ | Adapter timing settings (#489 H5, task 446) | available after release; no running hub configuration is changed here; defaults remain ACP tool ceiling 30 min, activity batch 50 ms and Jcode steer 5 s | independent legacy pins, bounds/schema/UI/apply checks, fake-based watchdog/batching/absolute-deadline behavior and real-daemon next-launch snapshots/inherit/undo; native/account timing and live console/dashboard editing remain unverified |
113
+ | Model relay and inference settings (#489 H6, task 447) | available after release; no running hub configuration changed here; legacy defaults retained and all seven rows require daemon restart | default pins, schema/UI/apply parity, fake-clock cooldown/backoff and local fake-server journal coverage; configured native/model calls and live console/dashboard editing remain unverified |
114
+ | UI and console settings (#489 H7, task 448) | available after release; no running console or hub configuration changed here; defaults retained | independent default pins, bounds and config/inherit/undo, startup snapshot/fallback, poll seam, notice/history, cold-start counted trimming and permission/close handling; native interactive console editing remains unverified |
110
115
  | Context gate | off: `context.gate = 0` | context-pressure events |
111
116
  | Context headroom admission (#802) | off by default: `context.max_assignment_used = 0`; not enabled here by this PR | `ahub route explain` known/unknown context decisions and offered owners; pure/session-freshness, real Tasks/manual override and machine-local config regressions; native live admission remains unverified |
112
117
  | Always-ask action classes (#636) | on after release for managed Claude forms, hub-owned ACP in ask mode, local/Pi tools and received Codex command approvals; Codex native bypass stays not enforceable | classed permission requests (a `gh api graphql` query is a read, a mutation, `gh run` or `gh workflow` write and every command with a shell expansion in a classed position (`git push $B`, `rm -rf "$DIR"`) is asked about: #756, #762; `test/action-class.test.ts` holds the table), full local/Pi command visibility and denied effects for incomplete native views; actionPolicyInfo current-child enforceability, remaining class budgets and weakening attention; Codex native no-request and Kimi non-ask ceilings are explicit |
@@ -125,11 +130,14 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
125
130
  | Feature | State here | Measured by |
126
131
  | --- | --- | --- |
127
132
  | Screened room chat and person inbox (#446 X4/X5/X5b) | default off here; requires person enrollment/exposure; console panel 8 and dashboard Agents remain metadata-only; person CLI alone can show one inbox body; real two-machine/account leg remains unverified | signed completed (admitted) versus immutable completed (inbox); once-only inbound count, body-free metadata/list JSON, framed/control-stripped person show, done/discard/expiry purge and fixed stored-byte cap/age refusals, post-ack retries; 1,100-row complete-list ceiling; no broadcast/roles/board/body telemetry, key/storage creation from status or mixed-version success claim; existing restart/binding/disposition regressions |
133
+ | Capability discovery (#445 C1) | off here until a person shares an entry: nothing is listed without `invocation.share` in `.agenthub/config.local.json`; Kimi 2.3.2, Qwen 0.26.1 and Codex 0.166.0 list, every other version lists nothing; the live listing under a real login and a Codex 0.166.0 install remain unverified (the C0 probes used fixture homes); invoking is C2 | `ahub capabilities --json` per-peer entry counts and share states; `hub_capabilities` calls (the `capabilities_list` and tool lines in the hub log) against the count of shared entries; a peer on an unprobed version showing zero entries is the pin working, not a fault |
134
+ | Capability invocations (#445 C2/C3) | off here until a person shares a capability; read needs no approval, write/external require task scope and allow-once approval for each call; unattended refuses effects. Stand-in round trips and approval/audit/hold tests are the verification target; live Kimi/Qwen/Codex receipts remain C4 | `invocation` events and rows by outcome (completed, refused, failed, expired, timeout, needs_review), approval ids and task audit entries, result bytes, time from queue to settle; a read that times out against a live peer is the signal to look at that peer, not at the hub |
135
+ | Cross-hub read invocations (#446 X6) | off until both persons expose peers and independently pin the exact capability/digest/read class; discovery is manual; two-daemon broker/crypto fixtures are the verification target, live X7 receipts remain unverified | source/target invocation ids, request admission versus capability result and result transport outcomes; needs_review counts and no duplicate native dispatch |
128
136
  | Dogfood scorecard (#687) | on after release; read-only weekly receipt | `ahub report --dogfood --since 7d --json`; bounded evidence and explicit unknowns |
129
137
  | Claim verdicts and fact_check provenance (#901) | source/fixture coverage; the live research pilot (a person entering verdicts with `ahub task claim`) remains unverified | `ahub report --dogfood` research rows: `claims.reachedPersonVerdict`, per-slot `personVerdict`/`modelLabel`/`agreement`, `factCheck.selection`/`passed`/`failed`/`missingKeysPerRun`; null until a verdict or a keyed check is recorded |
130
138
  | Dirty-submodule notices (#798) | source/fixture coverage; the live writing/general pilot (a workspace with 26 submodules) remains unverified | `ahub turns` shows `<path> (changed, contents unobserved)` for a turn that only edited a submodule; the D1 notice at `hub_task_propose` and `hub_task_accept`; `ahub undo` names what it cannot restore; a project without `.gitmodules` makes the same git calls per turn (counting-shim test) |
131
139
  | Profile scorecard (#799) | source/fixture coverage; live writing/general/research pilot remains unverified | ahub report --dogfood profile rows; pure and reader dev byte goldens, private-text exclusion, research declarations/unknown verdicts and per-source unknown tests; anonymous weekly receipt templates |
132
- | Domain packs (#318, #513a) | off here: this repository has no pack in `.agenthub/roles`, so `ahub init --profile <pack>` is not used on it; live adoption in another project is unverified | the `ahub init --profile <pack>` preview and apply, the pack validator and the shipped-pack catalogue, covered by test/packs.test.ts (40 mutants killed); adoption would show as a `.pack.json` under `.agenthub/roles` and a `profile` in `.agenthub/config.json` of the adopting project, read from `ahub init --profile` runs. Runnable checks stay display-only (#513b needs its own reviewed runner) |
140
+ | Domain packs (#318/#322, #513) | off here: this repository has no pack in `.agenthub/roles`, so `ahub init --profile <pack>` is not used on it; live adoption in another project is unverified | the `ahub init --profile <pack>` preview and apply, the pack validator and the shipped-pack catalogue, covered by test/packs.test.ts (40 mutants killed); adoption would show as a `.pack.json` under `.agenthub/roles` and a `profile` in `.agenthub/config.json` of the adopting project, read from `ahub init --profile` runs. The writing-pack, analysis and ops packs load with shipped roles; test/domain-packs.test.ts executes all three standalone checks on synthetic passing/refusing fixtures. Their notes remain advisory and no check is auto-selected; live domain-workflow use remains unverified |
133
141
  | Agent-shell read-only commands (#812 T1) | source/fixture coverage; active after release | the kept Allowed forms succeed from an agent shell and the demoted forms are refused with the console line (identity tests plus the live table in the PR body) |
134
142
  | Public agent CLI reads (#812 T1b) | source/fixture coverage; active after release | ordinary tools and agent-shell read success, private/secret projection tests, mutating-neighbour and planner-only refusals |
135
143
  | Conductor operational CLI (#812 T2, #899, #933) | source/fixture coverage; active after release; `ahub doctor --orphans --kill` from the conductor's shell prints the stopped pids for a person and the hub's reply with `--json`; #933 adds board import, intake approve, schedule run/pause/resume, research backfill, bench run, source add, goal link and `tasks.escalate_after`, and lets any agent cancel, label or re-role its own task and list the deliverables it may see | peer-attributed effect admissions, authority/domain-error separation, delayed-handshake and incomplete-output CLI tests, cached privacy projections and revocation checks; `test/conductor-cli.test.ts` holds the orphan cleanup's readable default and `--json` byte equality; `test/console-class-933.test.ts` holds one admission row per #933 form, the operational refusal for another agent, own-task cancel and role, and the screened deliverables list; metric: `agent_cli` rows by label in `events.jsonl` and the person-only refusals that remain |