@staix/agent-hub 0.18.0 → 0.19.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 (73) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +4 -4
  3. package/docs/agent-notes/README.md +6 -0
  4. package/docs/agent-notes/adapters.md +10 -8
  5. package/docs/agent-notes/daemon.md +5 -2
  6. package/docs/agent-notes/local-worker.md +2 -1
  7. package/docs/agent-notes/tasks.md +4 -1
  8. package/docs/agent-notes/tests.md +6 -3
  9. package/docs/dogfood.md +9 -3
  10. package/docs/memory.md +1 -1
  11. package/docs/operations.md +66 -28
  12. package/docs/quickstart.md +9 -5
  13. package/docs/security.md +22 -13
  14. package/docs/smoke.md +3 -3
  15. package/docs/specs/2026-09-19-agent-hub-design.md +16 -5
  16. package/docs/verification/2026-10-25-remote-docker-transport.md +10 -0
  17. package/docs/verification/2026-10-26-agy-1.4.7.md +69 -0
  18. package/docs/verification/2026-10-26-claude-implementer-2.1.310.md +69 -0
  19. package/docs/verification/2026-10-26-headless-claude-shell-context.md +62 -0
  20. package/docs/verification/2026-10-26-jcode-0.99.1.md +49 -0
  21. package/docs/verification/2026-10-26-kimi-232-reviewer-compat.md +53 -0
  22. package/docs/verification/2026-10-26-pi-140-compat.md +41 -0
  23. package/docs/verification/2026-10-26-qwen-0261-acp.md +60 -0
  24. package/docs/verification/2026-10-27-0.19.0.md +86 -0
  25. package/docs/verification/claude-permission-transport-native.py +187 -0
  26. package/docs/verification/claude-shell-wrapper-host.ts +20 -0
  27. package/docs/verification/claude-shell-wrapper-native.py +145 -0
  28. package/docs/verification/pi-140-model-free.ts +57 -0
  29. package/docs/verification/pi-140-native.mjs +21 -0
  30. package/docs/verified.json +14 -14
  31. package/package.json +2 -2
  32. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  33. package/plugins/agent-hub/server.js +273 -68
  34. package/src/adapters/agy/entry.ts +1 -1
  35. package/src/adapters/claude-channel.ts +7 -3
  36. package/src/adapters/claude-implementer.ts +5 -3
  37. package/src/adapters/compat.ts +32 -18
  38. package/src/adapters/conformance/fakes/pi-rpc.ts +1 -0
  39. package/src/adapters/definitions/builtin-metadata.ts +2 -2
  40. package/src/adapters/definitions/native-factories.ts +7 -2
  41. package/src/adapters/definitions/reviewer-variants.ts +2 -2
  42. package/src/adapters/pi.ts +39 -3
  43. package/src/adapters/qwen-dual.ts +25 -0
  44. package/src/adapters/registry-types.ts +6 -2
  45. package/src/adapters/registry.ts +1 -0
  46. package/src/cli/agents.ts +69 -7
  47. package/src/cli/claude-shell-wrapper.ts +54 -0
  48. package/src/cli/main.ts +30 -24
  49. package/src/cli/output.ts +4 -0
  50. package/src/hub/agent-drift.ts +61 -0
  51. package/src/hub/board.ts +14 -2
  52. package/src/hub/check-runner.ts +148 -18
  53. package/src/hub/claude-shell-wrapper.ts +349 -0
  54. package/src/hub/config-edit.ts +2 -2
  55. package/src/hub/config-tier.ts +2 -2
  56. package/src/hub/daemon.ts +197 -35
  57. package/src/hub/host-executable.ts +20 -1
  58. package/src/hub/instances.ts +18 -2
  59. package/src/hub/lifecycle.ts +1 -1
  60. package/src/hub/operational-authority.ts +7 -0
  61. package/src/hub/operational-view.ts +6 -10
  62. package/src/hub/read-only-shell-proof.ts +7 -6
  63. package/src/hub/review-scope.ts +13 -0
  64. package/src/hub/sdlc-metrics.ts +6 -4
  65. package/src/hub/task-shell-command.ts +85 -0
  66. package/src/hub/task-shell-default.ts +78 -0
  67. package/src/hub/task-shell-proof.ts +171 -0
  68. package/src/hub/task-shell-sandbox.ts +61 -0
  69. package/src/hub/tasks.ts +139 -13
  70. package/src/local/deny.ts +91 -11
  71. package/src/local/sandbox.ts +67 -28
  72. package/src/local/shell-environment.ts +10 -0
  73. package/src/ui/index.html +1 -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.19.0
8
+
9
+ - The headless Claude implementer runs proven commands without asking in `ask-when-needed` (#845, #857): read-only commands and closed task-gate commands (check, test and build; `git add` and `git commit`; `git push` of the bound task branch; `gh pr` reads and create) go through a fixed host wrapper with a single-use ticket bound to task, child, root, branch, argv and environment, rechecked before spawn. Check, test and build code runs under the local-worker sandbox and cannot write project Git metadata (objects included); host add and commit run outside it with hooks, fsmonitor, signing and maintenance off. The default branch comes from the remote once at startup. `git add` and `git commit` still ask when Git filters or textconv drivers are configured; push and `gh` grants are kept with a global Git LFS install.
10
+ - Optional `model` for the headless Claude implementer instance (#844, #848).
11
+ - Code-only review scope (#852, #853): the sandboxed codex and kimi fresh reviewers declare `review scope: code`, `review_scope` overrides it per instance, and a code-scope `changes_requested` with no open structured finding is evidence-only (recorded, never escalated, one ids-only notice to the conductor).
12
+ - Agent CLI drift (#849, #855): a daily watch of every public CLI source records the receipt a new version should repeat, and the planner is told when an installed agent CLI is not a verified version; `ahub status` shows an `agent CLI drift` line.
13
+ - Pi has a native version gate (#843, #847): 1.4.0 is verified, any other version is refused at every launch path unless the person runs `ahub agents trust pi <version>`.
14
+ - Planners get one merged `Planner work ready:` notice when a write frees an owner's capacity or releases backlog (#785, #842).
15
+ - The remote check runner mounts a lockfile-keyed, read-only dependency volume; a manifest with an install lifecycle script is refused (#793, #846).
16
+ - Verified CLI versions: agy 1.4.7 (#851), Jcode 0.99.1 (#850), kimi-review 2.3.2 (#856), headless Qwen ACP 0.26.1 (#859), claude-implementer 2.1.310 (#858).
17
+ - Sandbox: a linked worktree's submodule git dirs and `config.worktree` are protected (#862), and every write under a modules root outside a discovered submodule git dir is refused (#865, #866).
18
+ - Qwen dual output: Qwen's own background-task notice is not the person's prompt and ends no hub turn (#861).
19
+ - Conductor operations keep their own error and are audited as refused only on an authority refusal; safe history is cached (#812, #839).
20
+ - CI admission retries a transient GitHub 5xx or 429 twice inside its deadline and stays fail-closed otherwise (#872, #873); check-runner receipt saves are serialized per run (#867, #868).
21
+ - The suite watchdog stops a run on lost progress, not elapsed time (#516): `scripts/progress-tee.sh` forwards bun's output and appends one byte per test result line or file header to a progress file, and `scripts/hang-watch.sh` kills a suite whose progress file has not grown for `AHUB_CHECK_QUIET_S` seconds (300 by default), printing the process table below it first. `AHUB_CHECK_HANG_S` stays as a backstop (1500 s on Linux and 1800 s on macOS in CI, with 35 and 40 minute job limits). A slow suite that keeps passing tests is no longer cut at the total bound.
22
+ - Pin @staix/agent-mem 0.10.0 (standalone MCP screening/ranking and capture-worker prefetch improvements; unchanged hub policy and storage contracts). Refs agent-mem#13.
23
+ - Pin @staix/agent-mem 0.9.1 (split-home sync holds a verified foreign-generation prune as "held, not applied" instead of refusing the sync, cached MCP policy screening, `--capture-days` claude-mem capture import; 0.9.0 was never published). Refs agent-mem#13.
24
+ - Tests and docs: the snapshot clock is held in the projection-surface daemons (#515, #854), benchmark Python bytecode is ignored (#840), and the cut-payload guard is stated as refusing session-wide grants (#843, #860).
25
+
7
26
  ## 0.18.0
8
27
 
9
28
  - Readable output by default (#814): `ahub sdlc status|explain|queue|release|metrics|costs|authorize-release` (#831), `task show|cancel`, `backlog groom|return|plan|pin|split|undo|pull-ready` and `release list|show|new|plan|activate|freeze|ship|drop` (#834), and `restart` and `upgrade` off a terminal (a pipe or `--dry-run`), `recovery status` and the results of `recovery reattach|resume|abort|dispose`, `models setup|start`, `queue resolve` and `budget execution configure|disable` (#833) print for a person. Scripts must pass `--json`, which prints exactly the previous document (the hub tool's text for the task, backlog and release commands). `--json` and `--color=` are flags only at the start or end of a command that takes free text, and an invalid or repeated `--color` is refused before any request is sent. Guard tests fail when a CLI path prints JSON outside `--json` or a named data format, or prints a hub tool's text that is not behind `--json` or a named exception (#837).
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.18.0, control protocol 16. Bun + TypeScript, one loopback daemon per
8
+ Status: 0.19.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
@@ -76,8 +76,8 @@ the [Quickstart](docs/quickstart.md) ("Upgrading to ...") and the
76
76
  refreshing an agent's MCP entry.
77
77
 
78
78
  Agent CLIs update themselves. `ahub agents` shows which installed versions the hub
79
- accepts, and `ahub agents trust <id> <version>` accepts an untested one on this
80
- machine.
79
+ accepts, `ahub agents trust <id> <version>` accepts an untested one on this
80
+ machine, and the hub tells the planner when an installed version is not verified.
81
81
 
82
82
  Before you migrate a memory home with `amem`, read
83
83
  [memory profiles and rollout](docs/memory.md#selected-memory-profile). Every hub that
@@ -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-25-0.18.0.md) and the
97
+ [release verification report](docs/verification/2026-10-27-0.19.0.md) and the
98
98
  [smoke checklist](docs/smoke.md).
99
99
 
100
100
  ## Documentation
@@ -48,3 +48,9 @@ Background behind the 0.18.0 rules:
48
48
 
49
49
  - CLI print guards (#814, tests.md): a command that printed JSON by default made scripts the only reader; each converted command keeps `--json` byte-identical to the old output, and the guard scans for both a bare `JSON.stringify` print and a bare print of a hub tool's text so a new unlisted command cannot slip back.
50
50
  - Agent shell authority (#812, adapters.md): reading public state is allowed for every agent, the conductor gets a closed operational family, and every other form stays the person's. An allowed read form is added only when the daemon admits it for an ordinary tools connection too.
51
+
52
+ Background behind the 0.19.0 rules:
53
+
54
+ - Code-scope verdicts (#852, tasks.md): two review rounds showed that any classifier reading a `code` reviewer's unmet text files real defects, such as "the CI workflow is missing the trigger", as verification-only, so the evidence-only rule reads only the structured findings.
55
+ - Policy-table fixtures (#843, tests.md): a fixture with a fixed list of keys or versions, which a planned `COMPAT` entry was not in, was reported twice in #843.
56
+ - Suite watchdog (#516, tests.md): a total-time kill cut suites that were still passing tests line by line (hosted ubuntu runs at 1080 s, the macOS leg of PR #857 at 1440 s), so a lost-progress bound became the hang detector and the total bound only its backstop.
@@ -1,6 +1,6 @@
1
1
  # Native adapters and spawned agents
2
2
 
3
- Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` and `activity` events), peer turn state, approvals, and the agent processes the hub spawns and stops. Read before editing `src/adapters/`, `src/pi/`, `src/hub/peers.ts`, `src/hub/permission-mode.ts`, `src/hub/tool-refusal.ts`, `src/hub/action-enforce.ts`, `src/hub/permission-payload.ts`, `src/hub/child-process.ts`, `src/hub/lifecycle.ts` (shutdown deadline), `src/hub/start-mode.ts`, `src/hub/terminal-open.ts`, `src/memory/capture.ts` (session end), `src/hub/native-hook-launch.ts`, `src/hub/peer-catalog.ts`, the launcher-managed MCP entries (`src/cli/agy-mcp.ts`, `src/cli/kimi-mcp.ts`, `src/cli/jcode-mcp.ts`), `src/cli/configured-acp-entry.ts`, the version policy (`src/adapters/compat.ts`, `src/cli/agents.ts`) or the process-tree stop in `scripts/benchmarks/teardown.ts`, or before adding a native peer.
3
+ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` and `activity` events), peer turn state, approvals, and the agent processes the hub spawns and stops. Read before editing `src/adapters/`, `src/pi/`, `src/hub/peers.ts`, `src/hub/permission-mode.ts`, `src/hub/tool-refusal.ts`, `src/hub/action-enforce.ts`, `src/hub/permission-payload.ts`, `src/hub/claude-shell-wrapper.ts`, `src/hub/task-shell-command.ts`, `src/hub/task-shell-default.ts`, `src/hub/task-shell-proof.ts`, `src/hub/task-shell-sandbox.ts`, `src/cli/claude-shell-wrapper.ts`, `src/hub/child-process.ts`, `src/hub/lifecycle.ts` (shutdown deadline), `src/hub/start-mode.ts`, `src/hub/terminal-open.ts`, `src/memory/capture.ts` (session end), `src/hub/native-hook-launch.ts`, `src/hub/peer-catalog.ts`, the launcher-managed MCP entries (`src/cli/agy-mcp.ts`, `src/cli/kimi-mcp.ts`, `src/cli/jcode-mcp.ts`), `src/cli/configured-acp-entry.ts`, the version policy (`src/adapters/compat.ts`, `src/cli/agents.ts`) or the process-tree stop in `scripts/benchmarks/teardown.ts`, or before adding a native peer.
4
4
 
5
5
  - For conceptual architecture, definition fields, generic ACP config and the new-adapter review checklist, see [Adding an AI Tool](../adding-an-ai-tool.md). For definition interfaces, see [registry-types.ts](../../src/adapters/registry-types.ts).
6
6
 
@@ -8,7 +8,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
8
8
 
9
9
  - Keep per-kind native policy in `src/adapters/definitions/` and consume it through the registry. Private CLI implementations accept only fixed host-bound ports; keep identity, audit, lifecycle admission and effect ordering in the host. Except for the verified person-owned A4 startup-module contract, do not add config-selected callbacks or runtime imports from registry leaves into eagerly initialized hub/CLI modules; use type-only ports for existing host planners.
10
10
  - Adapter registry (#451): kind-specific policy lives in that peer's `AdapterDefinition` under `src/adapters/definitions/` (runtime definitions `NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN` in `builtin-metadata.ts` plus `qwen.ts`; the daemon constructs every native driver through `start.create` and `DAEMON_PORTS`; `BUILTIN_DEFINITIONS_WITHOUT_QWEN` is the frozen A1 export, read by no runtime consumer). Outside `src/adapters/`, read a semantic field of `definitionOf(id)`: never a raw peer-id comparison, a resolved kind (`kindOf`, `definitionOf(...).kind`) or a constant alias compared with a native name, a vendor-keyed object, a peer-name array, or a closed-list import from `peer-catalog.ts`, `start-mode.ts` or `permission-mode.ts`; the guard resolves aliases and import/re-export bindings. `test/registry-guard.baseline.json` is `[]` and stays empty: a new site moves into a definition, never into the baseline. The only exceptions are the cross-release literals `KNOWN_PEERS`, `OPTIONAL_PEERS` and `START_MODE_PEERS`, which `test/registry-guard.test.ts` holds equal to the derived lists. `LaunchContext.nativeCommand` carries only validated config argv (or its redacted preview), never model input; a definition's `permission.grants` text is what `permissionGrant` shows, ahead of the built-in wording. Keep native mode-id maps in `definitions/*-native.ts`, free of registry factories (`test/adapter-import-order.test.ts`).
11
- - Registry load runs `assertDaemonEngine` and `assertNativeCapabilityScope` before publication. A definition that is not a fixed builtin (object identity in `fixedBuiltinDefinitions`, never a kind name) may not declare `start.acpCommand`, `attach.admission`, `budget.resetSoftTokensOnResume`, `session.crashIdentity`, `session.nativeRevert`, a `pi-rpc` or `claude-implementer` daemon port, `identity.currentChild`, `routing`, `projection`, `reviewerVariant`, `headlessVariant`, `reviewer`, `hooks.factsHook`, `cli.entry`, `cli.preview` or `cli.usage`. Only `definitionOf(id, instances)` picks a kind's `reviewerVariant`, for a declared extra with `start_mode: "headless"`, roles exactly `["reviewer"]`, and either a variant whose `reviewer.declaration` is `"automatic"` (Claude, agy) or `review_mode: "fresh"` (Codex, Kimi); without that opt-in a reviewer-only Codex or Kimi instance keeps its native adapter. It picks a kind's `headlessVariant` (Claude, #525-4) for a declared headless extra whose roles include `implementer` and hold only `implementer` and `verifier`. Metadata-only reviewer variants never establish native peer support; `isSupportedPeer` shares the registry runtime/validated-instance predicate with the catalog and daemon. A new field that depends on fixed-builtin assumptions or host-global native state joins that list (`test/adapter-capability-scope.test.ts`). `trust.onPrem` admits PII work (rule: `tasks.md`).
11
+ - Registry load runs `assertDaemonEngine` and `assertNativeCapabilityScope` before publication. A definition that is not a fixed builtin (object identity in `fixedBuiltinDefinitions`, never a kind name) may not declare `start.acpCommand`, `attach.admission`, `budget.resetSoftTokensOnResume`, `session.crashIdentity`, `session.nativeRevert`, a `pi-rpc` or `claude-implementer` daemon port, `identity.currentChild`, `routing`, `projection`, `reviewerVariant`, `headlessVariant`, `reviewer`, `hooks.factsHook`, `cli.entry`, `cli.preview`, `cli.usage` or `instances.modelSelect`. Only `definitionOf(id, instances)` picks a kind's `reviewerVariant`, for a declared extra with `start_mode: "headless"`, roles exactly `["reviewer"]`, and either a variant whose `reviewer.declaration` is `"automatic"` (Claude, agy) or `review_mode: "fresh"` (Codex, Kimi); without that opt-in a reviewer-only Codex or Kimi instance keeps its native adapter. It picks a kind's `headlessVariant` (Claude, #525-4) for a declared headless extra whose roles include `implementer` and hold only `implementer` and `verifier`. Metadata-only reviewer variants never establish native peer support; `isSupportedPeer` shares the registry runtime/validated-instance predicate with the catalog and daemon. A new field that depends on fixed-builtin assumptions or host-global native state joins that list (`test/adapter-capability-scope.test.ts`). `trust.onPrem` admits PII work (rule: `tasks.md`).
12
12
  - Native branch ratchet (#625): `test/daemon-native-branches.test.ts` counts every call of a native class predicate (an `is*Peer` imported from an adapter module other than `registry.ts`) per enclosing function in `src/hub/daemon.ts` and `src/hub/claude-launch-proof.ts`, and the counts must equal `test/daemon-native-branches.json` (`predicate`, `function`, `count`, `reason`). A PR that adds, moves or removes such a call edits its row by hand with a concrete reason; never regenerate the table from a test. Bind a predicate alias with `const` only and never call one through a computed namespace key: the scan refuses both.
13
13
  - Native proof state (Codex proxy URL, attachment and permission policy, `statusNotice`, `session.nativeRevert`, the Claude launch proof and terminal execution root) is read in the daemon only after `isFixedBuiltinDefinition(definitionOf(peer))`: a same-name foreign definition gets none of it. A product of a definition that is not a fixed builtin may not have a `frontendProxy`, `statusNotice` or `revertTurn` member at all, even undefined (`assertNativeProofPorts`, run by `createFromDefinition` and the conformance kit).
14
14
  - Adding a native peer: declare its `AdapterDefinition` under `src/adapters/definitions/` (or its own directory, as agy) and list it in `ADAPTERS` (`src/adapters/registry.ts`). `KNOWN_PEERS`, `OPTIONAL_PEERS`, `SUPPORTED_PEERS`, `START_MODE_PEERS` (a kind with both a TUI and a headless form), `ROLE_ORDER`, `HELP_ORDER` and `PLANNER_PEERS` are derived from it by `deriveAdapterLists`: set `optional`, `projection` (`roleRank`, `startRank`, `helpRank`, `plannerEligible`; a configured or external definition cannot) and, for an `ahub <kind>` launcher, `cli.entry`, `cli.usage`, `cli.preview` and `cli.startWords`, never a list edit (`builtinCliEntries(definitions)` projects the entries). The literal cross-release copies `KNOWN_PEERS` and `OPTIONAL_PEERS` in `src/hub/peer-catalog.ts` and `START_MODE_PEERS` in `src/hub/start-mode.ts` stay hand-kept for older coordinators: `test/registry-guard.test.ts` fails when one differs from the derived list. Adding a peer is the one change that updates their probe counts and `VENDORS` in `test/helpers/registry-inventory.ts` (`REGISTRY_COMPATIBILITY_PROBES`), the probe literal in `test/adapter-registry-a2.test.ts`, the `ADAPTERS` order in `test/adapter-registry.test.ts` and the literal in `test/adapter-catalog-a2.test.ts`; say so in the PR as an expected test change. `isSupportedPeer` answers from `ADAPTERS`, and for a declared instance from its kind's runtime definition unless the instance resolves to an unavailable reviewer variant; `isKnownPeer` and `defaultPeerRoles` answer from `definitionOf`; `STARTABLE_PEERS` reuses `SUPPORTED_PEERS`.
@@ -25,8 +25,8 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
25
25
  - An ACP permission request may carry no `rawInput` (Kimi 2.0.1): the arguments were on the earlier `tool_call` update, which `acp.ts` remembers by `toolCallId`. Kimi 2.1.1 sends no `rawInput` before the answer at all: the argument JSON streams as `content` text on `tool_call_update`, and only complete JSON counts. A payload that still cannot be resolved, or is too long to show whole (marked `[cut, N chars]`), withholds its `allow_always` option - never render a bare tool name as if it described the call.
26
26
  - Auto-approval covers the hub's own tools by exact `mcp__agent-hub__<name>` title and only ever picks `allow_once`. Identity comes from the permission request title or, when an agent titles the request with the argument JSON (Qwen), from the announced `tool_call` title bound to the call id and resolved against the session's configured MCP servers, never from payload text, a prefix or an unrelated display title. Never widen it to a prefix or replace it with an agent-selected session mode. Configured ACP kinds (`nativePolicy` or `requireOwnedGroup` set) never auto-approve from a request or tool-call title, even an exact hub tool name; under a locked read-only mode, reject any request whose ACP `kind` is not read, search or think, and never offer `allow_always`. Set Kimi `yolo`/`auto` only for its verified native identity and an operator mode; config never-ask waits for a console y, and other ACP agents stay unmanaged on fresh/resumed starts without setting/resetting modes or failing over their reported mode; refuse runtime changes until their map exists; await the advertised mode change after session new/load and before a prompt, and fail startup on refusal. Use the shared `KIMI_MODE_IDS` mapping (`src/adapters/definitions/kimi-native.ts`, re-exported by `src/hub/permission-mode.ts`).
27
27
  - Permission modes (#242) are a person's and live in the daemon's memory only: `permission` and `permission_default` are console-role control messages, `ahub permission` is `console` in `classifyPeerCommand`, and no hub tool or conductor tool sets one. The dashboard and `ahub settings` set one only through `settingsSet` (`src/hub/daemon.ts`, #269), which calls `changePermission` inside `permissionChange` as the console does: never add another path that writes a runtime choice to `permissionModes`; a reloaded project default (#439) is applied only by `applyPendingPermissionDefault`, and only while that peer is absent or offline. Never write a runtime choice to a restart snapshot.
28
- - Proven read-only shell grants (#816, `src/hub/read-only-shell.ts`, `read-only-shell-proof.ts`) apply only to Pi and the local worker under `ask-when-needed`, at the host permit boundary: parsing grants nothing, and a grant needs the closed program and flag table, trusted executable lookup and the path guard. Between the grant and the spawn the host calls `shellExecutionCheck` synchronously; `ask` re-asks once with `forceAsk` and a second unstable proof refuses without running, and an aborted or superseded turn (`executionGeneration`) never spawns. Never widen the table or skip the recheck to make a command pass.
29
- - Agent shell authority (#812, `classifyPeerCommand` in `src/cli/identity.ts`, `src/hub/operational-authority.ts`): the Allowed read forms live in `peerReadForms` and are admitted only when the daemon also admits the same form for an ordinary tools connection; the conductor family is the closed `conductorOperation` list, each admitted by `admitOperational` (role recheck, closed audit label, no argument text). A new CLI form is `console` until it is added to one of those tables with a test in `test/identity.test.ts`.
28
+ - Proven read-only shell grants (#816, `src/hub/read-only-shell.ts`, `read-only-shell-proof.ts`) apply to Pi/local and the #845 headless Claude host wrapper under `ask-when-needed`, at the host permit boundary: parsing grants nothing, and a grant needs the closed program and flag table, trusted executable lookup and the path guard. For Pi/local, between the grant and the spawn the host calls `shellExecutionCheck` synchronously; `ask` re-asks once with `forceAsk` and a second unstable proof refuses without running, and an aborted or superseded turn (`executionGeneration`) never spawns. Never widen the table or skip the recheck to make a command pass.
29
+ - Agent shell authority (#812, `classifyPeerCommand` in `src/cli/identity.ts`, `src/hub/operational-authority.ts`): the Allowed read forms live in `peerReadForms` and are admitted only when the daemon also admits the same form for an ordinary tools connection; the conductor family is the closed `conductorOperation` list, each admitted by `admitOperational` (role recheck, closed audit label, no argument text; `queue show`, a settings preview and the authorize handshake recheck through `operationalCurrent` without an audit record). Throw `OperationalAuthorityError` only from an authority fence: only it is audited as refused and gets the conductor or person guidance, and an ordinary operation failure keeps its own error (#839). A new CLI form is `console` until it is added to one of those tables with a test in `test/identity.test.ts`.
30
30
  - A headless ACP Kimi or Codex whose mode lives in the native agent joins the bus only through `reconcileNativeStart` (`src/hub/daemon.ts`): after `start()` the daemon's current mode is applied, then `bus.add` (it can drain at once), then the state `start()` reached is replayed. Every mode change runs inside `permissionChange`, one per peer at a time.
31
31
  - Generic permission and generation dispatch in the daemon reads the optional `PeerAdapter` ports (`src/hub/peers.ts`, #667): `permissionModeState`, `setPermissionMode` (resolves only after native acknowledgement; changes approval frequency, never the sandbox), `sessionGeneration` and `checkpointUsesSessionGeneration`; never add a native-class predicate (`isAcpPeer`, `isCodexPeer`) for it. An absent `sessionGeneration` keeps the recovery-session/owner-token fallback; a present but undefined one is unknown and refuses. `BasePeer` installs no default generation, and only an explicit `checkpointUsesSessionGeneration = false` (Pi) skips the checkpoint generation fence. A `permissionModeState` of `unknown` or `unmanaged` refuses a change.
32
32
  - Codex takes a mode as an `approvalPolicy` override on `turn/start`, and the override persists on the thread: pass the hub's and the TUI's `turn/start` alike through `applyPermissionPolicy`, and return to `ask` only by sending the captured native policy once. With none captured, refuse while the TUI is attached; never just stop sending the override. Keep the captured native policy and the restoration debt per thread (`nativeApprovalPolicies`, `overriddenApprovals`) across detach and `thread/start` or `thread/resume`; clear a debt only on a successful response to its restoring `turn/start` while the mode is `ask` (`settlePermissionRestore`), so a rejected turn keeps it. While a thread owes a restoration with a known baseline, in any mode, never replace that baseline: a `thread/resume` response and the TUI's own `turn/start` can both echo the sticky override, and the restoring turn sends the kept baseline whatever the TUI supplied. That protection is one turn: after the restoration succeeded, a policy the TUI sends is taken as the person's choice, captured and passed through, because the proxy cannot tell a repeated echo from one. A detached proxy's `clearPermissionMode` is never refused; if the native policy is unknown, return a CLI-visible note that the thread keeps its last policy. Attached refusal directs the person to close the TUI, then run `ahub permission codex ask`.
@@ -66,6 +66,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
66
66
  ## Pi
67
67
 
68
68
  - Tag a Pi hub-tool refusal only at a proven pre-effect guard with `PreEffectToolRefusal`; return its exact error and settle its receipt. Never classify a generic/post-effect/storage failure or a legacy pending receipt from error text or current board state. After durable `markStale`, cancellation or output consumption, retain ordinary failure classification; a later binding/privacy rejection cannot prove that no effect occurred. Recheck task revision, owner/session generation and privacy across awaited bounded spec/check operations; never turn those tools into arbitrary host execution.
69
+ - Version gate (#843): `PiPeer.startImpl` first awaits the preflight the production factory `createPi` passes as a separate constructor argument (keep it out of `PiOptions`: transport option identity), which runs `piVersionAsync` on the exact configured `pi.cmd` plus `--version` (owned group, 64 KiB, 10 s) against compat `pi`. Only a `PiVersionRefusal` reaches the caller as is (sanitized, 1000 characters); never wrap another start error in it. A fake Pi in a daemon fixture answers `--version` with a verified version.
69
70
  - Report each Pi owner exit once, with code/signal (unknown for an unobserved TUI exit), turn/tool activity and only the last tool name. Auto-restart only a current, started, unexpectedly idle headless owner on its verified persisted session, once per 60 s; preserve shutdown/requested-stop/recovery/startup/active-tool fences and never fall back fresh.
70
71
  - Keep Pi owner identity on the shared `processSignature`/`processLiveness` contract exported by the exact-pinned package's Node-safe `@staix/agent-mem/platform` entry, re-exported unchanged by `src/pi/process-signature.ts` for the startup claim, extension claim, TUI owner monitor, `recoveryReady` and verified teardown. Memory's `sync.lock` uses the same implementation. Preserve the Node-safe process-entry regression in `test/memory-parity.test.ts`, the existing Pi process behavior tests, the LC_ALL=C/TZ=UTC `ps` read and strict pid + start + command identity; never accept PID-only or a fuzzy command match.
71
72
  - Return the shared `toolResultFailed` verdict as `/tool`'s `failed`; map only `failed === true` to Pi's `isError`. Preserve text and the older bridge's absent-flag behavior. The one exception is a person's `!command` whose process exit was observed: its `failed` is `exitCode !== 0` and the reply carries `exitCode`, from `onProcessExit` through `packPiShellResult`, never parsed from the output (#253).
@@ -81,6 +82,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
81
82
  - Judge a Qwen TUI by `holdsTerminal()` (its bound owner verified alive, or a session seen with no owner bound), never by peer state: a detached TUI reads offline while Qwen keeps running. A headless start over a held terminal is refused and a TUI start answers `already`.
82
83
  - Gate the TUI on `COMPAT["qwen-dual"]` (verified 0.26.1, no `line`), both before the start (`qwenDualVersionAsync`) and from the TUI's own `session_start` (version, `protocol_version` 2, the events read). Never widen the headless `qwen` entry for it, or the other way round: their receipts are separate.
83
84
  - `deliver()` sets `busy` synchronously and appends one `submit`; a prompt that is not the hub's own (matched by its text) is the person typing: the peer stays busy, nothing is shared, no approval is asked of the console. A submit that starts with `/`, `!`, `@` or `?` gets an `[agent-hub]` first line: Qwen reads a submit as typed input (slash command, shell line, file include). A turn ends at an `assistant` whose `stop_reason` is not `tool_use` followed by `message_stop` (or a `result`); a `tool_use` message never ends it.
85
+ - A Qwen background-task notice is not the person typing (#517): when a background shell finishes Qwen writes a `user` event of its own, which can land in the middle of a running hub turn, and several finished shells fold into one event. `isQwenTaskNotification` accepts exactly one or more whole `<task-notification>` envelopes (task id, kind, status header, blank line between envelopes) and nothing else; `onUser` judges the raw event, not `promptText` (which joins every text block and drops the rest): the content must be exactly one text block that is wholly the envelope, so a split or mixed-block event is a person's prompt. Such an event changes nothing: a running hub turn keeps its owner and is not failed, and an idle peer stays idle (never mark it busy for a notice nobody may answer). Read nothing inside an envelope beyond its header, and never loosen the shape: a prompt that only contains or resembles one is still a person's prompt and still settles a hub turn as uncertain. The old reading held 7 of 7 recorded `needs_review` queues; when a new Qwen event type shows up inside a turn, check the live `events.jsonl` against `onUser` before widening anything.
84
86
  - Limit 1: Qwen emits nothing for a deny, so after `allowed: false` the adapter ends the turn itself (the delivery `completed`, since a deny is a known outcome and `needs_review` holds the peer's whole queue: measured against the real Qwen; no answer; a notice). Only an unknown outcome (Qwen exited, Esc then a new prompt, an unreadable event file) settles `needs_review`. Limit 2: the watchdog never cancels. `onWatchdog` marks the turn stalled and keeps the peer busy; the next event clears it. Never type into the terminal, send a signal to stop a turn, or add an input type: the protocol has only `submit` and `confirmation_response`. A pending approval is a person deciding, not silence.
85
87
  - Only an exact hub tool name is auto-approved (`autoApprove`, the daemon's `HUB_TOOL_TITLES`), and an always-ask class call never is. A `control_response` for a request the hub has not answered means the person answered in the TUI: abort the hub's pending request so a late console answer finds nothing. A request other than a hub tool that names the hub state dir (`qwenDualNamesStateDir` against `guardDir`) is refused before anyone is asked: the input file is a prompt channel (`docs/security.md`).
86
88
  - The event file is read from the saved offset in `state.json`. Qwen opens an existing event file at byte 0 without truncating it (measured on 0.26.1), so a fresh or resumed launch starts from empty files (the old event file is kept as `events.prev.jsonl`) and a hub restart adopts a still-running Qwen from the saved offset instead. The owner is bound from the host's process table (the root of the tree of processes whose argv names this adapter's input file; two separate roots is no owner), never from anything the launcher sends. An unreadable event file is `detached` (offline, the `qwen-detached` status notice), never idle, and reattaches when it reads again; a stall shows `qwen-stalled`. An event line over 1 MiB is dropped through its newline and the saved offset moves past it.
@@ -91,13 +93,13 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
91
93
  - Kimi TUI is a pull-only WsPeer under the one kimi id, with no hub busy/watchdog/permission/quota machinery. Only the person's launcher merges or removes its project MCP entry; a daemon headless start refuses a stale entry and never edits the file. Scrub mode/channel markers from the native child; managed AGENTS instructions and tool descriptions carry the pull rules, not MCP initialize instructions.
92
94
  - Empty-inbox polling advice (#681, `InboxPolling` in `src/hub/inbox-polling.ts`) is advisory: never refuse or delay an inbox read for it. The daemon computes it only for a pull-only peer after the pull completed, so a throw drops the advice, never the pulled envelopes; it counts only empty reads with no open, non-backlog task as owner or reviewer, and an assignment resets it. The plugin shows `[polling detected]` only for a well-formed `pollingAdvice` (count >= 3, minutes 0 to 10).
93
95
  - TUI launchers (Kimi, Jcode, agy) write the plugin bundle the hub reports in `status` into their MCP entry, and the CLI's own copy only when the hub reports none (#502). Only the Jcode start path checks the entry against the daemon's own `SERVER_JS` (`jcodeMcpReady`); Kimi TUI admission does not compare it.
94
- - Native CLI versions (#691): every version gate asks `assessVersion` in `src/adapters/compat.ts`; no other file holds a version constant. Add a version to `verified` only with a receipt in `docs/verification/`; a `line` is for contract gates only (an isolation gate never assumes a newer version compatible); the person's `ahub agents trust` admits one exact version on this machine and never skips a runtime proof; `ahub agents` with anything but `--json` stays `console` in `classifyPeerCommand` and refused in `liveTestRefusal` (`src/cli/identity.ts`). A version refusal starts with `VERSION_REFUSAL` and names only versions and the trust command; keep it that way so the daemon may show it.
96
+ - Native CLI versions (#691): every version gate asks `assessVersion` in `src/adapters/compat.ts`; no other file holds a version constant. Add a version to `verified` only with a receipt in `docs/verification/`, and point the entry's `receipt` at the receipt a later version should repeat (the daily `scripts/agent-compat-watch.ts` names it, #849); a `line` is for contract gates only (an isolation gate never assumes a newer version compatible); the person's `ahub agents trust` admits one exact version on this machine and never skips a runtime proof; `ahub agents` with anything but `--json` stays `console` in `classifyPeerCommand` and refused in `liveTestRefusal` (`src/cli/identity.ts`). A version refusal starts with `VERSION_REFUSAL` and names only versions and the trust command; keep it that way so the daemon may show it.
95
97
  - Jcode (#529): accept the native version only through `jcodeVersionSupported` (compat `jcode` line, 0.98.0 up to 1.0.0), in the daemon preflight (`jcodeVersionAsync`: off the event loop, owning its process group, output capped at 64 KiB), the doctor probe (`jcodeVersion`) and ACP `expectedAgent`; never pin the minor, and lower `line.below` when a release breaks the contract. Every owned jcode process (probe, serve, ACP and TUI) gets `JCODE_OWNED_ENV`, and the private socket name ends in `.sock`.
96
98
  - Jcode TUI (#464): use only the current owned launch's top-level `--socket`/`--resume` tuple after native saved-session proof. Prove the save only through `savedSession` (no-follow, single-link regular file, size-bounded, unchanged across the read, its `id` and `messages` matching) and refuse a requested model, provider profile or session that differs from the running launch with `JcodeOptionMismatch`, which names the flag, never its value (#559); both the launcher and `--print-command` report it through `refuseNamedOption` (#574). Hand the tuple only to a person's launcher through `native_tui_launch` (admission: `daemon.md`); previews (`--dry-run`, or `--print-command` from anyone but a person) start nothing and never resolve it. Never synthesize a save or prompt to induce one. Ctrl-C during the save wait and frontend close retain ACP; preserve session/load identity refusal and headless owned recovery metadata, and never record the TUI as a managed terminal (`recordTerminalLaunch`): Jcode has no terminal recovery adapter (#564).
97
99
  - Jcode steer (#572): reach a busy owned jcode only through `nativeSteer` on its private server socket, never by cancelling its turn: `ping` must list `soft_interrupt_ids`, `subscribe` uses `target_session_id` (the ACP session id) and `continue_on_disconnect`, `state` must report `is_processing` (a soft interrupt sent while idle starts a turn by itself), then `soft_interrupt {content, urgent: false, client_id}`. `canSend` rechecks the captured turn, busy state, owned server process, socket identity and session immediately before the write. Every refusal before the send is `failed_safe` with nothing sent; a sent message that is not acknowledged, or whose turn cannot be confirmed, is `needs_review`, never `failed_safe` or `completed`. Read `currentTurn()` before sending and adopt only through `AcpPeer.adoptIntoTurn(turn, ...)`, which refuses a different or ended turn; adopted senders join the turn answer's audience (`replyParent` and `replyAudience` over the turn envelopes, so the hop cap holds), and each steer receipt is `accepted` at once and settles with the turn (`completed` on a normal end, `needs_review` on a cancel, failure, stop or watchdog). Headless Kimi and Qwen have no steer: status reports their waiting important messages only through `importantWaitingStatus` (`src/hub/daemon.ts`) past `budget.stale_min`. A turn that ends and restarts between jcode's `state` reply and the write still receives the message, held `needs_review` and adopted into nobody: keep that ceiling until upstream offers an atomic per-turn steer, and never add a cancel-to-deliver path.
98
100
  - Jcode MCP set (#572): `jcode acp` and `jcode serve` start every server named in `<JCODE_HOME>/mcp.json` and the project's `.jcode/mcp.json`, `.mcp.json` and `.claude/mcp.json`, and no flag or variable selects them (`--mcp-tools`, `JCODE_MCP_TOOLS`, `JCODE_DISABLED_TOOLS` and `JCODE_DISABLE_CLAUDE_MCP` do not). Do not paper over it: `ahub doctor` lists the extra names through `jcodeExtraMcpServers` (`src/adapters/definitions/native-cli-jcode-mcp.ts`), names only, those four paths only, no home search, no link followed. Below the project root or home, `.jcode` and `.claude` must be real directories and the file a regular file (per-component `lstat`, an `O_NOFOLLOW` open and an `fstat` identity recheck); a system alias at or above the configured root stays allowed. Revisit if upstream adds a server selector.
99
101
  - Extra Jcode instances (#685, #368): one `JcodePeer` per declared id (at most 8), each with its own `jcode_home`, socket and state. The person authenticates inside Jcode; the hub passes `-p`, `--model` and `--provider-profile` exactly as declared, handles no model credential or URL, and only checks that the home is a directory the person owns (`jcodeHomeRefusal`: no read or write under it). Never put `AGENTHUB_PEER_ID` in the shared `.jcode/mcp.json` entry (`jcodeMcpEntry`): every owned launch supplies it through `peerChildEnv(<id>)` or the TUI launch env, together with its `AGENTHUB_LAUNCH_ID`. The MCP server with no `AGENTHUB_PEER_ID` in tools mode refuses every tool and never defaults to a peer id. The daemon admits a `tools` hello for a peer whose definition is `owned-acp` only when it carries the launch id of that peer's current owned launch (`JcodePeer.ownsLaunch`, from `jcodeLaunches`); never trust the env alone. `jcode --as <id>` never opens a TUI. Declare the semantic `instances.nativeHome` on a definition, never compare the kind name (`registry-guard`); it is a fixed-builtin capability that `assertNativeCapabilityScope` refuses elsewhere. `checkJcodeInstanceHomes` (`src/hub/instances.ts`, run once at daemon startup) refuses a home equal to or nested in the base Jcode's or another instance's; compare homes only through its `canonicalHome` (`realPath`, case-folded on a volume that folds case, inode), never `resolve()` text. The preview of `--as <id>` passes the instance to `previewJcode` so it shows the daemon's real flags.
100
- - Antigravity P1 (`src/adapters/agy/`): keep it optional, cloud, off roster by default, implementer/verifier only and pull-only. Until per-mode native prompt behavior is verified, reject hub permission/unattended mappings, headless starts and resume selectors; do not infer a quota source. The launcher refuses extra instances, any `agy --version` that compat `agy` refuses (1.4.2 is T0-verified; other 1.4.x patches launch with a stderr warning, #589; another version needs the person's trust, #691), and a hub without the `native_hook_launch` feature.
102
+ - Antigravity P1 (`src/adapters/agy/`): keep it optional, cloud, off roster by default, implementer/verifier only and pull-only. Until per-mode native prompt behavior is verified, reject hub permission/unattended mappings, headless starts and resume selectors; do not infer a quota source. The launcher refuses extra instances, any `agy --version` that compat `agy` refuses (1.4.2 and 1.4.7 are T0-verified; other 1.4.x patches launch with a stderr warning, #589; another version needs the person's trust, #691), and a hub without the `native_hook_launch` feature.
101
103
  - Only the person's `ahub agy` launcher manages `.agents/plugins/agent-hub/{plugin.json,mcp_config.json,hooks.json}`. Refuse linked/foreign entries before any write; cleanup only the matching launch. Never write project-level `.agents/hooks.json` or personal Gemini settings. The plugin MCP server is named `agent-hub_agent-hub` by the pinned native client.
102
104
  - Managed native facts use the definition's typed `NativeFactsPort` and the daemon's ephemeral `native_hook_launch` authority. Bind every observation to the daemon, launch, native session, project and current socket generation. Stop requires `fullyIdle`, normal termination and a fresh validated completion row; a missing readback keeps the peer busy. Native fact acknowledgement accepts only SDK ephemeral-message rows in the exact session transcript path. `decodeAgyHook` classifies as `Read` only the observed native read (`view_file` `AbsolutePath`, or 1.4.3 `read_file` `Path`); a `StartLine`/`EndLine` range becomes `offset`/`limit`, so a partial read never counts as a whole-file view. Unobserved edits get no scoped grant or effect attribution. The daemon records Pre/PostToolUse through `Facts.preTool`/`postTool` and offers nothing on them (#555).
103
105
  - `terminalOwner(launchId)` (`src/hub/peers.ts`, #482) answers only for the adapter's current native TUI owner bound to that launch (Codex: the claimed link with that `launchId`, both sockets open; the launcher sends the id as the upgrade's bearer token through `--remote-auth-token-env`, because `codex --remote` refuses any path or query (#586); Pi: a started, verified TUI owner on its requested session); a start acknowledgement is never ownership. A Codex link that is not the claimed one or has a closed socket never adopts a thread.
@@ -117,10 +119,10 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
117
119
 
118
120
  ## Headless Claude implementer (#525-4)
119
121
 
120
- - `ClaudeImplementerPeer` (`src/adapters/claude-implementer.ts`) runs one owned print child per turn in its verified worktree and none while idle. Keep the native argv of `claudeImplementerArgs`: `--permission-mode default`, `--strict-mcp-config` and `--permission-prompt-tool mcp__agent-hub__hub_permission_request`; never `acceptEdits`, `dontAsk`, `bypassPermissions` or `--bare`. `never-ask` and `--unattended` are refused at every layer (factory, `setPermissionMode`, daemon start, `validateInstances`); a mode change only changes what the host facts gate allows. A start needs a version compat `claude-implementer` admits, and each turn rechecks the binary fingerprint from `proveClaudeImplementer`.
122
+ - `ClaudeImplementerPeer` (`src/adapters/claude-implementer.ts`) runs one owned print child per turn in its verified worktree and none while idle. Keep the native argv of `claudeImplementerArgs`: `--permission-mode default`, `--strict-mcp-config` and `--permission-prompt-tool mcp__agent-hub__hub_permission_request`; never `acceptEdits`, `dontAsk`, `bypassPermissions` or `--bare`. `--model` is added only from the instance's validated declared `model` (#844, `instances.modelSelect` on the headless variant; one `NATIVE_WORD`, headless implementer only, untracked config only), on every turn, resumed ones included; never from task, chat or tool text. `never-ask` and `--unattended` are refused at every layer (factory, `setPermissionMode`, daemon start, `validateInstances`); a mode change only changes what the host facts gate allows. A start needs a version compat `claude-implementer` admits, and each turn rechecks the binary fingerprint from `proveClaudeImplementer`.
121
123
  - Per-task native sessions live only in `ClaudeTaskSessions` (`src/adapters/claude-task-sessions.ts`, file `claudeStateFile("claude-task-sessions", id)`, ids only). A reserved UUID becomes `--resume` only after `markStarted` saw a stream-json frame naming that session; forget it only when `transcriptPersisted` answers false (unknown keeps it); an unreadable or invalid state file throws, never resets. Chat without a task ref goes to the instance's single `in_progress` task (`chatTask`), else to a `--no-session-persistence` one-off child.
122
124
  - A private envelope refuses the delivery before spawn; an item `admit` refuses is dropped without vetoing the others. Settle `failed_safe` until the child's stdin was released (`inputReleased`), `needs_review` after; a failed group cleanup keeps the handle and stays `busy`.
123
- - The child's tools-mode server (`AGENTHUB_PERMISSION_TOOL=1`) lists only `IMPLEMENTER_HUB_TOOL_NAMES`. `hub_permission_request` returns allow only for the person's allow-once; a malformed request, no hub, expiry or a changed child is deny. The original arguments stay in the child's request; the person sees only `permissionPayload` (`src/hub/permission-payload.ts`): the whole value withheld on any screen match, cut with `[cut, N chars]` inside 600 characters. Daemon admission of that child: `daemon.md`.
125
+ - The child's tools-mode server (`AGENTHUB_PERMISSION_TOOL=1`) lists only `IMPLEMENTER_HUB_TOOL_NAMES`. `hub_permission_request` returns the original input only for the person's allow-once. Automatic Bash grants require the #845 host wrapper, a current owned public task/root/branch, existing read-only or closed task-command proof, and wrapper protocol 1 on request and reply; never return an automatic allow for the original native command. Consume the expiring single-use ticket before rechecking and executing with the fixed host environment, absolute executable and proven cwd; changed proof/task/child/root/branch runs nothing. Require the local-worker mutating Seatbelt profile for check/test/build with an additive deny for all project Git metadata and the canonical hub state. Only closed host Git add/commit/push and gh stay outside it; refuse configured filter/external drivers/textconv for add/commit only (global Git LFS must not disable push/gh), retain the config-include/partial-clone refusal for privileged Git/gh, and disable signing/automatic maintenance. Pin default-branch authority from the remote, never mutable local origin/HEAD. Unknown or unavailable confinement asks. Enabled action classes and `ask` still ask; malformed requests, no hub, expiry or a changed child deny. The original arguments stay in the child's request; the person sees only `permissionPayload` (`src/hub/permission-payload.ts`): the whole value withheld on any screen match, cut with `[cut, N chars]` inside 600 characters. Daemon admission of that child: `daemon.md`.
124
126
 
125
127
  ## Always-ask (#636)
126
128
 
@@ -5,10 +5,11 @@
5
5
  - For explicitly user-authorized live verification, use the per-invocation CLI delegation in `docs/security.md` §Agent CLI identity and conductor authority; keep caller markers and record native evidence separately from fixtures.
6
6
  - Keep `export const LIVE_TEST_SUPPORTED = true;` in `src/cli/identity.ts`: delegated recovery staging reads this declaration before any source effect.
7
7
  - A delegated non-recovery command runs only after `recordDelegatedCliAudit` committed `delegated` against a hub whose `features` list `cli_audit_delegated`, that instance's console `cli_audit_ack` confirmed retention, and `ControlClient.pinInstance` pinned the invocation to it; a refused feature, failed ack or changed consumer executes nothing (#410). Upgrade, restart and recovery record delegation only in the operation receipt (`delegatedAudits` through `createOperation` or `recordRecoveryDelegation`). The daemon counts an audit retained only after `event()` returned true.
8
+ - Conductor operational refusals (#812, #839): throw `OperationalAuthorityError` (`src/hub/operational-authority.ts`) for an authority refusal, never a plain `Error`. Only it sets `authorityRefused: true` and its scope on the reply, which the control `reply` wrapper audits once as a refused `agent_cli` event; any other failure of a labelled request keeps its own error with `authorityRefused: false` and is never audited as refused, and the CLI's `fail` audits only when `authorityRefused` is not `false`. An unreachable or dead hub is "authorization unavailable", never a missing conductor role. `publicOperationalView` is cached on the board status revision, config revision, routing and the private-path policy: a new input to that projection joins the cache key.
8
9
 
9
10
  - Console attention navigation selects a delivery only by an exact visible row id. A missing id or row prints read-only guidance; never select by peer or fall back to the first Queue row.
10
11
 
11
- 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) or the instance modules (`src/hub/instances.ts`, `src/hub/instance-config.ts`, `src/hub/instance-execution.ts`), or before adding a native peer.
12
+ 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.
12
13
 
13
14
  - 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).
14
15
  - `peer_stop` is console-only and stops only a hub-owned headless ACP, Pi, local, print-reviewer or Claude implementer runtime through its adapter. Refuse native-terminal peers and recovery-held mutations; serialize with starts and permission changes, and refuse a recovery `prepare` while a stop runs. An admitted stop holds delivery through `BusOptions.deliveryHeld` before any teardown await (never a console pause that resume can undo, or a global recovery hold that blocks unrelated peers) and withdraws the peer's pending approvals before the adapter stops. A requested stop holds an in-flight delivery as `needs_review` with an ids-only stop reason, suppresses restart/escalation, and never resolves that delivery automatically.
@@ -19,6 +20,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
19
20
  - Dashboard snapshot `view` (#683 U4): the daemon's `ui_snapshot` and the manager's forward pass a request's `view` only through `dashboardView` (`DASHBOARD_VIEWS` in `src/hub/ui.ts`); keep that list equal to the page's `DASHBOARD_VIEWS` in `src/ui/index.html`. Any other value is dropped, never forwarded.
20
21
  - Dashboard Usage view (#683 U4): `uiSnapshot` builds `usage` only when the request's view is `agents`, never on a hidden view's poll. It folds with `catchUp(100)` and sets `behind: true` when that is not done, so a partial figure is never shown as the whole. A failed read answers `{ unavailable: true }`, never `undefined`, which the page reads as a hidden view and keeps the old figures. The snapshot carries task ids, a `pii` flag and numbers only, never a title; the page shows a private task as `#id [pii]`. Daily figures come only from `UsageRollup.daily` (rule: `budget.md`).
21
22
  - Dashboard Code group (#684 2c): `codeGroup` (`src/hub/task-code-group.ts`) is the only dashboard classifier of changed symbols against `plan.symbols`, called in `uiSnapshot` with the public `codeSymbols`, `tasks.publicMetadataText` and `tasks.isPii`; the page (`codeGroupSection`) draws it and never matches names. A private task gets no `codeGroup`. A stored receipt (one with `repositoryKey`), a missing or an unknown one is `state: "unknown"`. A malformed or screened-out plan name is counted in `planWithheld` and marks the group incomplete, never dropped. Lists stop at `CODE_GROUP_MAX` (40) with the rest in `more`, and `counts` are totals. The memory view reads a `node` of up to 512 characters only when it starts with `symbol:` (200 otherwise).
23
+ - Agent CLI drift (#843 AC3): the daemon's `sweepAgentDrift` (once after start, then `agentDriftEveryMs`, default one day, on its own timer apart from the task sweep) is the only caller of `noticeDrift` (`src/hub/agent-drift.ts`). It reads versions only through `agentVersionsAsync` (`src/cli/agents.ts`), which shares `probeTarget` and `probeRow` with the `ahub agents` probe: never add a second probe path, a network read or an install. The first probe is owed until one has run (`agentDriftOwed`): a daemon that starts inside a controlled recovery skips it and the 1 s tick pays it once `recoveryActive()` ends, so a restart or upgrade never waits a day for the first notice. `startDaemon` probes only when `agentDrift` is set (`lifecycle.ts` sets `true`; tests inject a function), so a test daemon never spawns a real CLI. Drift is an installed executable with a readable version whose state is not `verified`; a missing or unreadable one is silent. The notice carries id, version, state and the receipt path only, is marked per (id, version, planner) in `agent_drift_notices` before it is published to that planner alone (and forgotten if it cannot be queued, so only that planner is retried), and goes to `plannerPeers()` that are rostered; with none, nothing is recorded. `agentVersionsAsync` reads stdout as a stream with the 64 KiB cap enforced as it arrives (kill and cancel on overflow) and cancels the reader at its deadline and a short grace after the probe exits, so a descendant holding the pipe cannot hang it. Never put CLI output in it.
22
24
  - 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`.
23
25
  - `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.
24
26
  - 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.
@@ -66,7 +68,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
66
68
  - `ahub claude` (`src/cli/launch.ts`, #270) publishes `claude-launch.json` (`claude-launch.<id>.json` for an `--as` instance) for every launch against a running hub, atomically and without a lock: hooks, permission status, session observation and recovery read its `launchId`, so never make it wait for native-proof state. The native child's pid and signature go only into the private `claude-native-<binding hash>.json`, keyed by the exact launch record, written once under `claude-launch.lock` and removed at that child's own exit; never put them in `claude-launch.json`, where a late callback of an earlier launch could replace a newer record. Crash cleanup of a previous launch's settings file (`cleanupStaleClaudeSettings`) needs the launcher gone by `processLiveness` and that native proof, and for a `direct-child` proof the native pid absent from an available `processTable`: a direct child may exec in place, so its signature certifies nothing. A script or interpreter gets no proof and keeps its settings file.
67
69
  - Read a `claude-launch*.json` record in the daemon only through `readClaudeLaunchProof` (`src/hub/claude-launch-proof.ts`: no-follow, 16 KiB cap, this instance only), never by parsing the file. An `--as` launch records the `executionCwd` its entry got from `resolveExecutionCwd`; the entry never chooses or creates a worktree.
68
70
  - Terminal Claude implementer roots (#525-5): bind `executionRoots`/`executionOwners` only at a hello where the resolved worktree is not the project root, the private launch record names this hello's `launchId` and that root, and the channel's reported `cwd` resolves to it; bind it to that socket and session generation. `currentExecutionOwner` rechecks the record and the worktree in each event-loop turn (reuse only within synchronous code), and `revokeTerminalExecution` drops the proof on a mismatch, detach or new claim. Missing or unknown evidence keeps the channel attached but never grants isolation; the channel omits `cwd` when it cannot resolve its own.
69
- - 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 `permission_request` is `classed: true`, offers allow-once and deny only, and denies on expiry, abort or a changed child. Its facts get `permission` only for the current child's launch id, session id and this instance.
71
+ - 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.
70
72
 
71
73
  - 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.
72
74
  - 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.
@@ -97,6 +99,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
97
99
  - Bind adapter and instance declarations once at startup through the daemon-local `adapterCatalog` (`createProjectAdapterCatalog(loadedConfig.adapters, externalDefinitions)`, the definitions `loadExternalAdapterDefinitions` imported at startup; a reload passes the same `externalDefinitions` to `loadConfig` and imports nothing), its `definitionOf` closure and the `configuredIds`/`extraIds` catalogs; never publish them to the global registry or take them from a request. `adapters` and `instances` are restart-class, and `adapters` is `NEVER_EDITABLE` and in `ALL_MACHINE_LOCAL`. Settings rows come only from `projectSettings(declarations, adapterCatalog)`; a `locked` row (a read-only instance's permission) is refused by `checkSettingValue` and `settingRefusal` for every authority, and the console and dashboard never arm an edit or undo for it.
98
100
  - Configured ACP starts (`startPeerBody`): before effects refuse native launch overrides (model, route, backend, unattended, provider profile, session file, fresh) and an invalid session selector; answer `already` only for the same requested native session and never pass `qwen_cmd`; after a failed start keep the owner with `pendingOwnedProcess` on the bus, let `peer_stop` admit that offline owner and refuse starts until it is confirmed gone, reporting only a withheld-detail reason. In `taskOp`, refuse a read-only instance's op outside `READ_ONLY_HUB_TOOLS` before effects. Publish `configuredAdapters`, `startablePeers` and `readOnlyPeers` in status as bounded ids only. Keep controlled recovery refused (`session.recoveryRefusal`); crash resume passes only a bounded, control-free recorded `sessionId`, never a recorded model.
99
101
  - `validateInstances` admits `review_mode: "fresh"` only on a headless instance whose roles are exactly `["reviewer"]` and whose kind declares a fresh reviewer; a fresh reviewer refuses `worktree` and `permission_mode` (variant selection: `adapters.md`). `review.pool` and `review.rerotate` are validated by `reviewerPoolConfig` at startup and in the config schema: 1 to 16 distinct kinds from `REVIEW_POOL_KINDS`.
102
+ - Instance `model` and `review_scope` (#844, #852): `validateInstances` admits `model` outside Jcode only for a kind whose `headlessVariant` declares the fixed-builtin `instances.modelSelect` (the headless Claude implementer), only on a headless writer and only as one `NATIVE_WORD`; the daemon passes it to that peer's native turns (`claudeImplementerArgs`) and shows it as `modelSource`, and a change applies at the next daemon start. `review_scope` is admitted only on a fresh reviewer, as `code` or `full` (verdict rules: `tasks.md`). Both are `TIER3_INSTANCE_FIELDS` (`src/hub/config-tier.ts`), which `planConfigEdit` also reads to refuse a whole-instance replacement that would drop one: a new instance field that chooses a home, account or model, or widens what a verdict may skip, joins that list.
100
103
  - Channel-kind instances (#525 R1, `validateInstances`): a terminal instance declares roles within its kind's `instances.roles` (Claude: planner, implementer, verifier, reviewer). An `implementer` or `verifier` needs its own `worktree`; a planner or reviewer refuses one. A headless channel-kind instance is either a reviewer with roles exactly `["reviewer"]` or (#525-4) a writer whose roles include `implementer` and hold only `implementer` and `verifier`: it resolves to the kind's `headlessVariant`, needs its own `worktree` and accepts `permission_mode` `ask` or `ask-when-needed` only. `planner` needs `projection.plannerEligible`: `validateInstances` refuses it, `loadConductorPolicy` drops it with the ceiling notice, and `effectiveInstanceRoles` filters it for instance-keyed `roles`. A declared worktree never proves owner isolation (rule: `tasks.md`).
101
104
  - Every start resolves `resolveInstanceExecutionCwd` and passes that `executionCwd` as adapter cwd, sandbox, grant and Pi session root; set `executionOwners` only for the adapter actually started, or at a verified terminal Claude hello (rule above). Its git calls share one `GIT_BUDGET_MS` allowance through `gitWithin` (#225): a call that finds it spent refuses without starting git; never give each call its own timeout. Extra-instance native state is per instance, while its MCP binding keeps the canonical hub state dir.
102
105
  - Recovery: record an extra Codex launch row only after the start reply supplies `executionCwd`; keep `projectRoot`, `executionCwd` and `terminalRoot` separate and never bind an extra by kind metadata. Abort and archive validate receipts without current declarations; restore, fresh and reattach require them; a screen that only shows a recorded receipt reads it with `restoreReceiptStatus(value, { authority: "historical" })`, never for a decision. `instanceRecoverySourceRefusal` refuses before effects when the source lacks `external_adapter_modules` while `adapter_modules` is declared, and lacks `configured_acp` or `peer_instances` while a configured or declared id is not offline or the source's peer states are unreadable; a source whose such ids all read offline proceeds. Extra-instance turns write `turn_workspaces` in the `turns` transaction and leave the legacy snapshot fields unknown; undo requires the same trusted declaration and validated root.
@@ -3,9 +3,10 @@
3
3
  Scope: the hub-native local worker, its tools, path guard, denylist and seatbelt sandbox. Read before editing `src/adapters/local-worker.ts`, `src/local/`, `src/memory/capture.ts` (reads the denylist), `src/hub/facts.ts` (denylist and large-file stamps), `src/hub/git.ts`, the grant filter in `src/hub/permission-mode.ts`, or the sandbox root in `src/hub/instance-execution.ts`.
4
4
 
5
5
  - Nothing the local worker executes may run outside `sandboxedExec`; a new tool that spawns a process goes through it, and a tool that touches a path goes through `guardPath`.
6
+ - The headless Claude host wrapper (#845, `HostShellWrapper` in `src/hub/claude-shell-wrapper.ts`) runs its read-only and check/test/build grants under this same `profile()` plus a deny-only refinement (`src/hub/task-shell-sandbox.ts`, and the hub state dir): it repeats `sandboxedExec`'s own-temp allow suffix and its `readOnlyData` broad-write guard, so change both places together, and keep the refinement deny-only. The wrapper and the read-only shell proof take their fixed environment from `hostShellEnvironment` (`src/local/shell-environment.ts`), which must equal the baseline `sandboxedExec` builds inline (`test/claude-shell-wrapper.test.ts` compares them); never inherit a native agent's environment there.
6
7
  - A timeout or abort in `sandboxedExec` marks the result `(killed: <signal>, timeout?)` from its own `killRequested` flag, never only from the exit signal: a shell may report its killed child as a normal exit 137.
7
8
  - The sandbox denies home reads by default (toolchain dirs, the project and its real git dir excepted) and all network, loopback included: claude-mem and the Codex app-server listen on loopback without auth. Tests that bind a local port therefore fail when the worker runs them, also with `local.bash_network: true` (the egress proxy keeps loopback closed, and the proxy variables send a test's plain-HTTP `fetch` to the proxy, which refuses it); that is the intended trade-off, and `"direct"` is the switch until a later release removes it.
8
- - The seatbelt profile refuses the `.git` name itself at any depth (creation, rename, symlink, gitfile), `config` and `hooks` directly under it, `commondir` at any depth below it or below an external git dir, and `.agenthub` anywhere in the project, every name folded against the 16 code points HFS+ ignores (#270). A submodule's git dir under the common dir's `modules/` tree gets the same `config`/`hooks` refusal at the depth submodules nest, every term anchored below the discovered modules root so ancestor directories named `refs`/`config`/`hooks` neither disable nor trip the rule, and a `/refs/` or `/logs/` segment below it excludes the path so refs named `config` or `hooks` stay writable (#281); the discovered git dirs, the modules root and the components in between are also refused as directory-entry targets (rename, replace, symlink, recreate). Known residuals: a submodule whose own path holds a whole `refs` or `logs` segment (a substring, `catalogs` included, keeps the refusal; the exclusion matches whole components only), a nested repository in bare layout (the hub never runs git there; docs/security.md), folded spellings of the components between `.git` and a submodule's git dir on a volume that folds names (the 1024-byte SBPL literal cannot hold a folded path; external git dirs carry the same residual since #270), and the submodules of a repository nested inside the project (only the project's own common dir is discovered). An external git dir's `config`/`hooks` rules cover only its own children plus the hooks directory's contents: at any depth they would deny refs named `config` or `hooks` in a worktree or submodule project. The fold is a group alternation, never a character class (the seatbelt regex engine is byte-oriented and a quantifier after a multi-byte class member binds to its last byte), and every pattern keeps its `^` anchor (an unanchored starred alternation does not match). Folded literals never contain the project root: an SBPL string literal dies at 1024 bytes. The cost: `git init`, `git clone` and `git worktree add` do not run inside the sandbox, and neither do `git worktree remove`, `git worktree prune` of a stale record, `git clean -ffd` with a nested repository, `git submodule update --init` (its git dir's `config` write is refused), or `rm -rf` of a directory holding a `.git`.
9
+ - The seatbelt profile refuses the `.git` name itself at any depth (creation, rename, symlink, gitfile), `config`, `config.worktree` and `hooks` directly under it, `commondir` at any depth below it or below an external git dir, and `.agenthub` anywhere in the project, every name folded against the 16 code points HFS+ ignores (#270). Every `modules/` tree the project can reach (the common dir's, the worktree's own, every linked worktree's under `<common>/worktrees/<wt>/modules/` as a pattern so a tree or worktree that does not exist yet is covered, #518, and each discovered submodule's own `modules/`) refuses EVERY write that is not inside a discovered submodule git dir (#520): nothing is exempt by name, so a fake git dir called `refs`, `logs` or anything else cannot be planted for a later unsandboxed `git submodule update --init` to reuse. Inside each discovered git dir `D` the refusals are anchored to `D`: `D/config`, `D/config.worktree` (folded in both halves), `D/hooks` and what is below it, and `D/worktrees/*/config.worktree`, with `D/worktrees`, each `D/worktrees/<name>` and `D/hooks` refused as directory entries too, so none can be moved out, edited and put back; a submodule's own linked worktrees keep a `modules/` of their own at `D/worktrees/<name>/modules`, which gets the same everything-not-discovered refusal by pattern (so one that does not exist yet is covered) and has its `modules` entry refused as a target; `D/refs`, `D/logs`, `D/objects`, `D/index` and `HEAD` stay writable, so commit, branch, tag, gc and rebase work, and a submodule really named `refs` or `logs` is protected like any other. `hooks` is anchored by path depth (`hooksUnder`), never by an unanchored any-prefix name, which also matched a project under a directory named `hooks`. The worktrees container and each worktree git dir are refused as directory entries too (otherwise an ancestor can be moved out of the protected path, edited and moved back), and so are the discovered git dirs, the modules root and the components in between (rename, replace, symlink, recreate). Keep every regex literal under 1024 bytes (UTF-8 bytes, not characters), and never put the git path and a folded name in one literal: the root is its own `require-all` filter, so the folded literals are the same size at any path length (`test/sandbox.test.ts` asserts it). Known residuals: a nested repository in bare layout (the hub never runs git there; docs/security.md), folded spellings of the components between `.git` and a submodule's git dir on a volume that folds names (the 1024-byte SBPL literal cannot hold a folded path; external git dirs carry the same residual since #270), and the submodules of a repository nested inside the project (only the project's own common dir is discovered). An external git dir's `config`/`config.worktree`/`hooks` rules, and the pattern rules for every linked worktree git dir under `<common>/worktrees/`, cover only its own children plus the hooks directory's contents: at any depth they would deny refs named `config` or `hooks` in a worktree or submodule project. The fold is a group alternation, never a character class (the seatbelt regex engine is byte-oriented and a quantifier after a multi-byte class member binds to its last byte), and every pattern keeps its `^` anchor (an unanchored starred alternation does not match). Folded literals never contain the project root: an SBPL string literal dies at 1024 bytes. The cost: `git init`, `git clone` and `git worktree add` do not run inside the sandbox, and neither do `git worktree remove`, `git worktree prune` of a stale record, `git clean -ffd` with a nested repository, `git submodule update --init` (its git dir's `config` write is refused), or `rm -rf` of a directory holding a `.git`.
9
10
  - Build every git invocation outside the sandbox through the single implementation exported by exact-pinned `@staix/agent-mem/platform` (`memoryGitArgv`/`memoryGitEnv`/`memoryGitSync`), re-exported as `hubGitArgv`/`hubGitEnv`/`hubGitSync` by `src/hub/git.ts`. Keep the git export identity, behavior and direct-spawn tripwire checks in `test/hub-git.test.ts` passing; retain home compatibility and Node-safe process-entry checks in `test/memory-parity.test.ts`. The sandboxed git tool in `src/local/tools.ts` retains its own behavior. Use `{ credentials: true }` only for the memory repository the hub creates itself, never a project's repository. Use the core deny policy re-exported by `src/local/deny.ts` for guards, sandbox rules and capture; do not add another denylist.
10
11
  - SBPL strings go through `sbplString` (the plain `"..."` form). In the raw `#"..."` form a backslash escapes nothing, so a `"` in a path ends the literal and the whole profile fails to parse.
11
12
  - Seatbelt sees absolute paths, so `local.deny` entries are anchored under the project root.
@@ -1,6 +1,6 @@
1
1
  # Task board and hub tools
2
2
 
3
- Scope: the task flow, assignment, the state machine and the hub's MCP tools. Read before editing `src/hub/tasks.ts`, `src/hub/board.ts`, `src/hub/routing.ts`, `src/hub/task-sweep.ts`, `src/hub/settled-roles.ts`, `src/hub/conductor.ts`, `src/hub/supervision.ts`, `src/hub/tool-refusal.ts`, `src/hub/hub-tools.ts`, `src/hub/review-brief.ts`, `src/hub/reviewer-pool.ts`, `src/hub/reviewer-pool-config.ts`, `src/hub/release-admission.ts`, `src/hub/main-checkout.ts`, `src/hub/checks.ts`, `src/hub/task-repository.ts`, `src/hub/review-findings.ts`, `src/hub/plan-conformance.ts`, `src/hub/sdlc-metrics.ts`, `src/hub/no-writes.ts`, `src/hub/workflow.ts`, `src/hub/deliverables.ts` or `src/hub/deliverable-files.ts`.
3
+ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Read before editing `src/hub/tasks.ts`, `src/hub/board.ts`, `src/hub/routing.ts`, `src/hub/task-sweep.ts`, `src/hub/settled-roles.ts`, `src/hub/conductor.ts`, `src/hub/supervision.ts`, `src/hub/tool-refusal.ts`, `src/hub/hub-tools.ts`, `src/hub/review-brief.ts`, `src/hub/reviewer-pool.ts`, `src/hub/reviewer-pool-config.ts`, `src/hub/release-admission.ts`, `src/hub/main-checkout.ts`, `src/hub/checks.ts`, `src/hub/task-repository.ts`, `src/hub/review-findings.ts`, `src/hub/review-scope.ts`, `src/hub/plan-conformance.ts`, `src/hub/sdlc-metrics.ts`, `src/hub/sdlc-merge.ts`, `src/hub/sdlc-mirror.ts`, `src/hub/no-writes.ts`, `src/hub/workflow.ts`, `src/hub/deliverables.ts` or `src/hub/deliverable-files.ts`.
4
4
 
5
5
  - Scope every task-place comparison through `TaskRepositories` (`src/hub/task-repository.ts`, #476): explicit `refs.repo`, else a common outside-absolute-ref repository or parent, else this project. Preserve relative display spelling within that key and gate overlap, contradiction, replay and held evidence by equal keys. Warm/cache repository identity at task mutation and startup; never repeat Git reads for unchanged overlap inputs. Set `refs.repo` for work in another repository; repo-only accept metadata never grants execution or file access.
6
6
  - Tool callers are models: MCP `inputSchema` is not enforced on the way in. Normalize at the boundary (`cleanRefs`) before anything reaches the board, and never throw after a board write: a publish or notify that follows a `review()` or `done()` board write goes through the one `releaseNotice` guard, which turns a failed publish into a console notice, drops a failed notify and never retries (#231, #243). An assignment's delivery once its board write is saved (#276, #297): the move is fact, the failed publish is not retried in place (the journal's own recovery covers a storage error, and a retry may duplicate a publish that actually left), and a person looks for a queued or needs_review delivery with `ahub queue list --peer <owner>` (then `ahub queue show <id>`, or the accepted entry in `ahub task show <id>` if the journal is down) before re-sending with `ahub task assign <id> <owner>`. The distinction is made once, in `assignOwner`: a failure after the write prints the one console notice from `undeliveredNotice` right there, once, and throws `AssignmentUndeliveredError` carrying the saved task; callers (`review()`'s repeated rejection, `escalate` and its callers, the ready offer, both relays, the idle sweep, rescreen) decide control flow by the type and print nothing more, and `propose`, `assignTo` and `decline` additionally let the error reach the caller in its turn, which is a person or peer that can act. Two exceptions get no notice, by construction: a row superseded meanwhile, meaning a later history entry that moves or settles the task (another assignment, an unassign, the owner's accept, done, approval, close), while entries that do neither (the hub's quiet bookkeeping: answers, reviewer changes, idle sweeps, check results) leave the assignment undelivered and the notice stands, so the original error goes to the caller as before only on supersession; and a claim's own accepted write and "claimed by" line stay outside the delivery rule (a claim is its own hand-over; its overlap and routing announcements to earlier owners are deliveries and share the wrapper, so a failure there does read as one). A failure before the write stays a plain `Error` (#254), a failed write leaves the ready offer unconsumed (#231), and the notices before the write (waits, no-owner, the reserved-owner line) go through `releaseNotice` too, so those pre-write notices cannot fail their caller.
@@ -18,6 +18,7 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
18
18
  - Readiness never puts a reserved owner over its planner WIP cap (#655). `offerReady` asks `reservedHold` (the same `wip()` plus `pendingClaims` against `wipLimit()` that `acceptanceCapacity` refuses at) before the `ready` write is offered; a hold records `ready` and one quiet `ready held` entry (in `QUIET_EVENTS`, never a stall, never an owner) and keeps the reservation. Offer dependents and held work through `readyOrder` (priority, rank, id), and free a slot through `releaseDependents` (approval), `releaseHeld` (cancel, untouched return) or the release timer; never offer a held task past the cap, and never let the idle sweep's auto-reassign override `reservedHold`. `offerReady` records its slot in `readyInFlight` in the same synchronous step as the check and frees it in `finally`, so an await in `assignOwner` never lets two offers share the last slot; `releaseReady` treats `isHeld` work as eligible even after a `planned` entry. Planner-pulled work and an excluded reservation are not held. The hold text carries peer ids and counts only.
19
19
  - Capture the coordination preference once per task at creation (`coordinationModes` from `TasksDeps.coordination`, in `propose` and for each split child, #439). `formCohort` joins only overlaps with the same captured mode and forms a silent cohort only for a turn-free one; a reload never silences an existing task or lifts an existing silent cohort. A daemon restart re-pins retained rows to the startup mode (a compatibility ceiling, not task metadata). `configureSweep` takes effect at the next pass: a running `sweep` keeps the config it started with.
20
20
  - The planner "board changed" notice (#758, only with the lifecycle on) is built from what was added to a card's history since the last scan (`Tasks.groomNotices`, `groomScan`), never from the status revision alone: skip the peer whose own entry it was (`entry.by`), tell only the card's planners (`taskPlanner`, #525-2), send a planning revision or dependency change (a `planning` entry, or `deps` that differ from the last scan whatever wrote them: a propose with `after`, a split) and a card's creation (a peer's propose, a split child, a generated card) at task weight, since new work and its dependencies are what a plan review consolidates, and replace an unsettled copy (`sendGroomNotice` withdraws it by the `The board changed` prefix) so a planner meets the latest revision once, and send state moves, reviews and assignments at `fyi` weight (recorded, no turn), at most once per planner per `GROOM_FYI_WINDOW_MS`. The hub's own bookkeeping entries (`by` is `hub` with no `planning`: lifecycle links, idle sweeps, holds, plan observations) tell nobody and spend no window; a deps change with no new entry has no known author, so no planner is skipped for it. Scan every configured planner, not only the attached ones, so a change made while one was offline is kept for it. The first notice a planner gets after a hub start is task weight. Keep the scan out of the status revision and never read history that is not new.
21
+ - Planner work-ready notices (#785 P1a, `writePlannerNotices`): a write that can free a reserved owner's WIP or release backlog (done, approval, decline, cancel, backlog return or pull, untouched return, pull undo) takes `capacityBefore` (the `wip()` of the task's owner and reservation) before its board write and calls `writePlannerNotices` after it. Each rostered planner of the task (`taskPlanner`) gets one ids-only `task` envelope starting `Planner work ready:`: `capacity_released: <peer> WIP <n>; reserved ready #<id>|none` for a peer whose WIP dropped, and, when the write approved the task, `backlog_ready: #<id>` for each backlog dependent with nothing left to wait for. Merge the lines of that planner's unsettled copies, read back from `bus.pendingEnvelopes` (they survive a restart; never keep them in memory), withdraw them, then publish, so at most one stays queued per planner; all of it runs inside `releaseNotice`. The notice only wakes the planner: never pull, reserve or assign from it, and never put a title or note in it.
21
22
 
22
23
  - Completed-change notices (#713, one per recipient per done, accepted work only, superseding by `completedFor`): rule in `bus.md`.
23
24
  - Plan-symbol overlap (#682 1b) compares through `symbolsOverlap` (`src/hub/symbol-normalizer.ts`): arguments stripped, `section `/`source ` keys exact, two qualified names equal, otherwise last segments equal. `places`, `overlapHits`, `contradict`, the completed-notice replay, `plannerGrooming` and `planSymbolMatches` all use it; never compare plan symbols with `includes` or set membership.
@@ -62,6 +63,7 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
62
63
  - Cost reports recheck planner/private-turn authority and current linked privacy before each underlying external read and final projection. Do not emit a stale/relinked observation as a complete zero-cost row. S7 owns band configuration and automatic proposal writes.
63
64
 
64
65
  - `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).
66
+ - 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.
65
67
  - 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.
66
68
  - 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`.
67
69
  - 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.
@@ -84,6 +86,7 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
84
86
 
85
87
  - 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 check, 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.
86
88
  - Owned implementer child (#747): `propose`, `acceptReviewed`, `done` (through `complete()`) 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.
89
+ - 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.
87
90
  - 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.
88
91
 
89
92
  - 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.
@@ -1,9 +1,10 @@
1
1
  # Tests
2
2
 
3
- Scope: tests, fakes and the test gate. Read before adding or changing a test or fake under `test/` or `src/adapters/conformance/`, or editing `scripts/check.sh`, `scripts/hang-watch.sh`, `scripts/seeded-check.ts`, `scripts/seeds.json`, `scripts/lib/ts-ast.ts`, `scripts/changed-tests.mjs`, `scripts/check-closing-refs.mjs` or `.github/workflows/closing-refs.yml`.
3
+ Scope: tests, fakes and the test gate. Read before adding or changing a test or fake under `test/` or `src/adapters/conformance/`, or editing `scripts/check.sh`, `scripts/hang-watch.sh`, `scripts/progress-tee.sh`, `scripts/seeded-check.ts`, `scripts/seeds.json`, `scripts/lib/ts-ast.ts`, `scripts/changed-tests.mjs`, `scripts/check-closing-refs.mjs` or `.github/workflows/closing-refs.yml`.
4
4
 
5
5
  - A fake's gated action that never sees its gate skips the action; never fall through to a late, unsynchronized action. Owned-group tests inject `AcpOwnedGroupPorts`; a test never signals a process group it did not spawn.
6
6
  - When extracting production closures into a VM fixture, bind every referenced host port and actual imported policy helper. Keep native identity checks on real unstarted instances; never replace a policy predicate with an always-true stub or weaken existing assertions to accommodate a moved dependency.
7
+ - A fixture that walks a policy table (a `COMPAT` entry list, a registry) derives its per-entry values from the table, never a fixed list of keys or versions a planned entry is not in: the compat watch test builds every served version and the ahead/unknown row from `COMPAT[id]`, so an added entry (`pi`, #843 AC2) is covered without editing the test.
7
8
  - Tests that are not about batching build the bus with `batchMs: 0`; with the default 15 s window a lone status envelope looks like a lost message.
8
9
  - A test timeout that fires while `Bun.spawnSync` runs can start the next test inside spawnSync's own event loop (Bun 1.3.14 and 1.4.2; background: README). `scripts/check.sh` runs tests with a 20 s timeout and under `scripts/hang-watch.sh`; a test that spawns and reads the process table gets a timeout of its own, never above 20 s: an explicit larger timeout overrides the gate's `--timeout 20000`, so shorten the scenario instead.
9
10
  - Sandboxed commands and the MLX server get a fixed environment (`PATH`, `HOME` and their own keys), so test knobs for their fakes travel in a wrapper script or argv, not in `process.env`; native peer children inherit the hub's environment minus the agent markers (`peerChildEnv`).
@@ -22,7 +23,7 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
22
23
 
23
24
  - `scripts/check.sh` pins the test environment (#610): `GIT_CONFIG_GLOBAL=/dev/null`, `NO_COLOR` unset, `TERM=xterm-256color`, temp root under the OS per-user temp dir, agent markers cleared. A test that needs a person's git config or color settings sets them itself. `--changed` selects tests with `scripts/changed-tests.mjs`: changed tests (nested suites included) plus every test that names a changed file directly; docs need none; any other changed file no test names, and any gate, package or shared-fixture change, runs everything.
24
25
  - `scripts/check.sh` holds the repo's heavy-job slot (`<git common dir>/ahub-heavy.lock`, #398) for the whole run, so a test never runs the real gate; `test/check-lock.test.ts` runs a copy in a scratch repo.
25
- - The whole-suite watchdog is `AHUB_CHECK_HANG_S` (default in `scripts/check.sh`, hosted values for both OSes in `.github/workflows/check.yml`; #378, #593): raise it only when the suite was still progressing at the cutoff, never for a hung test, and keep the per-test 20 s timeout. The check job's `timeout-minutes` must leave room above that value for setup: raise them together.
26
+ - The whole-suite watchdog is `scripts/hang-watch.sh PID SECONDS [PROGRESS_FILE QUIET_SECONDS]` (#115, #516). `scripts/check.sh` sends bun's stderr (where it prints results) through a fifo to `scripts/progress-tee.sh FILE`, which forwards every line unchanged and appends one byte to FILE per `(pass)`, `(fail)`, `(skip)` or `(todo)` line and per test file header; the watchdog stops the suite when FILE has not grown for `AHUB_CHECK_QUIET_S` (default 300, set in `.github/workflows/check.yml`, above the 120 s of the longest explicit test timeout). That is the hang detector, so a slow suite that keeps passing tests is never cut, and a test that logs without finishing is not progress. `AHUB_CHECK_HANG_S` (default in `scripts/check.sh`, hosted values for both OSes in `.github/workflows/check.yml`; #378, #593) is only the backstop for a run that keeps printing results while hung: raise it, with the job's `timeout-minutes` (room above it for setup), only when a healthy run nears it, never for a hung test, and keep the per-test 20 s timeout. The two-argument form (no file) is the old total-time bound, still used by the CI timing job and `scripts/seeded-check.ts`. A fifo and a filter of its own, never a pipe (`$!` must stay bun's pid) and never a process substitution (it cannot be waited on with a bound); `scripts/progress-tee.sh` is in the `--changed` FULL list. `test/hang-watch.test.ts` covers the watchdog, the filter and the fifo wiring.
26
27
  - A seed's named test must run on Linux, where the seeded job runs: a darwin-only test reports no result there and fails the job (#382). Cover a macOS-only protection with its regression test instead.
27
28
 
28
29
  - The `closing-refs` workflow (`.github/workflows/closing-refs.yml`, #760) runs `scripts/check-closing-refs.mjs` (PR number from argv or `PR_NUMBER`) on `opened`, `edited`, `reopened` and `synchronize`: it reads `gh pr view --json closingIssuesReferences,body,title,commits` and fails when GitHub would close an issue the title and body only mention with `Refs`, a negation or a qualifier (`Refs #15. Does not close #15.` and `Refs #15. Partially fixes #15.` fail, `Refs #15.` and `Fixes #15` pass), or when a commit message has a closing keyword, negated or not, for an issue the title and body do not mean (a squash merge carries it). An issue linked only in the sidebar passes. A keyword is meant only when nothing earlier in its sentence (ending at `.`, `!`, `?`, `;` or a line break, never a comma) matches `NEGATION`; widen that list, never narrow it to let a qualified close pass (background: README). `mentioned`, `keywordReferences`, `intentionalCloses`, `closingMisuse` and `commitMisuse` are pure and covered by `test/closing-refs.test.ts` with the real `gh` shape (`repository.name`, `repository.owner.login`); its command test puts a fake `gh` first on `PATH`, never the real one. Run it by hand with `GITHUB_REPOSITORY=<owner/repo> node scripts/check-closing-refs.mjs <number>`.
@@ -33,8 +34,10 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
33
34
  - Release verification requires both OS checks and the aggregate `seeded guards` success for an identical tree, a tag matching `package.json` on `origin/main`, and fresh ancestor-bound documentation stamps. Preserve the separate npm OIDC publish job and GitHub Release write job; neither job gets the other's credential authority.
34
35
  - `test/conformance/` runs real adapters against fake transports. The shared kit and the transport fakes live in `src/adapters/conformance/` (public `@staix/agent-hub/conformance`); `test/conformance/kit.ts` and the matching `test/fakes/*.ts` only re-export them, so change the packaged file, and `--changed` runs everything for it; `test/conformance/enrollment.ts` enrolls every `ADAPTERS` definition, each `:reviewer` variant and each `:implementer` (`headlessVariant`) variant. Record a case that does not apply in its `UNSUPPORTED` or `UNSUPPORTED_LEGS` map with a concrete reason naming the dedicated coverage, never `test.skip` or synthetic success, and never change native production behavior to satisfy a generic case.
35
36
 
36
- - Keep the CI admission lock around the whole controller workflow, not just its decision job, in its per-PR (`check-pr-<number>`, `queue: single`: a newer pending run replaces the older) or main (`check-main`, `queue: max`) group, never with `cancel-in-progress: true` beside `queue: max` (GitHub refuses it) (#609); two groups admitting one tree at once is an accepted duplicate run, never a reason to fail closed on another tree. Preserve successful identical-tree full proof and required job names; a skipped job, pending controller or unavailable active inventory is never a successful full gate. In `readFullGateAdmission`, lost successful proof (404, malformed proof, a per-read timeout) only means a fresh gate runs; unknown active coverage and the global read ceilings (`GateReadLimitError`) stay unknown and refuse. CI checks out the PR head (`pull_request.head.sha`), not a merge with main: after a fix lands on main, merge main into the branch before reading CI as proof. CI reuse tests use fixed metadata fixtures and never dispatch real duplicate workflows.
37
+ - Keep the CI admission lock around the whole controller workflow, not just its decision job, in its per-PR (`check-pr-<number>`, `queue: single`: a newer pending run replaces the older) or main (`check-main`, `queue: max`) group, never with `cancel-in-progress: true` beside `queue: max` (GitHub refuses it) (#609); two groups admitting one tree at once is an accepted duplicate run, never a reason to fail closed on another tree. Preserve successful identical-tree full proof and required job names; a skipped job, pending controller or unavailable active inventory is never a successful full gate. In `readFullGateAdmission`, lost successful proof (404, malformed proof, a per-read timeout) only means a fresh gate runs; unknown active coverage and the global read ceilings (`GateReadLimitError`) stay unknown and refuse. A GitHub 429 or 5xx on one read is retried at most twice after a short backoff (#872), only while the shared deadline leaves room for the backoff, and every retry counts against the 240-read budget; never extend retries to any other status, a timeout or malformed data. CI checks out the PR head (`pull_request.head.sha`), not a merge with main: after a fix lands on main, merge main into the branch before reading CI as proof. CI reuse tests use fixed metadata fixtures and never dispatch real duplicate workflows.
37
38
 
38
39
  - Headless-reviewer tests run `test/fakes/fake-claude-print.ts` and headless Claude implementer tests run `test/fakes/fake-claude-implementer.ts` through `command` or a PATH stand-in (the implementer with `HOME` and `CLAUDE_CONFIG_DIR` in a temp dir), and fresh Codex reviewer tests run `test/fakes/fake-codex-reviewer.ts` through `codex_bin`; a test that reaches the daemon's reviewer or implementer start must never run the real `claude` or `codex`. The fake's sandbox markers test host admission only, never native isolation. Fresh Kimi reviewer tests set and restore `KIMI_CODE_HOME` to a fixture native home (the backend links that home's `credentials/`, `oauth/` and `device_id`, else the person's `~/.kimi-code`) and run `test/fakes/fake-kimi-reviewer.ts` through `command` or a PATH stand-in, never the real `kimi`. Qwen TUI tests run `test/fakes/qwen-dual.ts` (scripted by submit text) as the launched Qwen or the daemon's `qwen_cmd` (an adapter unit test also passes its own `table`), never the real `qwen`. agy tests put a fake `agy` first on PATH and set `HOME` to a temp dir before any transcript readback (`readAgyTranscript` reads `$HOME/.gemini`). Code-facts tests build `new CodeFacts({ env })` with a PATH whose first `ripwire` is a wrapper around `test/fakes/fake-ripwire.ts` (`FAKE_RIPWIRE_MODE` selects its failures), or inject Tasks' `prepareReviewBrief`; never rely on an installed ripwire, which CI does not have.
40
+ - A Pi fake the daemon starts through `createPi` answers `--version` with a version `COMPAT.pi` verifies, as `src/adapters/conformance/fakes/pi-rpc.ts` and `test/fakes/pi-data-policy.ts` do: the factory's preflight (`piVersionAsync`, #847) refuses the start otherwise.
41
+ - A daemon test that declares a headless Claude implementer in a repository whose `origin` is on GitHub passes `shellDefaultBranchProbe` (`DaemonOptions`, #845), or the start runs `git ls-remote` against github.com; a test of `readTaskShellDefaultBranch` itself puts a fake `git` first on the `PATH` it passes.
39
42
  - Production `MemoryClient` (`src/memory/client.ts`) is the in-process amem store and never opens `memory.worker_url` (#752). A test that needs the legacy worker shape injects `FakeMemoryClient` (`test/fakes/mem-worker.ts`) as `DaemonOptions.memoryClient` or in place of the client; never add an HTTP path to the production client for a test. An amem-backed test sets its own temp `AMEM_HOME` and reads only its own fixture memory directory.
40
43
  - CLI print guards (#814, `test/cli-json-guard.test.ts`): two scans over every file in `src/cli`. One finds `console.log(JSON.stringify(...))` and passes only behind a `--json` branch (`selectsJson`), a named `DATA_FORMAT` or `NOT_YET_CONVERTED` entry. The other finds a print of a hub tool's text (`console.log(await taskOp(<op>))`, or a variable bound from one) and passes only behind `--json`, for a named `SENTENCE_OPS` op (checked against the daemon's case), or named debt (`TEXT_DEBT`, `CONDUCTOR_DEBT`, `REFUSAL_FALLBACK`). A list entry with no matching site fails, so the lists only shrink. Add a command's `--json` branch or renderer; never add a list entry to make a new print pass. Per-command output tests (`test/task-output.test.ts`, `test/sdlc-output.test.ts`) run the real CLI against a fake control server and assert default not JSON, `--json` byte equality, 80 and 200 columns and `--color=never`.
package/docs/dogfood.md CHANGED
@@ -14,7 +14,10 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
14
14
  | Compact board (#724) | on | `ahub board` at 80, 120 and 200 columns: one line per task (test/board-compact.test.ts) |
15
15
  | Backlog list table (#399) | on | `ahub backlog list` at a terminal width: one line per card by rank with the reserved owner and dependencies, and the count by owner (test/backlog-list-output.test.ts); `--json` for scripts |
16
16
  | Native branch ratchet and fixed builtin proof gate (#625 slice 2a) | active in source/CI after merge; running daemon gate takes effect after release and upgrade | reviewed predicate/function/count/reason rows equal source; foreign native proof products and nativeRevert declarations refused; reserved ACP ports leg remains unverified until 2b |
17
- | Remote isolated check transport (#793) | off here; fake checks and a bounded local Docker transport receipt, dependency preparation and person opt-in/SSH/live receipt follow separately | remote tests capability unavailable on failed SSH/Docker/image/transfer probe and while dependencies are unprepared; exact Git receipt, non-root hardened argv, zero Mac dependency copies and confirmed Docker cleanup before lease release; Darwin tar ignores PATH shims, probe warnings do not pollute JSON, bounded probes do not become unknown task-run orphans |
17
+ | Review scope for sandboxed reviewers (#852) | on after the release for the fresh Codex and Kimi reviewers (default `code`); a person may set `review_scope` on an instance in `config.local.json` and restart the hub daemon | `ahub status` shows `review scope` on each reviewer row; a code reviewer's review request carries the one fixed line and no check or CI line (test/review-scope.test.ts); its approval history entry carries `reviewScope: code`; a `changes_requested` with no open finding is recorded `evidenceOnly`, adds nothing to `rejections`, and the conductor gets one ids-only notice (count the `evidenceOnly` history entries against the 7 verification-only rejections seen on #467, #512 and #510, and watch that no task escalates on them) |
18
+ | Headless Claude host shell grants (#845) | source/fixture work; active only after release and hub/plugin upgrade with `ask-when-needed` on the declared implementer | automatic wrapper grants versus person requests; native poisoned-environment and replay receipt; unknown/expired/reused/stale task, branch, root or child tickets execute zero commands |
19
+ | Optional model for a headless Claude implementer (#844) | on after the release and once the person sets `model` on `instances.claude-impl` in `config.local.json` (a hub daemon restart applies it); unset it changes nothing | `ahub status` shows `model source` on the instance row; the native argv carries `--model <value>` on every turn and none when unset (test/claude-implementer.test.ts, test/claude-implementer-daemon.test.ts); a tracked-file or non-implementer declaration is refused or ignored with a reason (test/instances.test.ts, test/jcode-instances.test.ts) |
20
+ | Remote isolated check transport (#793) | off here; fake checks, a bounded local Docker transport receipt and a local run of the lockfile-keyed dependency volume; person opt-in, SSH and the live receipt follow separately | remote tests capability unavailable on failed SSH/Docker/image/transfer probe; one networked install per new lockfile key, none on later runs, the check mounting the volume read-only with no network; exact Git receipt, non-root hardened argv, zero Mac dependency copies and confirmed Docker cleanup before lease release; Darwin tar ignores PATH shims, probe warnings do not pollute JSON, bounded probes do not become unknown task-run orphans |
18
21
  | Task board, routing, roles | on (claude planner, reviewer and implementer; codex, jcode, agy implementers) | board history: lead time from propose to approve, review rounds, escalations |
19
22
  | Readable `sdlc` output by default (#814 T1) | on after the release: `ahub sdlc status\|explain\|queue\|release\|metrics\|costs\|authorize-release` print tables and labelled fields here; scripts pass `--json` | the person reading `ahub sdlc status` and `sdlc metrics` without `--json`; `test/cli-json-guard.test.ts` lists the CLI paths that still print JSON by default (target: only named data formats) |
20
23
  | Readable restart, upgrade, recovery and model output by default (#814 T3) | on after the release: `ahub restart --dry-run`, `upgrade --dry-run`, `recovery status`, `models start`, `queue resolve` and `budget execution configure\|disable` print text here; scripts pass `--json` | the person reading a dry-run plan and a recovery status without `--json`; `test/cli-json-guard.test.ts` lists the CLI paths that still print JSON by default (target: only named data formats) |
@@ -53,6 +56,7 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
53
56
  | Lifecycle identity linked by the hub (#694, #723) | on in advise; links at propose and accept from the title and `refs.repo`, with a groom backfill as the safety net | open tasks still showing `issue identity missing` right after propose (should be only titles that name no issue or a qualified title without `refs.repo`) |
54
57
  | Planner plan review (#481 S9) | advise | plans reviewed before build |
55
58
  | Planner board-changed notice (#758) | on here, with the lifecycle in `advise`: `claude` is the planner, so a card another peer proposes or re-plans reaches it once at task weight, and its own writes, state moves, reviews and assignments cost it no turn | per planner and week: `task`-kind hub envelopes whose body starts `The board changed` (target: one per planning change, none after the planner's own write), the same envelopes at `fyi` priority (recorded, no turn: at most one per 10 minutes), and the count still queued at once (target: at most one). Read from `ahub log` and the event log; the 12 copies in 44 minutes seen on 2026-10-24 are the baseline |
59
+ | Planner capacity and dependency notices (#785 P1a) | source/fixture coverage; active after release and hub upgrade | write-triggered `capacity_released` and `backlog_ready` lines in `Planner work ready:` task envelopes; at most one queued envelope per effective planner, peer/WIP/card ids only; no automatic pull |
56
60
  | Plan conformance (#481 S2) | advise | PRs with files outside the plan |
57
61
  | Test integrity for bug fixes (#481 S4) | advise | bug PRs without a red-first receipt, modified pre-existing tests |
58
62
  | Required checks | advise (`check (ubuntu-latest)`, `check (macos-latest)`, `seeded guards`) | first-pass CI rate, same-head reruns (`hub_sdlc_costs`) |
@@ -82,6 +86,8 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
82
86
  | Permission modes | on, person-chosen | approval prompts and their wait times |
83
87
  | Pi/local read-only shell class (#816 T1) | source/fixture coverage; person setting and live Pi receipt (AC4) remain unverified | automatic read-only class versus approval requests, parser/allowlist/proof revalidation tests, unchanged Seatbelt/path-guard byte checks |
84
88
  | Pi and the local worker on DGX | off: DGX unreachable recently; Pi is off the roster | turns served on-prem, relay errors |
89
+ | Pi native version gate (#843 AC2) | source/fixture and isolated model-free 1.4.0 receipt; active after release and hub upgrade, Pi stays off this repository's roster | `ahub agents` pi verdict, one version probe on each real launch, rejected versions naming the exact trust command; account-backed inference remains unverified |
90
+ | Local agent CLI drift notice (#843 AC3) | source/fixture and injected-probe daemon test; active on this repository after release and hub upgrade | one planner notice per (id, version), the `agent CLI drift` line in `ahub status`, no repeat after a hub restart |
85
91
  | PII routing | on | PII tasks and where they ran |
86
92
  | Multiple Jcode instances (#685) | pending | turns per instance |
87
93
 
@@ -93,7 +99,7 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
93
99
  | 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 |
94
100
  | 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) |
95
101
  | 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 |
96
- | Conductor operational CLI (#812 T2) | source/fixture coverage; active after release | peer-attributed agent_cli admissions, conductor/non-conductor command matrix, public projection and authority-revocation checks |
102
+ | Conductor operational CLI (#812 T2) | source/fixture coverage; active after release | peer-attributed effect admissions, authority/domain-error separation, delayed-handshake and incomplete-output CLI tests, cached privacy projections and revocation checks |
97
103
  | Review request CI evidence (#824) | on after release: each review request carries one `Hub-verified at <head>` line from the admission it already read | review requests to a review-only reviewer; the count of reviews that still list test execution or CI evidence as unmet (should fall); no new gh reads |
98
104
  | Verdict mirror (#786 T3) | `auto_merge = "dry-run"` first (same setting as the guarded merge): the dry-run notice names the one comment review it would post; `on` after one dry-run notice matches a verdict | one `agent-hub verdict:` comment review per (task, head, verdict) on each PR; none at a stale head; the headline alone for a private task |
99
105
  | Guarded merge and result recording (#786 T1/T2) | `auto_merge = "dry-run"` first in this repository's `.agenthub/sdlc.toml`; `on` after one dry-run report matches a manual merge | `ahub sdlc merge <task> --dry-run` reports the decision and the argv it would run; merges the hub made against merges a person or reviewer made by hand; host merge/cleanup records and owner-notice status; fake-gh ordering/idempotency proof, no live cleanup claim |
@@ -104,7 +110,7 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
104
110
  | Settings reload | on | reloads applied, pending restarts |
105
111
  | Config editing from the console and dashboard (#689) | on after release; `ahub settings edit` and console key `c` run here, the dashboard Structure group needs `ahub ui --settings`; executable keys stay console only | `config_edit` events (edits applied by tier and source), refusals shown with the loader reason, restarts left pending |
106
112
  | Upgrade and recovery | on, per release | upgrade receipts in `docs/verification/` |
107
- | Agent CLI version policy and trust (#691) | on after the release: Codex 0.163.0 verified by the [2026-10-22 receipt](verification/2026-10-22-codex-reviewer-isolation.md) and 0.164.0 by the [2026-10-24 receipt](verification/2026-10-24-codex-0164-reviewer-isolation.md); Kimi 2.3.0 and Qwen 0.26.0 verified by the [model-free receipt](verification/2026-10-22-kimi-qwen-compat.md), Kimi 2.3.1 by the [2026-10-25 model-free receipt](verification/2026-10-25-kimi-231-reviewer-compat.md), with account/provider live legs unverified; agy 1.4.4 in-line | model-free isolation receipts, `ahub agents` verdicts, versions trusted per machine, lead time from an `agent-compat` issue to a receipt |
113
+ | Agent CLI version policy and trust (#691) | on after the release: Codex 0.163.0 verified by the [2026-10-22 receipt](verification/2026-10-22-codex-reviewer-isolation.md) and 0.164.0 by the [2026-10-24 receipt](verification/2026-10-24-codex-0164-reviewer-isolation.md); Kimi 2.3.0 and Qwen 0.26.0 verified by the [model-free receipt](verification/2026-10-22-kimi-qwen-compat.md), Kimi 2.3.1 by the [2026-10-25 model-free receipt](verification/2026-10-25-kimi-231-reviewer-compat.md) and 2.3.2 by the [2026-10-26 one](verification/2026-10-26-kimi-232-reviewer-compat.md), with account/provider live legs unverified; agy 1.4.4 in-line | model-free isolation receipts, `ahub agents` verdicts, versions trusted per machine, lead time from an `agent-compat` issue to a receipt |
108
114
  | Qwen's own TUI over dual output (#286 Part 3) | off here: this repository's hub runs no Qwen peer (roster.qwen is off), and the limits (a hub deny ends the turn without an answer; a stalled turn is stopped only with Esc in the Qwen terminal) are for the person at that terminal to accept. Verified on Qwen 0.26.1 under an isolated HOME by the [2026-10-25 receipt](verification/2026-10-25-qwen-dual-output-t0.md); the live account leg is the person's | `test/qwen-dual.test.ts` against a fake dual-output Qwen; in real use `ahub status` stalled/detached notices per peer and the `needs_review` count of Qwen deliveries (an Esc or an exited terminal ends a turn that way; a deny completes) |
109
115
  | Benchmarks (#251, #637) | off: `bench.enabled = false`; routing gate pending | bench receipts |
110
116
 
package/docs/memory.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Hub memory integration
2
2
 
3
- Agent-hub consumes the exact-pinned first-party [`@staix/agent-mem`](https://github.com/STAIxBWLB/agent-mem) library (current pin `0.8.0`, initial pin `0.1.1`). The package owns portable storage, tiers, graph, goals, sync, import and raw commands. Agent-hub owns caller authority, current project privacy policy, task lifecycle integration and presentation. Claude-mem is an optional, explicit import source only; runtime recall and capture make no worker HTTP requests.
3
+ Agent-hub consumes the exact-pinned first-party [`@staix/agent-mem`](https://github.com/STAIxBWLB/agent-mem) library (current pin `0.10.0`, initial pin `0.1.1`). The package owns portable storage, tiers, graph, goals, sync, import and raw commands. Agent-hub owns caller authority, current project privacy policy, task lifecycle integration and presentation. Claude-mem is an optional, explicit import source only; runtime recall and capture make no worker HTTP requests.
4
4
 
5
5
  ## Portable core and data directory
6
6