@staix/agent-hub 0.15.0 → 0.17.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 (104) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +12 -8
  3. package/docs/agent-notes/README.md +4 -0
  4. package/docs/agent-notes/adapters.md +24 -8
  5. package/docs/agent-notes/budget.md +4 -2
  6. package/docs/agent-notes/daemon.md +11 -4
  7. package/docs/agent-notes/local-worker.md +2 -2
  8. package/docs/agent-notes/tasks.md +15 -3
  9. package/docs/agent-notes/tests.md +8 -3
  10. package/docs/dogfood.md +18 -6
  11. package/docs/events.md +1 -1
  12. package/docs/memory.md +78 -10
  13. package/docs/operations.md +160 -47
  14. package/docs/quickstart.md +13 -5
  15. package/docs/roles.md +1 -0
  16. package/docs/sdlc.md +11 -11
  17. package/docs/security.md +94 -22
  18. package/docs/smoke.md +58 -6
  19. package/docs/specs/2026-09-19-agent-hub-design.md +124 -12
  20. package/docs/specs/2026-09-20-upgrade-recovery-design.md +3 -1
  21. package/docs/verification/2026-10-24-always-ask-adapters.md +52 -0
  22. package/docs/verification/2026-10-24-amem-runtime.md +69 -0
  23. package/docs/verification/2026-10-24-codex-0164-reviewer-isolation.md +55 -0
  24. package/docs/verification/2026-10-24-delivery-holds-ac8.md +264 -0
  25. package/docs/verification/2026-10-24-hardcoded-values.md +2108 -0
  26. package/docs/verification/2026-10-25-0.16.0.md +82 -0
  27. package/docs/verification/2026-10-25-0.17.0.md +74 -0
  28. package/docs/verification/2026-10-25-codex-0164-reviewer-tool-call.md +52 -0
  29. package/docs/verification/2026-10-25-kimi-231-reviewer-compat.md +48 -0
  30. package/docs/verification/2026-10-25-kimi-usage-ac11.md +48 -0
  31. package/docs/verification/2026-10-25-qwen-dual-output-hub-e2e.md +23 -0
  32. package/docs/verification/2026-10-25-qwen-dual-output-t0.md +20 -0
  33. package/docs/verification/2026-10-25-reviewer-no-verdict-diagnostics.md +35 -0
  34. package/docs/verified.json +30 -18
  35. package/package.json +2 -2
  36. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  37. package/plugins/agent-hub/server.js +3421 -2176
  38. package/src/adapters/acp.ts +5 -2
  39. package/src/adapters/codex-appserver.ts +69 -1
  40. package/src/adapters/codex-review-isolation.ts +10 -1
  41. package/src/adapters/compat.ts +9 -4
  42. package/src/adapters/conformance/enrollment.ts +18 -4
  43. package/src/adapters/conformance/factories.ts +4 -1
  44. package/src/adapters/conformance/kit.ts +11 -0
  45. package/src/adapters/definitions/builtin-metadata.ts +3 -3
  46. package/src/adapters/definitions/native-cli-entry-qwen.ts +32 -4
  47. package/src/adapters/definitions/native-cli-preview-qwen.ts +9 -1
  48. package/src/adapters/definitions/native-factories.ts +8 -0
  49. package/src/adapters/definitions/native-recovery.ts +8 -0
  50. package/src/adapters/definitions/qwen.ts +7 -4
  51. package/src/adapters/local-worker.ts +5 -4
  52. package/src/adapters/qwen-dual.ts +578 -0
  53. package/src/adapters/registry-types.ts +6 -2
  54. package/src/adapters/registry.ts +9 -1
  55. package/src/adapters/reviewer-mcp.ts +12 -6
  56. package/src/adapters/reviewer-print.ts +100 -17
  57. package/src/cli/console-state.ts +20 -2
  58. package/src/cli/console-symbols.ts +3 -2
  59. package/src/cli/console-usage.ts +5 -2
  60. package/src/cli/console.ts +5 -1
  61. package/src/cli/help.ts +7 -5
  62. package/src/cli/identity.ts +3 -1
  63. package/src/cli/main.ts +44 -10
  64. package/src/cli/memory.ts +119 -7
  65. package/src/cli/output.ts +12 -4
  66. package/src/cli/upgrade.ts +4 -3
  67. package/src/hub/action-class.ts +211 -17
  68. package/src/hub/action-enforce.ts +13 -5
  69. package/src/hub/action-policy.ts +19 -8
  70. package/src/hub/ask.ts +16 -4
  71. package/src/hub/board.ts +86 -31
  72. package/src/hub/budget-notes.ts +65 -0
  73. package/src/hub/budget.ts +42 -10
  74. package/src/hub/bus.ts +1 -1
  75. package/src/hub/code-facts.ts +56 -16
  76. package/src/hub/conductor.ts +5 -2
  77. package/src/hub/config-apply.ts +7 -0
  78. package/src/hub/control-client.ts +1 -1
  79. package/src/hub/daemon.ts +384 -71
  80. package/src/hub/envelope.ts +1 -1
  81. package/src/hub/events.ts +2 -2
  82. package/src/hub/hub-tools.ts +3 -3
  83. package/src/hub/kimi-usage.ts +12 -0
  84. package/src/hub/lifecycle.ts +10 -5
  85. package/src/hub/memory-code-head.ts +44 -0
  86. package/src/hub/memory-code-overlay.ts +119 -0
  87. package/src/hub/memory-plan-guard.ts +11 -27
  88. package/src/hub/memory-stale-anchors.ts +159 -0
  89. package/src/hub/memory-symbols.ts +74 -0
  90. package/src/hub/planning.ts +10 -4
  91. package/src/hub/read-tools.ts +21 -3
  92. package/src/hub/sdlc.ts +34 -0
  93. package/src/hub/settings.ts +17 -2
  94. package/src/hub/settled-roles.ts +15 -3
  95. package/src/hub/task-sweep.ts +25 -3
  96. package/src/hub/tasks.ts +378 -76
  97. package/src/hub/token-window.ts +15 -2
  98. package/src/hub/window-kind.ts +13 -0
  99. package/src/local/tools.ts +5 -5
  100. package/src/memory/brief.ts +66 -16
  101. package/src/memory/capture.ts +26 -40
  102. package/src/memory/client.ts +137 -65
  103. package/src/memory/recall.ts +13 -22
  104. package/src/ui/index.html +100 -18
@@ -926,7 +926,7 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
926
926
 
927
927
  This slice provides the pure classifier, machine-local policy, host-only per-session budget ledger and per-peer status. Adapter pre-tool enforcement is a later slice. Until a runtime actually consumes the policy before execution, its status says `always-ask not enforceable: adapter integration pending`; native modes, sandboxes and approval frequency retain their existing behavior. The default-on configuration is not a claim of active enforcement.
928
928
 
929
- - `action_policy` may be set only in untracked `.agenthub/config.local.json`, never in `config.json` even when that file is untracked. `ALL_MACHINE_LOCAL` trust drops a tracked/unknown local source. Default: `enabled: true`, all four classes in `always_ask`, budgets `publish: 1`, `destroy: 3`, `external-write: 5`, `install: 2`. Budgets are integers 0..1000; zero caches no follow-up grant. Empty `always_ask` selects no classes. The settings registry exposes enabled and each budget; the closed class set is changed in the local file with a person-confirmed reload. Weakening on live reload requires existing person confirmation; shared-file policy edits are refused before any confirmation offer. Startup currently adopts the untracked local policy without a retained confirmation (ponytail: bind weakened startup policy to person confirmation or surface attention before adapter enforcement in #312/#313). Reload never imports code or changes a sandbox.
929
+ - `action_policy` may be set only in untracked `.agenthub/config.local.json`, never in `config.json` even when that file is untracked. `ALL_MACHINE_LOCAL` trust drops a tracked/unknown local source. Default: `enabled: true`, all four classes in `always_ask`, budgets `publish: 1`, `destroy: 3`, `external-write: 5`, `install: 2`. Budgets are integers 0..1000; zero caches no follow-up grant. Empty `always_ask` selects no classes. The settings registry exposes enabled and each budget; the closed class set is changed in the local file with a person-confirmed reload. Weakening on live reload requires existing person confirmation; shared-file policy edits are refused before any confirmation offer. Startup adopts the untracked local policy without a retained confirmation and surfaces attention in status when it disables enforcement, omits a class or increases a default budget (ponytail: retain a person-bound startup confirmation). Reload never imports code or changes a sandbox.
930
930
  - `classifyAction(tool, args, executionRoot)` returns all matching classes for bounded literal tool/argv or shell forms. Quotes, command/group separators, fd duplication and plain input/output redirections are parsed without evaluation; known wrapper prefixes and literal shell -c forms are unwrapped. Multi-class chains retain every class, so narrowing the ask set cannot hide another class. Publish covers remote Git/package/release publication, external-write covers known non-read HTTP/gh/mail/chat calls, destroy covers destructive Git and recursive deletion/force-overwrite outside the lexical execution root, and install covers known global installs. Unsupported syntax stops parsing while retaining classes from earlier complete segments; the unsupported current segment remains unknown. Hard input/segment limits still return no classification. Unknown forms retain native policy, never an automatic permission. Literal cwd/workdir, home spellings conservatively treated as outside, and conservative literal cd candidates are covered; aliases, functions, expansion, scripts and symlink targets are not attested. The ceiling is bounded syntax/lexical evidence; the upgrade path is typed native tool arguments and canonical path evidence from each adapter.
931
931
  - The first selected action asks. One explicit person approval of the class allowance authorizes that invocation and caches at most budget minus one follow-up invocations of each selected class. An ordinary allow_once answer never seeds this cache; adapter prompts must describe the class allowance they request. The next invocation after exhaustion asks again. Requests consume all selected class slots synchronously or none; an unselected/unclassified action returns `native`, which means the existing permission decision still runs. No ledger result overrides a denial, suppresses a prompt required by the native mode, or grants a tool capability. Session identity comes from the host's current runtime port; missing or changed identity cannot use a grant. New sessions, owner revocation and policy publication invalidate grants. One-use approval challenges are bound to the exact session handle and policy, so a late answer cannot grant to a replacement session or changed policy.
932
932
  - Status reads never create/bind/reset a session. Status carries only classes, limits, remaining counters and enforcement state, without native session ids or command/task text. Approval copy is built through `actionApprovalText`, which takes the host's `publicView`, private-turn decision and whole-command privacy projection; private commands are withheld. Adapter integrations must retain the existing timeout-denies and generation/privacy rechecks after awaiting a person. Slice 1 creates no approval requests itself and makes no per-adapter A2 or live/native enforcement claim.
@@ -936,7 +936,10 @@ This slice provides the pure classifier, machine-local policy, host-only per-ses
936
936
  - **Claude.** The `facts` reply to a PreToolUse hook call carries `actionClasses` when `classifyAction` classes the tool and its arguments under the peer's execution root and the policy selects at least one class. `factsHook` then emits `permissionDecision: "ask"` (with `permissionDecisionReason: "agent-hub always-ask: <classes>"`) in every permission mode, and the `ask` replaces any `allow` the mode would add. The managed hook's contract is therefore: it may add `allow` as before and `ask` for a classed call, never `deny`; a hub that is down, slow or refusing decides nothing (`docs/agent-notes/daemon.md`). A value that is present but unreadable asks as well; an absent or empty one leaves the mode. Claude's dialog cannot carry a class-budget choice, so the ledger is never consulted for Claude: every classed call asks. A hub that cannot answer, or a call outside a verified hub hook, leaves Claude's own policy.
937
937
  - **ACP.** `AcpPeer.answerPermission` consults `AcpOptions.actionGate` before `autoApprove`, with the announced and canonical tool names, the ACP kind and the arguments from `rawInput`, the announced `tool_call` or complete streamed JSON. A call with no reported payload is not classed (unknown follows the mode). A verdict (a classed call with no session budget left) is never auto-approved, drops `allow_always`, replaces the title with `actionApprovalText` (the task title only through `publicView`, the command only through the PII screen, withheld for a private turn or an open PII task) and sets `classed` on the `PermissionRequest`, so `--unattended` does not answer it. A throwing gate asks. The request uses the existing approval timeout and a timed-out request denies.
938
938
  - **Class budget.** When a selected class has a budget above one, the request carries the hub's own option `agent-hub:action-budget` (kind `allow_once`). Only a person's choice of it calls `ActionBudgets.approve`, which spends the allowance for the current native session and policy; the agent is answered with its own `allow_once` id and never sees the hub's. A refused approval (changed policy, replaced session, no slot left) answers cancelled. An ordinary allow never seeds the ledger. Requests are concurrent, so several challenges can be open while a class is spent: the first approval opens the allowance, a later approval of a challenge issued before it consumes one slot instead of opening another (refused whole when any class has none left), so a budget of N lets through at most N approved calls. The ledger entry is revoked whenever an ACP owner goes offline, is stopped or removed, or a new owner is created, so a resumed session id never brings an allowance back. A call inside the cached allowance follows the permission mode as before.
939
- - **Visibility.** `status.actionPolicyInfo[peer].enforceable` is true only where the call is seen before it runs: a Claude session with the launcher's PreToolUse hook that is not `--unattended`, and a hub-owned ACP peer in native mode `ask` (Kimi's `yolo` and `auto` send no request for a call they allow). For Claude the launch record alone is not proof (it outlives the process): the attached session must be online and its current generation must have run the hub's hook (`claudePermissionGenerations`). Every other peer, and Codex, Pi and the local worker until slice 3, says `always-ask not enforceable: <reason>`.
939
+ - **Visibility.** `status.actionPolicyInfo[peer].enforceable` is true only where the call is seen before it runs: a Claude session with the launcher's PreToolUse hook that is not `--unattended`, and a hub-owned ACP peer in native mode `ask` (Kimi's `yolo` and `auto` send no request for a call they allow). For Claude the launch record alone is not proof (it outlives the process): the attached session must be online and its current generation must have run the hub's hook (`claudePermissionGenerations`). Live local/Pi tool boundaries and a managed headless Claude implementer with a current busy child generation are enforceable. Codex remains conservatively not enforceable because native policy may omit a request; every other peer without a hub pre-tool path says `always-ask not enforceable: <reason>`.
940
+
941
+ - **Remaining adapters (#636 slice 3).** Codex command approval RPCs (v2 command execution, excluding writeStdin, and legacy exec argv) consult the shared gate before native forwarding. A classed request is answered only once, bound to its link/thread/turn; cancellation, disconnect and stale answers deny. Unknown payloads and other approval schemas retain native handling. This proxy cannot intercept a native call that emits no request, so status never claims complete Codex enforcement. Local/Pi tools pass parsed arguments into their existing permit boundary and classify before mode or tool-wide grants. Pi's persisted call receipt encloses the gate, so replay spends no allowance twice. Only verified person-authored idle shell requests retain their existing bypass.
942
+ - **Rendered approvals.** Public/native command views are privacy-screened before rendering. A cut or withheld view offers deny only and cannot authorize even one invocation or a class budget. Local/Pi use their exact full host-generated tool title with a class line on the person's on-prem console, PII turns included; logs, public status, other peers and dashboard projections still withhold it. The same explicit class-budget choice, session/owner revocation and final-answer fence apply to received Codex, local and Pi approvals. Managed Claude forms keep asking through their native prompt path.
940
943
 
941
944
  ## Amendment: approval notifications and timeout (issue #5)
942
945
 
@@ -2646,6 +2649,28 @@ the queue.
2646
2649
  - Activity contains delivery ids/senders, message/thought chunks, bounded tool summaries, plans, permission request/outcome ids and turn end. It never enters persistent events/logs, UI, tools or cloud memory. Existing final answers still travel through the answer bus.
2647
2650
  - Kimi TUI defaults remain pull-only. Qwen TUI and native visualizer support remain gated by unverified T0 checks. See [verification](../verification/2026-10-11-acp-watch-t0.md).
2648
2651
 
2652
+ # Qwen dual-output TUI amendment, 2026-10-25 (#286 Part 3)
2653
+
2654
+ - T0 ran live on Qwen Code 0.26.1 under an isolated HOME ([verification](../verification/2026-10-25-qwen-dual-output-t0.md)): Q2, Q3, Q5 and Q6 pass, Q4 fails. The owner decided on 2026-10-25 to build Part 3 with the two limits below stated, so AC9's condition reads "Q2, Q3, Q5 and Q6 pass; Q4's limit is stated".
2655
+ - Launch: `ahub qwen --mode tui` runs `qwen --input-file <in> --json-file <out> --mcp-config <hub server> --approval-mode default` in the person's terminal, as `ahub pi --mode tui` does. Both files live under the project state dir, mode 0600. The event file is a regular file the adapter tails from a saved offset.
2656
+ - Version gate: a separate compat entry for the dual-output contract, verified 0.26.1 only. It does not widen the headless `qwen` entry, whose ACP receipts are separate.
2657
+ - Delivery: `deliver()` sets busy synchronously and appends one `{"type":"submit","text"}` line with the digest. Qwen queues a submit that arrives during a turn and runs it after that turn ends.
2658
+ - Turn end: Qwen emits no `result` event in this mode. A turn ends at an `assistant` event whose `stop_reason` is not `tool_use`, followed by `message_stop`; that assistant text is the turn's shared answer. A `result` event, if a later build emits one, also ends the turn.
2659
+ - Approvals: `control_request` with `subtype: can_use_tool` goes to the hub approval path, and the answer is `{"type":"confirmation_response","request_id","allowed"}`. Allow or deny only. The hub's own tools are approved by the exact `tool_name` (`mcp__<server>__<tool>`), never by prefix. A `control_response` for a pending request that the hub did not answer means the person answered in the TUI: withdraw the hub's pending approval. A `control_response` error for a stale id is logged, never retried.
2660
+ - Limit 1, deny: a hub deny cancels Qwen's whole turn and Qwen emits nothing for it. After sending `allowed: false` the adapter ends the turn itself (idle, no shared answer, a notice that the turn was denied).
2661
+ - Limit 2, no external stop: the input protocol has only `submit` and `confirmation_response`, and SIGINT does not stop a turn. The inactivity watchdog never cancels a Qwen TUI turn. It reports the stalled turn to the person (status, console and dashboard: "stalled; press Esc in the Qwen terminal") and keeps the peer busy. The hub never types into the terminal or signals the process to stop a turn.
2662
+ - Prompt channel: the input file is a prompt channel. Qwen's own write and shell tools reach it only through approval, and the hub's approval path refuses any write or command that names the state dir. No other peer can write it.
2663
+ - Detached: the TUI keeps working when nobody reads the event file (T0 Q5). A daemon that cannot read the file treats the peer as detached, not idle; on reattach the adapter resumes from its saved offset.
2664
+ - Recovery: `qwen -r <session_id>` with the same two files resumes the conversation, does not replay earlier submits, and appends to the event file. The adapter records `session_id` from `system/session_start` and resumes from its saved offset, so events from before the restart are not processed twice.
2665
+ - Implementation notes (#449, measured on 0.26.1 where stated):
2666
+ - Files and state: `<state dir>/qwen/qwen-dual/{input.jsonl,events.jsonl,mcp.json,state.json}`; `state.json` holds `session_id`, the read offset, a generation counter and the bound owner (pid with its `processSignature`). `ahub qwen --mode tui` is explicit only: Qwen's `start.forms` stays `["headless"]`, so Qwen is not a start-mode peer and the hub never opens this terminal itself. The TUI driver is the definition's `start.createTui` on the `qwen-dual` daemon port, a fixed-builtin capability.
2667
+ - Gate: checked twice, by `qwen --version` against `COMPAT["qwen-dual"]` before any file is made, and by the TUI's own `session_start` (version, `protocol_version` 2, the events the adapter reads). An unverified TUI is not attached and the person gets a notice.
2668
+ - Event file: Qwen opens an existing `--json-file` at byte 0 without truncating it (it overwrote the head of a 3 KB file in place), so appending after a restart is not what it does. A fresh or resumed launch therefore starts from empty files (the old event file is kept as `events.prev.jsonl`, the saved offset restarts at 0 for the new file), and a hub restart while the person's Qwen keeps running adopts it and reads on from the saved offset with no launch. Qwen starts reading the input file at its size at start, so earlier submits are not replayed either way.
2669
+ - Owner: bound from the host's process table (the one process whose argv contains `--input-file <this adapter's file>`; two matches is no owner), at `session_start` and then every second. No launcher message is trusted for it. When the owner is gone the peer goes offline, an open hub turn settles `needs_review` and a notice says the terminal ended. The hub never signals it.
2670
+ - A prompt in the event stream that is not the hub's submit (matched by its text) is the person typing: the peer is busy, nothing is shared, and a `can_use_tool` request in that turn is the person's own (the console is not asked). A hub turn that a person's prompt cuts off (Esc, then a new prompt) settles `needs_review`.
2671
+ - `ahub stop qwen` refuses for the TUI (end it in its terminal); a headless Qwen cannot be started while the TUI runs, and the reverse. Crash and upgrade recovery refuse to restore this terminal as a headless worker (`recoveryRefusal`), as for Kimi's own TUI.
2672
+ - Not verified live: the T0 receipt covers Q2 to Q7 on 0.26.1; this PR's tests use a fake dual-output Qwen (`test/fakes/qwen-dual.ts`) and no live model, so end-to-end use with a real account remains the person's check (`docs/dogfood.md`).
2673
+
2649
2674
  # Historical workflow amendment, 2026-10-11 (#257, superseded by #481)
2650
2675
 
2651
2676
  - The original optional workflow selector used existing SDLC receipts and Tasks authority; its current replacement is the #481 amendment below. A reviewer/person-owned checklist stores bounded criterion ids, hashed evidence refs and verified/failed/unverified claims bound to the reviewed head.
@@ -4417,7 +4442,7 @@ Stage to role, fixed:
4417
4442
  - **I-1 (existing).** The reviewer is never the current owner, by id (`routing.ts:494`, `:541`).
4418
4443
  - **I-2 (new).** The reviewer is never a peer that implemented this task: any peer with an `accepted` or `done` history entry `by` it on this task, read from `task.history` inside `assign()`. Refusal code `policy`, reason `implemented this task`. When this empties the candidates the reviewer is the person (`reviewBlocked`), never a fallback to an implementer.
4419
4444
  - **I-3 (existing, #519).** Other family first; own family only when no other is eligible, recorded as `sameFamilyFallback=true` (`routing.ts:547-550`).
4420
- - **I-4 (new, enforced in `Tasks.recordPlanReview`, not in `assign()`).** The plan author does not record the plan review of that plan. Plan author: `by` of the latest history entry with `planRevision` (`src/hub/board.ts:69`, `:348`); when none (lifecycle off), the latest entry whose `planning.fields` includes `plan`; when none, `history[0].by` if `plan` is nonempty; otherwise unknown. If the caller is the author and another planner holder is rostered and not offline, refuse pre-effect ("an independent planner reviews this plan"). If the caller is the author and no other holder is eligible, record the receipt with `selfReview: true` (the single-planner case keeps working).
4445
+ - **I-4 (new, enforced in `Tasks.recordPlanReview`, not in `assign()`).** The plan author does not record the plan review of that plan. Plan author: `by` of the latest plan-authoring history entry with `planRevision`, then a legacy authoring entry whose `planning.fields` includes `plan`. Exclude `plan reviewed` entries from both paths: review/CI scope bookkeeping never changes the plan-content author. When only the initial proposal remains, use its author only if intent list fields are nonempty; review/CI scope alone leaves the author unknown. A later actual plan edit or explicit intent removal records its own author. If the caller is the author and another planner holder is rostered and not offline, refuse pre-effect ("an independent planner reviews this plan"). If the caller is the author and no other holder is eligible, record the receipt with `selfReview: true` (the single-planner case keeps working).
4421
4446
  - **Independent reviewer authority.** Recording a plan review is the one planner act that is not limited to the task's planner: any current settled planner holder other than the plan author may record it, and so may the person. That is what keeps I-4 from deadlocking against R3's per-task authority. The review is the only power this grants: the reviewing holder still cannot plan, split, pull, undo, cancel, reassign or release the task (R3). When the effective planner is the author and an independent holder is eligible, the hub sends that holder a plan-review request (ids only, through `publicView`), chosen by the same rotation as R3 and excluding the author. Unknown author records `planAuthor: null` and does not refuse. The receipt gains `planAuthor`.
4422
4447
  - Planner and owner may be the same peer; planner and reviewer may be the same peer (today's default `claude: [planner, reviewer]`). I-4 covers the only conflict between them.
4423
4448
  - **Purity.** The planner walk runs inside `assign()` with the same inputs it already takes plus `planners` (sorted settled holder ids) and `namedPlanner`. `Assignment` gains `planner?: PeerId` and `plannerReason?: "rotation" | "named" | "kept" | "none"`. Every caller that routes a new or pulled task reads the planner from this result; `ahub route explain` prints the same walk (`planner candidate X: ...` lines, then `planner: X (reason)`), so explain and assignment cannot drift. `Tasks` never chooses a planner outside `assign()`.
@@ -4430,9 +4455,10 @@ Stage to role, fixed:
4430
4455
  - a person-named planner (`namedPlanner`) is exact, refused like a named owner if not a holder (#601 rule);
4431
4456
  - otherwise rotation: holders sorted by id, start at `task.id mod holders.length`, walk forward skipping off-roster, offline, paused (`budget_paused = skip_peer`), failing and `routing.allowedRoles`-excluded holders; do not renumber by availability (the #519 seed rule). If all are skipped, take the seed holder with reason `rotation (none available)`.
4432
4457
  - PII tasks get `planner: null` and trace `planner: none (pii)`; their planning stays as today (stubs only, person acts).
4433
- - The choice is written on the `assigned` (or `backlog pulled`) history entry as `planner` and `plannerReason`, through the same metadata path as `reviewerFamily`/`reviewerReason`.
4434
- - **Effective planner.** `Tasks.taskPlanner(task)`: `task.planner` when it is still a settled planner holder and rostered; otherwise all current holders (legacy). A pause or offline state does not move it; the person moves it.
4458
+ - The choice is written on the `assigned` (or `backlog pulled`) history entry as `planner` and `plannerReason`, through the same metadata path as `reviewerFamily`/`reviewerReason`. A fixed pure host port binds every new proposal, split child and generated backlog card in its original insert transaction, with planner metadata on the initial `proposed`/`backlogged` entry. Notifications never expose an unbound intermediate new row, and split undo keeps its original child snapshot.
4459
+ - **Effective planner.** `Tasks.taskPlanner(task)`: `task.planner` when it is still a settled planner holder and rostered; otherwise all current holders (legacy). A pause or offline state does not move it; the person moves it. Routing explain uses the bound planner as `keptPlanner` and names it as `kept`; a draft or legacy null binding still runs rotation. Explaining either form writes no binding or history.
4435
4460
  - **Authority.** `planningAuthority(by)` (`tasks.ts:833`) becomes `planningAuthority(by, task?)`: with a task, `by` must be the person or the task's effective planner; without one (backlog list, release list, new release, grooming read, import) any holder, as today. Every call site that has a task in hand passes it: pull (`:892`), plan (`:1035`), undo (`:1089`), split (`:1148`), assign/configure/cut release (`:1218-1286`, against `releases.planner`), plan review (`:2461`, which follows I-4's independent reviewer rule instead of the task-planner check), planner cancel (`:2247`), and the custom stage actor (`:2511`). Rechecks after awaits keep their places. Grooming reads stay project-wide; grooming, sweep and stall notices (`:397-401`, `:426`, `:2236`, `:938-971`) go to the task's effective planner only.
4461
+ - **Planner lifecycle stage identity.** The effective task planner may record only `stage`, `repo` and `issue` when an explicitly supplied stage has actor `planner` or `owner/planner`. Existing enforce relink and stage/spec gates still apply; advise retains its existing identity relink behavior. This grants no reviewer evidence, checklist, head or deliverable authority.
4436
4462
  - **Naming.** `ahub task assign <id> <peer> --role planner` (person only; `TaskAssignmentRole` gains `planner`, `src/hub/tasks.ts:42`; parsers `src/cli/main.ts:1292`, `:2109`; help `src/cli/help.ts:86`). A planner cannot reassign planning to another peer. The write is a `planner changed` entry (reason `named`), which neither moves nor settles the task, so it never counts as supersession of an owner delivery (tasks.md assignment rule). `hub_task_assign` with role `planner` from a peer is refused pre-effect.
4437
4463
  - **Release planner.** `newRelease` records `by` as the release's planner (`null` for the person); cut/configure/assign check it like a task's.
4438
4464
  - **Migration.** No `planner` key and one planner holder: rotation always yields it. A single pinned planner: same. Old rows and old releases: `null`, legacy authority. Tests that assert a planner tool succeeds for the only planner stay green unchanged.
@@ -4506,7 +4532,7 @@ Additive optional fields, no `PROTOCOL` bump (daemon.md):
4506
4532
  - **Planner WIP cap (#655).** Unchanged: `reservedHold`/`acceptanceCapacity` key on the reserved owner and `wipLimit(owner)` (`daemon.ts:1350`, default 1). Each new instance has its own `tasks.wip_limits.<id>`. Planner pull requires the card's effective planner (R3); `plannerRefusal` text stays ids and counts.
4507
4533
  - **Budget pauses.** The Claude implementer shares the `claude` account's pauses and readings (I2). A pause holds new deliveries; a running child finishes its turn (not killed by the pause). Rotation skips paused planners at choice time only; a later pause does not move a task's planner. Reviewer pool behaviour is unchanged.
4508
4534
  - **Always-ask (#636).** Not a dependency. When #636 lands, its classifier runs in the same PreToolUse hook; an `ask` from the hook in `-p` mode reaches `hub_permission_request`, which carries the class and applies the class budget. Since the headless variant has no `never-ask`, #636 only adds the class label there. #636's adapter table must list both new Claude forms; both have the hook, so neither is "always-ask not enforceable".
4509
- - **Recovery.** New target feature `role_stages` in `DAEMON_FEATURES` and `incompatibleRecoveryTarget` (daemon.md floor rule), required while the project declares a Claude implementer instance, a Codex planner-only instance with the read-only sandbox, or a `planner` setting that is a list or names an instance, and also while any task or release row has a non-null `planner` or any plan-review receipt has `planAuthor` or `selfReview`, even with no new configuration (rotation alone can bind planners). An older daemon would ignore those bindings and fall back to "any holder acts", which widens planner authority, so recovery to it is refused, never silently allowed. The floor check reads the board (a bounded `EXISTS` on each column), not only config. An older daemon would throw on the list or instance planner (`daemon.ts:289-291`) and on the new declarations (`instances.ts:89`, `:91`). An older daemon reading the board ignores the `planner` column (`SELECT *` + `parse`; unverified that `parse` tolerates unknown columns, check in 525-2).
4535
+ - **Recovery.** Target feature `role_stages` retains its Claude implementer and Codex planner-only instance meaning. Slice 525-2 adds `task_planners` to `DAEMON_FEATURES` and `incompatibleRecoveryTarget` (daemon.md floor rule), required while a `planner` setting is a list or names an instance, or a declared instance holds planner, and while any task or release has a non-null `planner` or plan-review authorship markers remain in its receipt/history. A 0.15.0 daemon already advertises `role_stages` but ignores task/release planner bindings; recovery to it must refuse before any effect. The floor reads bounded SQL `EXISTS` evidence, not only config. Old board parsing tolerates the additive nullable columns without changing legacy null authority (verified with the published 0.15.0 Board parser).
4510
4536
  - **Settings.** `planner` stays a guarded settled-holder key (`roleHolders`, `daemon.ts:4431-4435`); a list change goes through the same confirmation.
4511
4537
 
4512
4538
  ### Acceptance (from the issue, mapped)
@@ -4529,7 +4555,7 @@ Additive optional fields, no `PROTOCOL` bump (daemon.md):
4529
4555
  - Planner walk, `Assignment.planner`/`plannerReason`, I-2, explain lines. No board write of the planner yet (result unused until 525-2).
4530
4556
  - ACs: AC-2.
4531
4557
  3. **525-2 Per-task planner.** Size L. Depends on 525-3.
4532
- - Paths: `src/hub/board.ts` (columns, migration), `src/hub/tasks.ts` (`taskPlanner`, `planningAuthority(by, task)`, notices, I-4 in `recordPlanReview`, release planner), `src/hub/settled-roles.ts`, `src/hub/settings.ts`, `src/hub/daemon.ts` (loader, status, `role_stages` floor for list/instance planner), `src/cli/main.ts`, `src/cli/help.ts`, `docs/verified.json` restamp of help.
4558
+ - Paths: `src/hub/board.ts` (columns, migration), `src/hub/tasks.ts` (`taskPlanner`, `planningAuthority(by, task)`, notices, I-4 in `recordPlanReview`, release planner), `src/hub/settled-roles.ts`, `src/hub/settings.ts`, `src/hub/daemon.ts` (loader, status, `task_planners` floor for list/instance planner), `src/cli/main.ts`, `src/cli/help.ts`, verify parser/help parity; leave `docs/verified.json` stamps to the release PR, because this feature PR is squash-merged.
4533
4559
  - ACs: AC-5.
4534
4560
  4. **525-4 Headless Claude implementer.** Size L. Depends on 525-1 (and 525-2 only for the daemon test's planner).
4535
4561
  - Paths: new `src/adapters/claude-implementer.ts`, shared child helper extracted from `src/adapters/reviewer-print.ts`, `src/adapters/definitions/builtin-metadata.ts` (variant), `src/hub/daemon.ts` (start, facts permission for child launch id, tools hello fence, `executionOwners`, `role_stages` floor), `src/hub/claude-state.ts`, `src/cli/reset.ts`, `src/hub/hub-tools.ts` (`hub_permission_request`, published only to this variant's child) and `src/adapters/claude-channel.ts` tools mode (rebuild `plugins/agent-hub/server.js`), `test/fakes/` fake `claude -p`.
@@ -4712,7 +4738,21 @@ export interface ClaudeLaunchProof {
4712
4738
 
4713
4739
  ### Call-site inventory
4714
4740
 
4715
- Counts are call occurrences in `src/hub/daemon.ts` (`grep -o`). `ripwire . --callers=<predicate>` lists the same functions and lines.
4741
+ The following inventory is historical, audited at `bab87cb2` (2026-10-22). Counts are call occurrences in `src/hub/daemon.ts` (`grep -o`). `ripwire . --callers=<predicate>` lists the same functions and lines.
4742
+
4743
+ Slice 2a freezes the current baseline at `f11a2a1c3e24d1c431937941ecd40b07ca00a5cc` in `test/daemon-native-branches.json`, with executable checking in `test/daemon-native-branches.test.ts` using the shared `scripts/lib/ts-ast.ts` helper. The JSON had 61 predicate/function rows and 85 calls at that baseline; #449 (the Qwen dual-output TUI, #286 Part 3) added three reviewed `isQwenDualPeer` rows (`startPeerBody` 2, `status` 1, `stopPeer` 1: 64 rows, 89 calls) across `src/hub/daemon.ts` and `src/hub/claude-launch-proof.ts` (the launch-proof reader currently contributes zero native predicate calls). Each row carries its current reason; additions and removals both require an explicit reviewed table edit. Tests never regenerate the table.
4744
+
4745
+ | Predicate | Historical count | Current count | Delta |
4746
+ |---|---|---|---|
4747
+ | `isCodexPeer` | 12 | 13 | +1 |
4748
+ | `isPiPeer` | 20 | 21 | +1 |
4749
+ | `isWsPeer` | 25 | 27 | +2 |
4750
+ | `isAcpPeer` | 12 | 13 | +1 |
4751
+ | `isLocalPeer` | 6 | 8 | +2 |
4752
+ | `isJcodePeer` | 1 | 1 | 0 |
4753
+ | `isReviewerPrintPeer` | 2 | 2 | 0 |
4754
+
4755
+ The seven additional calls retain later native proof behavior: `actionEnforcement` adds one call each for Codex, Pi, Ws, ACP and Local; `actionSession` adds Local's intentionally separate action-allowance session; `currentExecutionOwner` adds the terminal Claude socket-generation/worktree proof. Slice 2a removes none of these calls to match the historical plan. The detailed historical assignments below remain context for later slices; the committed table is the current per-function ratchet.
4716
4756
 
4717
4757
  | Predicate | Count | Ported | Intentional |
4718
4758
  |---|---|---|---|
@@ -4831,15 +4871,15 @@ Common acceptance criteria:
4831
4871
 
4832
4872
  1. **2a: Ratchet and trust gate.** Area note: `tests.md`. Size S.
4833
4873
  - Paths:
4834
- - new `test/daemon-native-branches.test.ts` and `test/daemon-native-branches.json` (rows `{predicate, function, count, reason}`; TypeScript AST over `src/hub/daemon.ts` and `src/hub/claude-launch-proof.ts`, reusing the source walk in `test/helpers/registry-inventory.ts`; never imports daemon code);
4874
+ - new `test/daemon-native-branches.test.ts` and `test/daemon-native-branches.json` (rows `{predicate, function, count, reason}`; TypeScript AST over `src/hub/daemon.ts` and `src/hub/claude-launch-proof.ts`, reusing `scripts/lib/ts-ast.ts` from #684 phase 5; never imports daemon code);
4835
4875
  - `src/adapters/registry.ts` (`isFixedBuiltinDefinition`; `session.nativeRevert` restricted);
4836
4876
  - `test/adapter-capability-scope.test.ts`;
4837
4877
  - `src/adapters/conformance/kit.ts` and `enrollment.ts`, with a ports leg.
4838
4878
  - ACs:
4839
- - The table equals today's counts (the inventory above).
4879
+ - The table equals the current baseline above (61 predicate/function rows, 85 calls at `f11a2a1c`); the older inventory remains historical context.
4840
4880
  - An inline fixture with one extra predicate call fails, and so does a row with an empty reason.
4841
4881
  - A configured definition that declares `session.nativeRevert` is refused.
4842
- - Kit leg: a non-fixed-builtin definition whose product has `frontendProxy`, `statusNotice` or `revertTurn` fails the kit. (Today no product has these members, so the leg passes at 2a and starts biting from 2d.)
4882
+ - Kit leg: a non-fixed-builtin definition whose product has `frontendProxy`, `statusNotice` or `revertTurn` fails the kit. (The proposed `frontendProxy` and `revertTurn` members arrive in 2d; Codex already has `statusNotice`. The foreign-product check is active in 2a and rejects presence even when a value is undefined.)
4843
4883
  - The ACP ports leg (`deliveryOnly`, `hubOwned`, `form: "headless"`) is not part of 2a: no ACP product exposes those members until 2b adds them, so 2a cannot assert them. 2a only reserves the leg in `UNSUPPORTED_LEGS` with the reason "ports added in 2b".
4844
4884
  - Builtins that do not apply name their reason in `UNSUPPORTED_LEGS`.
4845
4885
  2. **2b: Delivery-only, owned stop, form and roster detach.** Area note: `adapters.md` (Spawned processes; Turns). Size M.
@@ -5040,4 +5080,76 @@ The only artifact identity was GitHub (`repo`, `issue`, `pr`, `head`, `reviewedH
5040
5080
  - **Approval.** `Tasks.review` re-hashes the delivered files before an approval. Unchanged, the approval carries the same records. Changed, missing or unknown, a reviewer's approval is refused pre-effect (`deliverable changed since it was delivered`); the person may approve the current bytes, which are measured and bound, with a note. The delivered records are rechecked after the awaited hash, so a redelivery during the verdict refuses it.
5041
5081
  - **Reads.** `Tasks.deliverableStatus` compares the files as they are now with the approved (else delivered) records: `unchanged`, `changed`, `missing` or `unknown`, and `changedSinceReview` true only for a measured difference. It is added to `task_show` for the console and peers. A file the hub cannot vouch for is `unknown`, never `unchanged`.
5042
5082
  - **Privacy.** A record is private for a PII task or a path under `pii.paths` (`matchesPolicy`). `publicDeliverables` is the one projection every peer view, review request and status uses: a private record, or a path matching a PII pattern, shows id, hash and size only. The board keeps the path (as it keeps a PII task's text) so the file can be found again.
5043
- - **Not here.** `ahub deliverables` and the dashboard column (#509 AC3) are the next slice. `PROTOCOL` is unchanged: the new tool fields are optional. Known limit: tool input schemas are not enforced on the way in, and a daemon from before this slice reads only `summary` and `refs` from `hub_task_done`, so a newer plugin talking to an older daemon has its `deliverables` silently dropped and the task is recorded without them. The plugin is installed apart from the daemon (docs/agent-notes/daemon.md), so after an upgrade restart the hub before relying on deliverables; a floor like `role_stages` is the upgrade path if that ever matters.
5083
+ - **Not here.** `ahub deliverables` and the dashboard marker (#509 AC3) are the next slice (done in the #509b amendment below). `PROTOCOL` is unchanged: the new tool fields are optional. Known limit: tool input schemas are not enforced on the way in, and a daemon from before this slice reads only `summary` and `refs` from `hub_task_done`, so a newer plugin talking to an older daemon has its `deliverables` silently dropped and the task is recorded without them. The plugin is installed apart from the daemon (docs/agent-notes/daemon.md), so after an upgrade restart the hub before relying on deliverables; a floor like `role_stages` is the upgrade path if that ever matters.
5084
+
5085
+ ## Amendment: `ahub deliverables` and the dashboard badge (#509b, #317)
5086
+
5087
+ The second slice of #509 (AC3): a person-facing list of what tasks delivered and what a reviewer approved, and a dashboard marker. Nothing new is stored: both read the records 509a already writes.
5088
+
5089
+ - **Command.** `ahub deliverables [--task <id>] [--approved] [--check] [--json]` is the person's (`identity.ts` classifies it `console`; the daemon's `deliverables` task op sits behind the console gate, so an agent's tools connection is refused; `liveTestRefusal` refuses the delegated form). Each row is a task that has delivered files, newest task first: `#id title [delivered|approved]`, then each file's id, path (only where `publicDeliverables` allows), format, version, size and full sha256. A PII task or a `pii.paths` file shows id, hash and size only, and the title is `publicTitle`. `--task` takes one positive integer (a repeated `--task` is refused rather than keeping the last); `--approved` lists only tasks whose approval bound records, and shows those approved bytes.
5090
+ - **Measured or not.** Without `--check` no file is opened: the list is the board's records, `checked` is false and no row says anything about change. With `--check` every listed file is measured now through `Tasks.deliverableStatus` (the 509a reader, with its limits and its `unknown` for a file the hub cannot vouch for), each file shows `unchanged`, `changed`, `missing` or `unknown`, and the row says `changed since review` only for a measured difference. The row's own reading (`result`) follows the same order: it says `(unchanged)` only when every file did. Consumers of `--json` read `result`: `changedSinceReview` stays false next to `result: "missing"` or `"unknown"`, because it is true only for a measured difference. `Tasks.deliverableList` is the one reader.
5091
+ - **Dashboard.** The snapshot's task rows gain an optional `deliverables: {state, files, checked?}` (`Tasks.deliverableBadge`): the same public projection of the same records, never a file read, because the page polls every second and hashing up to 512 MiB per task on the poll is not acceptable. `checked: {at, result}` appears only when a live read (`task_show`, `ahub deliverables --check`) measured exactly these records; `result` is `changed`, `missing`, `unknown` or `unchanged` for the whole task, and `unchanged` only when every file measured unchanged (a change outranks a missing file, which outranks one the hub could not read, such as every file under an unverified execution root). The page and the CLI say `a file is missing` or `could not be measured` for those, never `unchanged`; `Tasks.deliverableResult` is the one reducer (the cache is keyed by the record ids and hashes, so a redelivery or a new approval clears it). The card shows a `Deliverables n` chip (with `approved` and `changed` when known); the task sheet gains a Deliverables group with each file's path (or `private file`), format, version, size and a twelve-character hash prefix, and says `not measured now: run ahub deliverables --check` when nothing was measured. `PROTOCOL` is unchanged: the field is optional and an older page ignores it.
5092
+ - **Interpretation.** The issue says "a dashboard column". The dashboard's board is kanban columns by task state with no per-task table, so a new column of its own has no place; the chip on the card and the group in the task sheet are the equivalent surface. Say so in review if a table was meant.
5093
+ - **Ceilings.** The dashboard's `changed` is as fresh as the last live read in this daemon's memory (a restart clears it, and a file edited after the last read shows nothing until the next); the command, not the page, is the way to measure now. The list reads the whole board each call (as `ahub board` does); there is no pagination.
5094
+
5095
+ ## Amendment: window kinds, source precedence and per-peer budget notes (#285 phase 4)
5096
+
5097
+ - **Kind.** `UsageWindow` gains an optional `kind: "native" | "count" | "manual"` next to `source` (`src/hub/window-kind.ts`). `windowKind` is the one classifier: a named kind wins; an untagged window is `count` when its id is `tokens`, `manual` when its source is `ahub budget set`, else `native`, so a reader that predates the field keeps working. The Kimi count and `ahub budget set` name their kind; the Claude and Codex readers stay untagged and read as native. `PROTOCOL` is unchanged: `kind`, `superseded` and the reply's `peers` and `kimiSource` are optional fields an older client ignores.
5098
+ - **Storage and precedence.** `Budget` keeps readings per window id and kind, so a person's number and the provider's for one window never overwrite each other (and `quota_reset` baselines are per kind). `Budget.gating` decides what the gate and `headroom()` act on: a fresh manual reading replaces the other kinds for its window id; while any native or manual reading is fresh, the count is information only. A stale or reset reading leaves `fresh`, so the count gates again when nothing better is left. `status()` marks such a window `superseded`. A hard provider refusal clears the person's number for its window. `forget(peer, id, kind?)` lets the Kimi count drop only its own reading.
5099
+ - **Notes.** `src/hub/budget-notes.ts` (pure) builds one line per attached peer that shows no number, from the definition and counters, never from a log: `budget.noQuotaReason` (Pi, local) gives `no quota: ...` plus the hub's own 5 h token count for that peer (a `TokenWindow` fed by `addTokens` and seeded from `tokens` events at startup, information only); a peer with a source that has not reported says which; Kimi says by its count state (`KimiUsage.status()`: off, waiting, counting, unreadable with a reason code). The `budget` control reply carries `peers` (the notes) and `kimiSource`; the dashboard snapshot adds `note` and `noteKind` per peer.
5100
+ - **Surfaces.** `ahub budget` prints KIND, SOURCE and MEASURED per window, `info only` for a superseded count, and each note; the console's Peers detail and Usage detail and the dashboard's Usage budgets row show kind and source. The dashboard keeps its own copy of `windowKind` (`windowKindOf`, the page imports nothing); `test/ui.test.ts` runs it against the source. `ahub doctor` adds `kimi usage source` (Hub section) from `kimiSource`; an older hub without it adds no row.
5101
+ - **Not here.** The OAuth probe for Kimi's plan quota (AC9) stays undecided, and the live checks (AC11) stay open.
5102
+
5103
+ ## Amendment: amem runtime (agent-mem#13 P4)
5104
+
5105
+ The hub uses exact-pinned @staix/agent-mem 0.7.0 for runtime recall/capture, with no HTTP to the claude-mem worker. The canonical alias-resolved project and selected-profile fence govern every operation. Curated notes remain the evidence for ask and briefs; raw deterministic capture and pending candidates do not become evidence. Explicit task notes use the native store once; auxiliary checkpoint/model answers are candidates. Legacy worker project labels and numeric observation ids are replaced by canonical keys and authentic amem entry ids. Recent recall is curated short-term data, not model-generated tool summaries. Claude-mem is import-only and off by default. Host PII/private-turn/path screening is retained. The supplemental curated view's 4096-row ceiling refuses an oversized view; policy-aware pagination is its upgrade path.
5106
+
5107
+ ## Amendment: backlog issue planning and phase grooming (#759)
5108
+
5109
+ - Duplicate candidates exclude two tasks related by directed `deps`/`after` reachability in either direction. No follow-up field or inference from titles, notes or `supersededBy` is introduced. Overlap findings remain unchanged. A bounded public ID/dependency context may include approved intermediate phases; missing/private context is never read as an invented edge.
5110
+ - `hub_backlog_plan` accepts an omitted `fields` or `{}` with the existing bounded reason. It records a note-only `planned` revision with an empty field list; authority, stale revision, pins and frozen-release guards remain.
5111
+ - `fields.sdlc` accepts only `repo`/`issue` on an unstarted, ownerless backlog card, by its effective planner or the person. Null identity members restore a previous absence during undo. Other lifecycle/evidence fields remain unavailable through planning, and ordinary `hub_sdlc_record` still refuses backlog cards.
5112
+ - Issue relinks use the same identity invalidation as `sdlcReceipt`: old spec acceptance and identity-bound evidence cannot survive a different identity. Existing stage is preserved, or the host's initial stage is used when the card had no receipt. The atomic planning write records the new revision and only previous identity members; undo cannot revive an old acceptance receipt. Authority and task/release/privacy state are rechecked after screening.
5113
+ - CLI `ahub backlog plan ... <fields-json> <reason>` uses `{}` for a note and nested `sdlc` for an issue relink. Tool descriptions expose the same contract. Actual cards #138 and #287 are relinked to #525 and #754 only after landing and availability in the running hub, through an authorized planner/person identity.
5114
+
5115
+ ## Amendment: the planner board-changed notice (#758)
5116
+
5117
+ The notice "The board changed. Before offering or accepting build work, read `hub_planner_groom` ..." went to every attached planner at every board status revision, including revisions the planner wrote itself, and queued a new copy each time. One planner session received it 12 times in 44 minutes. With the lifecycle on, `flushPlannerNotices` now reads what was added to each card's history since its last scan and decides per planner:
5118
+
5119
+ - **Author skipped.** A history entry whose `by` is the planner raises nothing for that planner.
5120
+ - **Scoped.** Only the card's planners are told (`taskPlanner`, #525-2): a card bound to one planner does not reach another.
5121
+ - **Task weight only when a plan review is needed.** A history entry that carries `planning` (a planning revision or dependency change), a card's creation (a peer's propose, with or without `after`, a split child, a generated card: new work and its dependencies are what a review consolidates) and any change of a card's `deps` since the last scan, whatever wrote it, is a `task` notice. An earlier copy that is still queued for that planner (never attempted or delivered) is withdrawn and replaced, so there is at most one unsettled notice per planner and it names the latest board revision. A settled notice is not replaced.
5122
+ - **The hub's own bookkeeping tells nobody.** An entry by `hub` without `planning` (lifecycle links, idle sweeps, holds, plan observations) neither raises a notice nor spends the `fyi` window.
5123
+ - **`fyi` weight otherwise.** A state move, review or assignment is recorded as an `fyi` task notice (console and log, no turn), at most once per planner per ten minutes (`GROOM_FYI_WINDOW_MS`, bounded by the injected clock `TasksDeps.now`).
5124
+ - **Restart and offline.** The first notice a planner gets after a hub start is task weight, as before. Changes made while a configured planner was offline stay pending for it.
5125
+
5126
+ Ceiling: a scan lists the board once per status revision (at most one per flush); an indexed history read is the upgrade. `PROTOCOL` is unchanged.
5127
+
5128
+ ## Amendment: canonical memory symbol anchors (#684 phase 3)
5129
+
5130
+ - Normalize symbol signatures with the phase 1b normalizer and preserve explicit file qualification in the existing `symbol:<qualified>@<path>` format. No amem operation, schema or code index is added.
5131
+ - "Link that task's memory entries" means the new completion note carries screened, current-head, actually changed symbols with positive unambiguous code-facts `exists` evidence. Earlier notes remain immutable and connect through their shared reset-safe task node while the existing short-term completion finding is live. `related(... [symbol] ...)` reaches an earlier note at memory hop two; the package graph's literal two-edge cap reaches the task node. No read-time overlay is part of this phase.
5132
+ - Non-completion notes retain their existing normalized plan-intent associations. Completion notes with unknown observations write no symbols and say unknown, never the plan's guesses. At most 16 candidates are checked; no new check starts after a two-second scheduling allowance, and an in-flight check retains the existing code-facts execution/cleanup bounds. Recheck current privacy, repository/head provenance and the admitted completion observation before writing. PII tasks query and store nothing.
5133
+ - `ahub doctor --stale-anchors [--json]` is read-only, bounded and advisory. Only positive absence at the reported current head means stale. Private, ambiguous, unverified and exhausted evidence stays unknown with explicit coverage/truncation. It never mutates notes or grants authority.
5134
+
5135
+ ## Amendment: phase 3 follow-up query and cleanup bounds (#684)
5136
+
5137
+ - Reuse a filled exact-tree symbol-preparation key across prepare-only and completion. Within a pass, memoize unknown and known exists results by query name; preserve privacy/provenance rechecks and optional fail-open results.
5138
+ - Query explicit raw and normalized symbol forms together for historical recall; never copy, supersede or rewrite old notes.
5139
+ - Unsupported `#`, `->` and `:` name forms remain unknown. Final absence rows are labelled "absent from indexed definitions", retaining existing state/reason compatibility; no imported-package existence claim is inferred.
5140
+ - The doctor may use transient detached worktrees. Interrupted cleanup can leave prunable registrations; no automatic prune is added. Repeated owned cleanup failure remains neutral and borrowed handles retain caller ownership.
5141
+
5142
+ ## Amendment: memory code-neighbour projection (#684 phase 4)
5143
+
5144
+ - Optional caller/callee edges are computed at read time through code-facts, one hop only, and stamped with the exact current HEAD. Require a unique path-qualified seed; hide ambiguous and name-only edges. Store no code structure or edges in amem.
5145
+ - Dashboard Memory's default is the stored graph. An explicit Code neighbours toggle adds dashed calls and highlights attached notes, fails first; the accessible edge list includes direction and SHA. Prepare asynchronously outside the synchronous snapshot budget, then recheck current policy/profile/HEAD.
5146
+ - Symbol-start `hub_memory_graph` merges the overlay inside the existing node/edge and 6144-byte response caps. Code coverage and truncation are explicit, including unknown evidence. Authored about/rules_out associations remain distinct from derived calls.
5147
+ - Preparation bounds: four seeds, 32 distinct neighbours, 64 call edges, 16 screened notes and a two-second query-start allowance; existing in-flight code-facts timeouts remain. PII tasks/turns query nothing, and names pass publicMetadataText before disclosure.
5148
+ - Native task briefs may prepare asynchronously, merge direct and neighbour-attached fails/decisions first, and preserve their current combined item/line cap, deduplication and fail-open policy. Snapshot the item cap once before selection; reloads apply to the next brief. The console detail reports the bounded selected-note count, not a complete inventory.
5149
+
5150
+ ### Round-1 optional-read scheduling bounds
5151
+
5152
+ - Keep screened direct task advice independent of overlay eligibility and validity. Board movement, foreign scope or a private owner turn may drop optional code context but must not drop the existing direct brief. PII tasks still make no optional query.
5153
+ - Default CodeFacts review/completion queries have priority over optional neighbours. Interrupt an active optional CLI, await all existing owned cleanup and then admit priority work; do not run native tree preparations concurrently. Optional unknowns have a bounded five-second TTL and never poison priority cache lookups.
5154
+ - Dashboard code work requires the Memory view to be open and visible. Derive starts share a five-second floor; known topology lives at most thirty seconds, with current note rows refreshed before publication. A single hardened native HEAD probe establishes a bounded HEAD/ref proof; cache checks do not launch Git.
5155
+ - Console task counts use cache-only optional context and return unknown on a miss within the existing task-detail deadline. Snapshot the native brief cap before awaits. Preserve stored nodes when adding derived edges and apply tier/kind/peer filters to neighbour notes; a non-symbol selection resets the toggle.
@@ -327,7 +327,9 @@ the runner (refusing while one is alive) and append to the receipt's audit:
327
327
  live instance through the lifecycle stop, which verifies the instance and
328
328
  waits until its manifest and registry claim are gone, and is then inspected
329
329
  again. A daemon this operation does not own, or one already released, is left
330
- running. Each project's outcome is recorded as it happens; a disposition that
330
+ running. Its audit entry keeps the operation's error at that moment as
331
+ `cause` (#776): the error itself is cleared on disposal, and without it an
332
+ abandoned operation could not be diagnosed. Each project's outcome is recorded as it happens; a disposition that
331
333
  stops partway keeps the lock, and resume and `--fresh-session` then refuse
332
334
  until `--stop-and-archive` is run again and finishes. A stopped project's committed
333
335
  snapshot of this operation moves to `restart.abandoned.<hash>.json`, which is
@@ -0,0 +1,52 @@
1
+ # Always-ask adapter receipt (#636 slice 3)
2
+
3
+ The host sees and gates received Codex command approvals and every local/Pi model tool call. Codex remains listed as `always-ask not enforceable`: a native call can execute without sending an approval request. This is a capability ceiling, not a statement that all native calls were observed.
4
+
5
+ ## Native Codex boundary
6
+
7
+ On 2026-10-24, the installed `codex-cli 0.163.0` ran under an isolated HOME and CODEX_HOME with a fresh project directory. The environment contained only the explicit executable PATH and those two isolated homes. Version, schema generation and app-server initialize completed with zero stderr bytes. No login, provider request, model turn or real personal Codex home was used. Native initialization confirmed 0.163.0 and the isolated home. Host and installation identifiers are omitted here.
8
+
9
+ The generated ServerRequest schema SHA-256 is `f339be472737a0003efa25fba2e6e6c9237e621cd065b6d6995c51256e9dc1fb`. It defines:
10
+
11
+ - `item/commandExecution/requestApproval`: threadId, turnId, itemId and startedAtMs; command/cwd may be null; kind may be command or writeStdin. The gate handles a reported command, excluding writeStdin, and binds its answer to the observed live turn. Its response is accept or decline.
12
+ - `execCommandApproval`: conversationId, callId, command argv and cwd. The gate preserves argv for classification and answers approved or abort.
13
+ - File-change and permission-profile approvals carry different payload contracts. They remain native, as do absent/unknown command payloads.
14
+ - Native approval policy includes `never`. The existing hub mode mapper sends approvalPolicy never for never-ask. The proxy has no pre-tool path when no RPC is emitted. Received-RPC tests prove gating in all hub modes, not interception of an absent request. No authenticated native never-ask execution was attempted.
15
+
16
+ Codex and ACP approval copy stays privacy-screened; a cut, withheld or changed command is deny-only. The local/Pi on-prem console receives the complete tool title as described below. An intercepted id is answered once and suppresses guessed, duplicated or late TUI replies. Thread/turn replacement, completion, interruption, disconnect and stop invalidate pending decisions before effects. A missing current native turn denies. `ponytail:` each socket retains at most 1024 intercepted ids; reaching the cap closes both sides instead of evicting late-reply fences. The upgrade is an explicit reattachment/session-lifetime protocol.
17
+
18
+ ## Local and Pi boundary
19
+
20
+ The shared tool runner passes its parsed arguments into the existing host permit callback. The host fixes cwd/workdir to the execution root it actually uses and runs the same action classifier before never-ask, ask-when-needed and Pi's per-tool grant. A classed call waits for a person's allow-once or denial even in an unattended daemon. Only the separately labelled explicit class-budget option opens a bounded allowance. Unknown calls still follow their existing mode. All existing path revalidation, readonly and sandbox checks remain in place after approval.
21
+
22
+ Pi's persisted call receipt contains the classification and approval operation, so replaying the same call id spends no second allowance. Its verified person-authored idle shell retains its existing person-command path. No model tool is relabelled as a person command.
23
+
24
+ The managed terminal and headless Claude forms remain in the status table: terminal evidence must match a live hooked generation; the owned headless form installs the managed hook and accepts only current-child allow-once/denial through its native prompt tool. Native requirements are never relaxed by a cached class budget.
25
+
26
+ A cut approval command is marked `[cut, N chars]` and offers only denial, with no allow-once or multi-call budget. Whole-command privacy screening precedes the cut. `ponytail:` startup still reads untracked machine-local action policy without retained person confirmation. Weakening defaults is visible as attention in status; the upgrade is a retained person-bound startup confirmation.
27
+
28
+ ## Verification
29
+
30
+ The dedicated tests use loopback fake transports and fresh temp projects with no git remote. The harmless marker before `git push` proves whether a denied or stale call executed without requiring network activity. Native sandbox-exec provides the allowed-effect leg on macOS; Linux preserves the explicit unavailable-sandbox refusal rather than widening execution.
31
+
32
+ Pinned Bun 1.4.2 typecheck and 74 focused tests passed (670 assertions), including real daemon Local/Pi approvals, managed headless Claude status, shared ACP/Claude regressions and the registry guard. Replacing the shared host gate with an unconditional native fallback made both new real-daemon tests fail named assertions, with no timeout; the original source was restored byte-for-byte. The full changed gate is recorded in the PR handoff. Real provider/model turns remain unverified. PR #749's budget kind/source fields and its token counters are separate from these per-session action allowances; its daemon and documentation hunks are retained when rebasing.
33
+
34
+
35
+ ## Rebase onto 0.15.0 and crash-status regression
36
+
37
+ The branch keeps the 0.15.0 release changelog section and its Jcode/headless-Claude documentation recheck, with this feature under Unreleased. The generated bundle was rebuilt with frozen dependencies and Bun 1.4.2. Runtime and test code were unchanged by the rebase itself.
38
+
39
+ A subsequent bot finding exposed an unexpected-exit status gap: Pi.fail marks the peer offline without setting its requested-stop flags, so acceptingTools alone could still report active enforcement. The status projection now requires both a non-offline state and acceptingTools. A real daemon regression makes its fake native Pi process exit with code 7; before the fix it fails the named enforceable=false assertion, and after the fix status is not enforceable and the stopped tool endpoint performs no effect. Typecheck, the 12 relevant tests (187 assertions), and bundle freshness pass. All test/gate invocations clear inherited AGENTHUB_HOME per the current area note. The interrupted pre-fix gate is not counted as passing proof; the final changed gate is reported separately.
40
+
41
+
42
+ ## Round-one presentation and status boundaries
43
+
44
+ The person's on-prem local/Pi console now receives the complete host tool title after the class line, even for a private turn. The host-only optional presentation port must match the exact executed bash/git form. A shortened, withheld or mismatched view is deny-only and carries no class-budget choice. ACP/Codex consumers also enforce the deny-only flag against forged answers. Their unknown/unclassed native flow is unchanged. A public positive budget control remains covered; the former private-command budget-acceptance expectation is replaced with denial because approval of hidden commands is the rejected behavior.
45
+
46
+ Red-first real daemon controls reproduced both failures: a 793-character command lost its significant tail at the public 512-character cut, and a PII-turn command became wholly withheld. They now verify full console visibility, denied effects and allow-once behavior, plus absence of private material from logs, status, ordinary peer messages and subsequent model history. Pi has its own long-command control. Incomplete host-title, native cut/withheld and forged grant controls deny.
47
+
48
+ Managed Claude implementer status requires a current busy child generation. An idle instance with no process is not enforceable (also fixes #402); the idle assertion failed before that correction, while the existing active-child positive control remains. Local action allowances now use a dedicated session port and no longer widen contextSession for checkpoints, telemetry or supervision.
49
+
50
+ Legacy Codex execCommandApproval has a conversation/call id but no provable native turn id by itself. Without an observed active turn from turn/started it is auto-declined, without a person request; no turn identity is invented. A dedicated legacy no-turn control pins this fail-closed compatibility ceiling. Native 0.163.0 command payload shapes were probed model-free; real legacy-provider behavior remains unverified.
51
+
52
+ All actual conflict markers in operations.md were removed, preserving the intended local-policy text and its open startup-confirmation ceiling. The first-party tracked/untracked tree scan (excluding .git and node_modules) returns zero markers. Relevant focused checks pass 59 tests / 648 assertions; final gate results are in the PR handoff.
@@ -0,0 +1,69 @@
1
+ # amem runtime integration receipt
2
+
3
+ Task #392, agent-mem#13 Design F and AC7. The branch consumes the published first-party library, without vendoring it. This receipt describes fixture evidence, not a personal-memory migration or a live provider/model run.
4
+
5
+ ## Publication
6
+
7
+ A cache-busting npm registry read on 2026-10-24 reported latest 0.6.0, published 2026-10-23T19:15:25.981Z, gitHead `2a182f2f23b7744a746b6854f9fb920df627a99f`. It contains P3 squash `0df2cf91`. The manifest and lock exact-pin 0.6.0; Bun 1.4.2 installed it before bundle generation.
8
+
9
+ ## AC7 mapping
10
+
11
+ - Recall: actual temporary MemoryStore rows supply bounded curated recall; canonical project scope ignores the old caller's basename chain. Complete-row host screens exclude another project and private rows before selection. Native long-term/fails-first recall remains; supplemental short-term context replaces model-produced worker observations.
12
+ - Capture: real published CaptureStore receives deterministic lifecycle/tool records, closes per append, and owns cap, retention and contention replay. A duplicate tool call is captured once; repeated Stop events at the same clock still remain distinct. Complete input/output is screened before the excerpt bound. Denied paths and memory bookkeeping are excluded. Capture rows never become curated ask/brief evidence.
13
+ - Ask: genuine full `mem:` ids survive evidence generation and citation validation. A fake model's correctly cited answer is accepted using only actual stored evidence. Model answers/checkpoints are pending candidates; candidate text retains submitted metadata and kind. Old model-answer titles are excluded from curated projections.
14
+ - Briefs: actual library rows supply search and neighbourhood results using published SearchIndex after screening. Native graph/fails-first presentation stays authoritative. Full ids do not invent legacy observation numbers.
15
+ - No worker: the actual-client integration runs with fetch blocked and a worker tripwire listener that receives zero requests. Runtime client/capture expose no URL or worker endpoint dispatch. HTTP retained in test/fakes/mem-worker.ts is an explicitly injected host-only fake for historical ordering/privacy tests.
16
+ - Privacy/profile: a real daemon with malformed selected-profile metadata completes two public local read turns and answers status while no capture database or spool is created, including SessionEnd. Selector bytes remain unchanged. Private turns remain suppressed by LocalPeer before capture; existing PII/secret/path tests retain their assertions.
17
+ - Gate: required full `scripts/check.sh` and its exact head/check tail are recorded in the task handoff. Both OS CI is a separate reviewer check.
18
+
19
+ ## Validation and boundaries
20
+
21
+ Pinned typecheck, bundle generation and 309 focused tests (1940 assertions) pass. A further 19 tests (265 assertions) verify the privacy change and profile fence. Reverting full-id citation support and bypassing complete-event capture screening each causes a named actual-store test to fail; original source is restored afterward. Final full-gate results supersede these focused checks.
22
+
23
+ OCR delegation selected 11 reviewable files. Independent source review covered the eight production modules and manifest; generated bundle is checked by regeneration/freshness, and the test-only HTTP fixture is classified separately. It found three concrete issues (citation grammar, event identity/time and unavailable-profile capture), all fixed and rechecked, plus a misleading successful-save suffix, removed. Excluded tests/docs/lock are not claimed as OCR-covered. No reviewer verdict or CI result is inferred from this local review.
24
+
25
+ Accepted behavior changes and ceilings are in docs/memory.md. Supplemental reads refuse above 4096 curated rows; policy-aware paging is the upgrade path. Duplicate-call timestamps retain 1024 newest ids; older repeated ids can capture again, and a host-supplied source-event timestamp is the upgrade path. Candidate text including provenance must fit the package's complete 4000-character bound or save is unavailable. `memory.native=false` deliberately disables curated reads/writes; enabled capture still requires its own selected-profile binding. `memory.worker_url` is ignored. Doctor reports installed library availability, not a store health proof. Historical benchmark arm labels are not evidence of a live worker or comparable new collection.
26
+
27
+ No personal memory bytes were migrated, no claude-mem worker was stopped, and no release or publication is performed by this PR. The person-run cutover, real device sync and P6 resource/retrieval week remain separate.
28
+
29
+
30
+ ## Full-gate contract refresh
31
+
32
+ The first full gate at `eb35d8b6` completed 6712 tests: 6708 pass, three obsolete contract assertions failed, one installed legacy/new consumer-parity leg skipped, and no current-invocation daemon leaks. The failed assertions expected the retired doctor worker row and duplicate auxiliary saves after a native write. Their replacements assert the exact-pinned library row, no worker row, native payload/kind preservation and zero auxiliary copies. A further 15 tests (133 assertions) pass, including a valid production daemon with a legacy worker tripwire URL: curated recall and control ask work, deterministic lifecycle/tool capture persists, raw capture is absent from evidence, and the listener receives zero requests. Reverting the unavailable-profile capture fence fails its actual daemon test on capture artifacts. Source is restored after each mutation.
33
+
34
+ New memory_brief events identify source amem while retaining the old scalar claudeMem count for log-reader compatibility. Doctor attests the installed version against the exact pin without opening a memory store. The installed legacy/new consumer-parity skip is retained as unverified; this environment did not provide an installed ahub for that leg. Final required full-gate results are in the handoff, not inferred from focused checks.
35
+
36
+
37
+ ## Capture invocation identity
38
+
39
+ The e90388b3 full suite passed 6712 tests with one installed-consumer skip and no leaks, but a separate actual-store probe found a missing case: two real turns reusing provider id call_0 produced only one tool row. A red-first real daemon test then executed four identical reads using call_0 twice per turn over two turns; all provider/control assertions passed before the capture count failed (expected 4, got 1).
40
+
41
+ Capture now receives the existing LocalPeer session/turn/step plus a host invocation counter as sourceId. Its timestamp cache and published-store receipt bind that identity, while the original provider toolUseId remains metadata. A same-clock direct test separately proves that one observation replays once and different host invocations remain distinct. The real daemon test and prior local-worker/privacy tests pass together (31 tests, 222 assertions). The modified host path retains its private-turn gate and tool exclusions. The final full gate and exact fixed head are recorded in the handoff; the earlier green head is not claimed as proof of this fix.
42
+
43
+
44
+ ## Structured write-policy boundary
45
+
46
+ The invocation-fixed 8b8b2025 full gate passed 6713 tests with one installed-consumer skip, no failures and no leaks (882.56 s). A later write-boundary probe found that JSON-escaped strings could bypass a whitespace PII pattern. The first probe used an inadmissible newline pii.paths entry and is not counted as production evidence. The corrected red-first tests use the valid routing pattern private\s+folder; raw path/value/key/title and a private JSON output tail beyond character 4000 are refused. A valid space-containing private-path policy also exposed cwd metadata falsely suppressing public events. These tests and complete custom-deny-path checks failed seven named assertions against 8b8b2025, without timeouts.
47
+
48
+ Write policy now sees literal structured keys/values (including key: value secret pairs) and encoded actual data. Known tool input/output JSON is decoded once from full source text; storage bounding follows screening. Non-persisted host cwd/session/source identifiers are separated from content while canonical project/profile fences remain. The published store still owns final normalization. Public controls prevent a deny-all implementation from passing; the old fake HTTP response alone retains its legacy 4000 cap. Focused typecheck and 39 tests/276 assertions pass. A follow-up red-first numeric-password case fails on the string-only projection and passes when finite JSON scalars also contribute key: value pairs; the failed assertion received a stored capture id. The red-proof wrapper initially expected an error label Bun did not print, so its exit was not treated as the test result; the named assertion and restored source were checked directly. The earlier green full heads are not claimed as proof of this final privacy delta; the handoff records its own exact full-gate head/result.
49
+
50
+
51
+ ## PR 752 first-review corrections
52
+
53
+ The obsolete worker-platform whitelist is removed from both the client and recall caller. Actual-store notes by local, jcode, Claude and Codex are delivered to every receiving peer, including its own notes; privacy and canonical-project exclusions stay before rendering. Supplemental reads now use the public bounded exact-project keyset pages, keep the newest candidates, and report truncation in recall/brief text and control status. Other projects' shared rows no longer displace these supplemental rows; native shared long-term recall remains. Relation metadata folding is a core cost still outside the row-body bound.
54
+
55
+ The production native ask source now emits full memory ids, matching the client before dedupe and evidence budgeting. Legacy short-id host fixtures canonicalize only when one full id uniquely matches the prefix. Colliding full ids remain distinct. Capture reuses its profile-bound handle, reopens on changed effective R6 config before the next append, releases it on refusal/shutdown, and persists relative file paths instead of commands or absolute paths. The substring fallback matches separate terms. The legacy fallback invocation ceiling is explicit; live LocalPeer always supplies its own invocation identity. Stale agent-facing worker text and the identical-branch ternary are removed. Final exact-head test and CI state belong to the handoff; earlier green heads do not establish this delta.
56
+
57
+
58
+ First-review validation before the final gate: pinned Bun 1.4.2 typecheck and bundle rebuild pass; 153 focused tests across nine files pass with 6644 assertions. Four restored-old-behavior probes each fail a named assertion for all-author recall, bounded page reads, ask id dedupe and relative-only path metadata, then restore source byte-for-byte. The cached-handle regression also changes home R6 bounds from 512 MB/90 days to 1 MB/1 day and proves the next append reopens the handle and removes the expired row. A real daemon control response and the person's status renderer both expose truncation. The required full-gate result and its exact head follow in the task handoff, not inferred from this focused run.
59
+
60
+
61
+ ## PR 752 second-review CI correction
62
+
63
+ At `e288b1a4`, run 40028391482 failed Ubuntu's cancellation-permission fixture. macOS job 120670470231 had no failing assertion: the cancellation test and both document-baseline tests passed, and the suite was still passing benchmark tests when the 1260-second whole-suite watchdog terminated it (exit 143, no invocation daemon leaks). No snapshot fixture change was needed for that run.
64
+
65
+ Only the cancellation late-answer fixture now holds the existing Bun fake-clock/timer ports after live child setup and before the permission request. It awaits a native WebSocket permission push instead of polling a fake clock, then cancels and answers allow while expiry and heartbeat remain held. The existing permit acknowledgement and final deny assertions are unchanged, with extra held-clock/timer/restoration controls. Real timers are restored in finally before daemon cleanup; ordinary expiry and stop tests retain real timers. A native WebSocket probe shows I/O remains live with both timer paths held. All 15 permission/projection tests pass (172 assertions). Removing the current-task cancellation guard fails the named final deny assertion, then the exact source is restored.
66
+
67
+ Per the progressing-cutoff rule in tests.md, hosted macOS alone gets a 1440-second whole-suite watchdog and a 30-minute job ceiling. Ubuntu remains at 1080 seconds and 25 minutes, and every individual test keeps its 20-second limit. Production permission, snapshot and P3 resource limits are unchanged. Exact-head full-gate and CI status are recorded in the handoff.
68
+
69
+ Pinned typecheck passes. The installed actionlint parser rejects the pre-existing concurrency.queue key identically on the baseline and modified workflow; it reports no additional diagnostic, and a successful actionlint run is not claimed. GitHub CI remains the workflow execution proof.
@@ -0,0 +1,55 @@
1
+ # Codex 0.164.0 fresh-reviewer isolation receipt
2
+
3
+ Date: 2026-10-24. Platform: macOS arm64. Scope: model-free production `codexReviewerBackend.prepare`; no `codex exec`, model request, account login, credential copy or live review was performed, and the person's real home and `~/.codex` were never passed to the Codex CLI. This extends the [0.163.0 receipt](2026-10-22-codex-reviewer-isolation.md) (and through it the [0.146.0 receipt](2026-10-19-reviewer-isolation.md)), retaining their limitations. Issue: #757.
4
+
5
+ Result: every leg passed. `0.164.0` is added to `COMPAT["codex-review"].verified`.
6
+
7
+ ## Source identity
8
+
9
+ The upstream annotated tag `openai/codex` `rust-v0.164.0` resolves to tag object `fe3b29455488c2e189e43ad5526a107900c5e1b7`, peeled commit `6aaac694803004a63c6f393d153c96d22521a6b9` (`git ls-remote` readback of `refs/tags/rust-v0.164.0` and `refs/tags/rust-v0.164.0^{}`, 2026-10-24). For comparison the 0.163.0 readback in the same call was tag object `129b8ec7ee6f4511557a4856279f529025c25af4`, peeled commit `83086188ae0614bd882818ff1aa23f3194962012`, matching the earlier receipt.
10
+
11
+ Each file below was fetched at the peeled commit (HTTP 200) and the quoted region was read, not only located. Lines moved since 0.163.0 (the Apps gate and the notify wiring in particular), so the claims were rechecked against the code and not against the old line numbers.
12
+
13
+ - [Exec switches](https://github.com/openai/codex/blob/6aaac694803004a63c6f393d153c96d22521a6b9/codex-rs/exec/src/cli.rs#L40): `--ephemeral` (no session files persisted), `--ignore-user-config` (`$CODEX_HOME/config.toml` is not loaded; auth still uses `CODEX_HOME`) and `--ignore-rules` (user and project execpolicy `.rules` are not loaded) are still global exec switches. They do not remove every config layer, so the host still fences config files itself.
14
+ - [Apps gate](https://github.com/openai/codex/blob/6aaac694803004a63c6f393d153c96d22521a6b9/codex-rs/core/src/mcp.rs#L458): the implicit Apps connection is registered only when `apps_enabled` holds and the session is not `SessionIsolation::Isolated`; otherwise the compatibility registration is removed. A session with MCP servers disabled sets `apps_enabled = false` ([L279](https://github.com/openai/codex/blob/6aaac694803004a63c6f393d153c96d22521a6b9/codex-rs/core/src/mcp.rs#L279)). The configured MCP inventory alone cannot show the runtime compatibility server, so effective feature readback of Apps stays a required leg.
15
+ - [Plugin manager](https://github.com/openai/codex/blob/6aaac694803004a63c6f393d153c96d22521a6b9/codex-rs/core-plugins/src/manager.rs#L823): `plugins_for_config` returns `PluginLoadOutcome::default()` (no plugins) when `plugins_enabled` is false. [MCP schema](https://github.com/openai/codex/blob/6aaac694803004a63c6f393d153c96d22521a6b9/codex-rs/config/src/mcp_types.rs#L240): `enabled` defaults true and a false server is not initialized; [`enabled_tools`](https://github.com/openai/codex/blob/6aaac694803004a63c6f393d153c96d22521a6b9/codex-rs/config/src/mcp_types.rs#L288) is an allow-list ("only these tools will be registered") and `disabled_tools` is applied after it.
16
+ - Legacy `notify` is independent of the hooks feature. [Session construction](https://github.com/openai/codex/blob/6aaac694803004a63c6f393d153c96d22521a6b9/codex-rs/core/src/session/mod.rs#L5581) copies `config.notify` into `legacy_notify_argv` while `feature_enabled` is set separately from `Feature::CodexHooks`; the [hook registry](https://github.com/openai/codex/blob/6aaac694803004a63c6f393d153c96d22521a6b9/codex-rs/hooks/src/registry.rs#L133) builds an after-agent hook from a non-empty `legacy_notify_argv` without consulting that flag, and the session comment ([L2343](https://github.com/openai/codex/blob/6aaac694803004a63c6f393d153c96d22521a6b9/codex-rs/core/src/session/mod.rs#L2343)) says legacy notify settings stay session-static. The host therefore keeps pinning `notify=[]` in both metadata proofs and the returned exec argv. This is a source and argv guarantee, not a model-driven hook execution test.
17
+ - [Seatbelt compiler](https://github.com/openai/codex/blob/6aaac694803004a63c6f393d153c96d22521a6b9/codex-rs/sandboxing/src/seatbelt.rs#L624): `build_seatbelt_unreadable_glob_policy` still turns each unreadable glob into anchored read and write denies, canonicalizes the static prefix (`canonicalize_glob_static_prefix_for_sandbox`), keeps the `/**/` literal-basename form and denies renames of every ancestor directory that could hold a match.
18
+
19
+ ## Native production preparation
20
+
21
+ The installed fnm launcher for 0.164.0 was selected explicitly (`command: [<codex.js>]`), without changing `codex_bin`. The production resolver maps it to the native `@openai/codex-darwin-arm64` executable and hashes its bytes. Binary identity: `sha256:5b9983835177b544bba49d54b6679faf2f833a8b278068a158698f5c4eef1395`.
22
+
23
+ How it was run. A throwaway producer (not committed) made a fresh temporary root with separate HOME, CODEX_HOME and project directories inside it, and started the production call in a child with a cleared environment: only PATH, TMPDIR, LANG, the two temporary homes and a marker for the temporary root. The child refused to run if HOME was the person's real home, if `os.homedir()` differed from HOME, if HOME, CODEX_HOME or the project was not inside the temporary root, or if any other variable was inherited. The hub transport was the inert `/usr/bin/false` with no token. The actual `codexReviewerBackend.prepare` was invoked with a fresh session id, an abort signal, a fixed prompt and private paths (`**/*.env`, `.private`, `nested/secret`, `~/private-proof`). It returned a launch description; that launch was never executed, and the temporary root was removed afterwards (no run directory was left).
24
+
25
+ Runs:
26
+
27
+ - Before the compat change, 0.164.0 with empty temporary homes: refused at the version leg only (`causeClass: version`, "Version not verified: ... 0.164.0 (verified 0.146.0, 0.163.0)"), in 0.13 s. This is the person's reported refusal, reproduced model-free.
28
+ - With `0.164.0` added, 0.164.0 with empty temporary homes: passed every production admission leg (0.25 s).
29
+ - With `0.164.0` added, 0.164.0 with synthetic ambient configuration in the temporary CODEX_HOME (a legacy `notify` command, Apps, plugins and hooks switched on, one stdio and one HTTP MCP server): passed again (0.26 s). Ambient servers were disabled stubs and only `agent-hub` stayed enabled with exactly the two review tools.
30
+ - Retained 0.146.0 executable with the same policy and empty temporary homes: passed (0.40 s), binary identity `sha256:4a6398187165c5bd75ad92c9a6fba6df6d401a59b62f7e1ecf9218a8196852a6`, unchanged from the earlier receipts.
31
+
32
+ Every successful launch returned `backend: codex`, `readOnly: true`, `strictMcp: true`, `ephemeral: true`, `current(): true`, `notify=[]` in its argv and no model run. The recorded evidence list was identical for all three passing runs: `model-free-mcp-inventory-disabled-ambient`, `hub-tools-exact-allowlist`, `plugins-and-hooks-disabled`, `legacy-notify-disabled`, `apps-disabled-effective-feature-readback`, `web-search-disabled`, `project-untrusted`, `native-public-read-private-deny-write-deny`, `native-glob-and-symlink-private-deny`, `portable-core-name-and-state-deny`, `standard-host-credential-roots-denied`, `binary-config-stable`, `binary-hash-once-metadata-fence`, `session-ephemeral`.
33
+
34
+ | Native canary | Required observed result | 0.164.0 |
35
+ | --- | --- | --- |
36
+ | Public file | Read allowed | passed |
37
+ | Exact private path | Read denied | passed |
38
+ | Nested `**/*.env` private path | Read denied | passed |
39
+ | Symlink alias into private glob | Read denied | passed |
40
+ | Portable-core `.env` and `.agenthub/state` | Read denied | passed |
41
+ | Synthetic host gh, kube and docker credential roots | Read denied | passed |
42
+ | Symlink alias into synthetic credential root | Read denied | passed |
43
+ | New file creation | Write denied; file absent | passed |
44
+
45
+ The producer inside `prepare` requires unique effective Apps, plugins and hooks rows with value false, exact MCP transport and tool readback, a bounded bundled-model catalog check, stable binary and config metadata, public read success and all of the canaries above; a failure of any one refuses with a closed cause class (#722). None failed.
46
+
47
+ ## Compatibility change
48
+
49
+ `COMPAT["codex-review"].verified` becomes `["0.146.0", "0.163.0", "0.164.0"]` (`src/adapters/compat.ts`). Another version is still refused unless the person runs `ahub agents trust codex-review <version>`, and every runtime isolation proof still runs after trust. The fixtures that used 0.164.0 as their example of an unverified version now use 0.165.0, and `test/compat.test.ts` covers the new entry and its neighbours (0.164.1, 0.165.0, a prerelease and a prefixed string stay refused).
50
+
51
+ ## Evidence boundaries
52
+
53
+ Unchanged from the 0.163.0 receipt. The network setting remains configured disabled, without a separate network probe. Web search is requested disabled; the effective model-turn mode is not read back. Returned exec argv is checked, but no run proves exec selecting the named default permission profile. No account or model review, TUI interaction or native Linux result is claimed. The runs exercise the installed macOS binary; same-user filesystem or config mutation after the final metadata check remains outside atomic snapshot guarantees. The supported credential-root list is bounded, not a classifier for every private host file. The source claims are read at the 0.164.0 tag only; a later 0.164.x is a different version and is not covered. Hosted CI and the changed-source gate are separate code-verification evidence recorded on the PR.
54
+
55
+ Each Codex upgrade still repeats these steps by hand; making the producer a committed one-command script is the follow-up named in #757, not part of this receipt.