@staix/agent-hub 0.13.0 → 0.15.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 (92) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +19 -7
  3. package/docs/agent-notes/README.md +4 -0
  4. package/docs/agent-notes/adapters.md +18 -4
  5. package/docs/agent-notes/budget.md +5 -4
  6. package/docs/agent-notes/bus.md +1 -0
  7. package/docs/agent-notes/daemon.md +19 -7
  8. package/docs/agent-notes/tasks.md +15 -7
  9. package/docs/agent-notes/tests.md +4 -4
  10. package/docs/dogfood.md +18 -10
  11. package/docs/events.md +21 -7
  12. package/docs/operations.md +131 -27
  13. package/docs/profiles.md +1 -0
  14. package/docs/quickstart.md +13 -3
  15. package/docs/sdlc.md +40 -2
  16. package/docs/security.md +45 -14
  17. package/docs/specs/2026-09-19-agent-hub-design.md +112 -10
  18. package/docs/verification/2026-10-22-codex-reviewer-isolation.md +14 -0
  19. package/docs/verification/2026-10-23-0.14.0.md +69 -0
  20. package/docs/verification/2026-10-23-0.15.0.md +80 -0
  21. package/docs/verification/2026-10-23-daemon-responsiveness.md +44 -0
  22. package/docs/verification/2026-10-23-headless-claude-permissions.md +47 -0
  23. package/docs/verification/2026-10-23-terminal-claude-execution-root.md +41 -0
  24. package/docs/verified.json +34 -17
  25. package/package.json +1 -1
  26. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  27. package/plugins/agent-hub/server.js +1078 -327
  28. package/src/adapters/acp.ts +43 -7
  29. package/src/adapters/claude-channel.ts +26 -4
  30. package/src/adapters/claude-implementer.ts +243 -0
  31. package/src/adapters/claude-print-child.ts +90 -0
  32. package/src/adapters/claude-task-sessions.ts +80 -0
  33. package/src/adapters/codex-review-isolation.ts +18 -7
  34. package/src/adapters/compat.ts +4 -0
  35. package/src/adapters/conformance/enrollment.ts +2 -1
  36. package/src/adapters/conformance/fakes/acp-server.ts +3 -1
  37. package/src/adapters/definitions/builtin-metadata.ts +15 -3
  38. package/src/adapters/definitions/native-channel-state.ts +1 -1
  39. package/src/adapters/definitions/native-cli-entry-claude.ts +13 -8
  40. package/src/adapters/definitions/native-cli-entry-codex.ts +13 -0
  41. package/src/adapters/definitions/native-cli-entry-types.ts +2 -0
  42. package/src/adapters/definitions/native-factories.ts +8 -0
  43. package/src/adapters/kimi-wire-usage.ts +303 -0
  44. package/src/adapters/registry-types.ts +7 -3
  45. package/src/adapters/registry.ts +14 -2
  46. package/src/adapters/reviewer-print.ts +64 -60
  47. package/src/cli/console-config.ts +123 -0
  48. package/src/cli/console-state.ts +28 -1
  49. package/src/cli/console.ts +25 -1
  50. package/src/cli/facts-hook.ts +9 -5
  51. package/src/cli/help.ts +6 -4
  52. package/src/cli/launch.ts +10 -4
  53. package/src/cli/main.ts +52 -9
  54. package/src/cli/output.ts +75 -2
  55. package/src/cli/reset.ts +1 -1
  56. package/src/cli/settings-edit.ts +89 -0
  57. package/src/cli/status-lines.ts +1 -1
  58. package/src/cli/terminal-recovery.ts +1 -6
  59. package/src/hub/action-enforce.ts +82 -0
  60. package/src/hub/action-policy.ts +22 -10
  61. package/src/hub/board.ts +48 -3
  62. package/src/hub/budget.ts +5 -0
  63. package/src/hub/bus.ts +5 -5
  64. package/src/hub/claude-launch-proof.ts +33 -0
  65. package/src/hub/config-apply.ts +5 -0
  66. package/src/hub/config-edit-host.ts +173 -0
  67. package/src/hub/config-edit.ts +282 -0
  68. package/src/hub/config-tier.ts +89 -0
  69. package/src/hub/control-client.ts +24 -3
  70. package/src/hub/daemon.ts +450 -71
  71. package/src/hub/deliverable-files.ts +131 -0
  72. package/src/hub/deliverables.ts +125 -0
  73. package/src/hub/delivery-journal.ts +18 -2
  74. package/src/hub/events.ts +3 -1
  75. package/src/hub/hub-tools.ts +21 -4
  76. package/src/hub/instances.ts +19 -6
  77. package/src/hub/kimi-usage.ts +144 -0
  78. package/src/hub/manager.ts +3 -3
  79. package/src/hub/peers.ts +7 -0
  80. package/src/hub/permission-payload.ts +15 -0
  81. package/src/hub/routing.ts +118 -3
  82. package/src/hub/sdlc-metrics.ts +260 -0
  83. package/src/hub/sdlc.ts +5 -3
  84. package/src/hub/settings.ts +1 -1
  85. package/src/hub/spec-read.ts +31 -0
  86. package/src/hub/task-code-group.ts +52 -0
  87. package/src/hub/tasks.ts +374 -37
  88. package/src/hub/token-window.ts +33 -0
  89. package/src/hub/ui.ts +3 -0
  90. package/src/hub/usage-rollup.ts +74 -6
  91. package/src/hub/workflow.ts +16 -0
  92. package/src/ui/index.html +388 -3
@@ -431,8 +431,12 @@ inside a peer.
431
431
  update whenever it names the current session, since 2.1.1's source text emits it
432
432
  after the prompt resolves (an order read from the source, not yet observed live
433
433
  through the hub; docs/smoke.md). A gate crossing records the
434
- event and notice for an ACP peer but sends it no checkpoint request yet. Only validated cumulative totals or input/output
435
- pairs feed native token readings; unsupported shapes remain unknown. `ahub budget set`
434
+ event and notice for an ACP peer but sends it no checkpoint request yet. Since #285 phase 2 the soft ceiling's counter is the
435
+ hub's own count of Kimi's session wire log (`src/adapters/kimi-wire-usage.ts`, wire protocol 1.5, read on Kimi 2.3.0; the issue
436
+ read 2.1.1), not an ACP total: each `usage.record` row of the owned session, sub-agents included, is a `usage` event
437
+ (`source: kimi_wire`) and a `tokens` event, and the rolling 5 h sum of `inputOther + inputCacheCreation + output` (cache reads are
438
+ recorded, never counted) is the `tokens` window, source `hub count, kimi wire log, of kimi_tokens_5h N`. Any read the reader
439
+ cannot trust gives unknown (one notice, the reading dropped), never a short count. `ahub budget set`
436
440
  feeds a reading by hand. `local` has no quota and is never paused.
437
441
  - Gate at `budget.gate` (default 0.9) on any fresh window; readings older than
438
442
  `budget.stale_min` are ignored. Checkpoint first, pause second: the peer gets one
@@ -927,6 +931,13 @@ This slice provides the pure classifier, machine-local policy, host-only per-ses
927
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.
928
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.
929
933
 
934
+ ## Always-ask enforcement: Claude and ACP (#636 slice 2)
935
+
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
+ - **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
+ - **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>`.
940
+
930
941
  ## Amendment: approval notifications and timeout (issue #5)
931
942
 
932
943
  - A waiting permission request raises one macOS notification (`osascript`) with
@@ -2463,6 +2474,19 @@ acts on the stopped state directory.
2463
2474
  - Control protocol: `settings_get`, `settings_set`, `settings_undo` and `settings_preview` are new console-role requests, and `ui` takes an optional `settings` flag and echoes it; an older hub answers the requests as unknown, and the CLI refuses a `ui` reply without the echo, so `PROTOCOL` stays 16. The `ui_snapshot` reply gains an optional `settings` object. Events schema stays 1.
2464
2475
  - `ahub settings` talks to the running hub (ponytail: a file-only path if setting a start-time value on a stopped hub is ever wanted).
2465
2476
 
2477
+ ## Amendment: structural configuration from the console and the dashboard (issue #689)
2478
+
2479
+ - The registry (`src/hub/settings.ts`) stays closed and unchanged. A second path, `src/hub/config-edit.ts` (plan and write) with `src/hub/config-tier.ts` (the tier rule) and `src/hub/config-edit-host.ts` (authority, confirmation, audit, scoped publish), edits structural keys. Tier 2: `instances.<id>.{kind, roles, start_mode, review_mode, readOnly, worktree, permission_mode}`, `review.{adaptive, min_reviews, pool, rerotate}`, `snapshots.keep`, `approvals.timeout_s`, `context.gate` (all `config.local.json`) and `sdlc.toml` `mode` and `required_checks`. Tier 3: the rest of `NEVER_EDITABLE` and an instance's `jcode_home`, `provider`, `model` and `provider_profile`. `roster`, `tasks.wip_limits` and the other registry keys are refused here with the registry command.
2480
+ - Validation is the daemon's own: `parseConfigSources` (raw bounds and instance declarations through `readInstanceConfig` and `validateInstances`), `loadConfig`, `checkJcodeInstanceHomes` for `config.local.json`, `sdlcConfig` for `sdlc.toml`. A loader's reason is returned as it is with the `field` it names. A candidate is built from the file on disk, and a request that would drop a tier 3 field of an existing instance (by replacing it with a tier 2 object) is refused.
2481
+ - Writes: revision check against the disk when planning and again before writing (sha256 of the file, or `absent`); a bounded backup in `<state dir>/config-backups/` (the newest five per file); a temp file and rename in the same directory with the file's mode; never a git-tracked `config.local.json`, a symlink or a file over its size limit (1 MiB, 64 KiB for `sdlc.toml`). `sdlc.toml` is edited line by line for `mode` and `required_checks` only, and the result is parsed and compared with the intended change before it is accepted.
2482
+ - Tiering reads the file: `configTier(file, path, value, held)` makes `instances.<id>` tier 3 to remove or replace when the instance holds `jcode_home`, `provider`, `model` or `provider_profile`, and `maskTier3` shows every tier 3 value in a reply, a diff line or a notice as `(set)`. `readEditable` refuses a symlinked `.agenthub` as well as a symlinked file, `viewRevisions` reports `unavailable` instead of throwing, and the sdlc editor keeps each untouched line's own line ending.
2483
+ - Authority: the console role (`config_edit_get`, `config_edit_preview`, `config_edit_apply`; `settings_get` also carries the file revisions) or a dashboard settings session (`config_edit_preview` and `config_edit_apply` actions; the manager forwards both). An ordinary session, a tools or peer connection and any agent shell are refused. A tier 3 key is refused for a dashboard session by the hub and is console only, with the key's name typed as the confirmation (one key per write). Recovery or shutdown holds edits.
2484
+ - Publication: `reloadConfig` stays the only path. The editor publishes each changed leaf with its own scope and registers that scope for the one reload (`structural` in `authorizeConfigLeaf`, admitted only for `config.local.json`, source `config` and `structuralLeaf`), so a settings session can publish a structural edit and nothing else it could not before. Instance declarations are restart-bound (`pending restart: instances`); `sdlc.toml` is read once at start and is added to `pendingRestart` in `status.json`, the console snapshot and the settings view until the running lifecycle equals the file.
2485
+ - Audit: a `config_edit` event (`file`, `paths` as `{path, before, after}` short value hashes, `fileBefore` and `fileAfter` revisions, `tier`, `source`, `by: "person"`, optional `reason`, `backup`). No value, no secret and no home path enters the event, the notice or `hub.log`.
2486
+ - Console: Settings key `c` (what to edit, each field, the hub's diff or loader reason, a reason, `y`; a tier 3 key asks for its name instead of `y`), `R` while a restart is pending prefills `restart`. CLI: `ahub settings edit` and `ahub settings backups`. Dashboard: a Structure group in Settings (forms inside a settings session, read only outside one, tier 3 as text with the console command, restart-pending badge, backups), both themes, 44 px targets, one column at phone width.
2487
+ - Control protocol: three new console-role requests and two new dashboard actions; no `PROTOCOL` bump, an older hub answers unknown and the CLI says to upgrade and restart it.
2488
+ - Unverified: a live `ahub ui --settings` browser session and a real restart that picks up an edited `sdlc.toml` (the person's leg).
2489
+
2466
2490
  ## Amendment: TUI by default, headless as opt-in (issue #269, part 2)
2467
2491
 
2468
2492
  - `peers.<id>.start_mode` (`tui` or `headless`, for pi, codex, kimi and jcode; `tui` when unset) is the mode a start takes when nothing more specific was asked. Claude has only a TUI; Kimi defaults to its own pull-only TUI, with ACP selectable; local stays headless. The loader refuses a malformed `peers` block.
@@ -2699,6 +2723,7 @@ the queue.
2699
2723
  - Base flow meanings, routing, budgets and permission checks stay unchanged; aliases persist beside their bases even for custom dev data. Ordinary tasks under custom dev data also retain profile semantics for document overlap; unmodified dev data keeps the legacy row shape. Unknown fields/base meanings, duplicate/unknown plan fields and oversized guidance fail before task effects.
2700
2724
  - Schemas, CLI classification, Tasks, daemon/native contracts, plugin instructions, both local-worker tool-building paths, check-name allowlists and init use the same resolved data. No-override dev contracts/schemas/instructions remain byte-identical. Old accept schemas support existing task fields.
2701
2725
  - Flow authority remains the separately validated `roles` map; executable checks remain trusted `checks`. Example checks are display data and do not execute. Managed instruction content keeps surrounding text and quotes forged headers.
2726
+ - Acceptance criteria with ids (#507b) reuse the #481 S8 `criteria` checklist, the only criteria model: `sendReview` lists the recorded criteria (`- <id>: <result>`, `(optional)` for a non-required one) ahead of the profile's checklist lines in every profile, and `Tasks.review` records on the verdict the ids that the reviewer's `unmet` items name, through the pure `unmetCriteriaIds` (an item names a criterion only by starting with its exact recorded id, followed by the end of the item or a delimiter; each id once, in first-named order). The ids come from the board's checklist at the verdict, never from the caller, and are stored as `HistoryEntry.unmetCriteria` (validated: at most 32 distinct bounded ids, on a saved review verdict only) and shown in the public history. The note text is unchanged. A PII task lists and records none; a task with no checklist, a verdict with no unmet items and an item with no matching id add nothing, so every existing request and verdict is byte-identical. #513 packing and #514 learning refer to these ids.
2702
2727
  - Override maps are bounded to 32 class entries and existing base classes; plan selections use existing fields. Role text is 2000 characters, agents 16000, examples 16 strings of 2000 characters, review checklist 16 lines of 500 characters (#507). Native ingestion/retention/measurement remains unverified; configuration changes apply after daemon/MCP restart.
2703
2728
 
2704
2729
 
@@ -4088,6 +4113,22 @@ These rules are settled here and land with hub tasks #284-#286; until then the S
4088
4113
 
4089
4114
  - S11 follow-up invariants: contiguous planning revisions changing exactly `["release"]` retain the S9 reviewed execution plan; other or missing planning history invalidates it. Cut-time CI cost reads are limited to the scoped PR ids (at most 16 in one repository); read errors or unbounded/mixed scope remain unknown and cannot abort a valid cut. Train history is capped at 256 events: configuration no-ops append nothing, meaningful configuration reserves cut/shipment slots, and capacity refusals are pre-effect.
4090
4115
 
4116
+ ### S7 metrics and control bands amendment (#481, hub task #200)
4117
+
4118
+ - `ahub sdlc metrics [--since]` and the planner tool `hub_sdlc_metrics` are a pure projection of board history (`src/hub/sdlc-metrics.ts`: no clock, no I/O, no model). `Tasks.sdlcMetrics` is the one read, for a planner or the person, refused during a private turn; private tasks are counted as withheld and read for nothing else. Anything the board cannot derive is `null`/`unknown` and counted, never 0.
4119
+ - Scope cuts, each deliberate:
4120
+ - **No PR readback.** The report reads only the board. Time to merge and same-head CI reruns need a merged PR and its check runs, which `ahub sdlc costs` already reads under its own bounds (16 PRs, 20 s, public tasks only); a second reader of the same GitHub surface would duplicate those bounds and their privacy revocation checks. Both read `unknown` here, and the pure functions accept them as optional inputs so a later slice can supply them.
4121
+ - **S10's `costBandProposals` is not consumed.** It returns per-PR upper-limit breaches over complete merged-PR rows, a different data set (review rounds, CI runs and reruns per merged PR) from board-history metrics, and it takes absolute limits rather than a rolling baseline. S10 left the baseline, configuration and automatic card scheduler to S7 and delivered the pure breach data only; S7 builds the scheduler for its own metrics and leaves the PR cost metrics' bands for the slice that also reads PRs, so one configuration and one dedupe rule cover both.
4122
+ - **No security-scan baseline.** The last S7 line (a scheduled security scan with a recorded baseline feeding the propose tier) is not built. No scan source exists in the repository to baseline, and inventing one would claim a security control nothing enforces.
4123
+ - **No release metrics, no release bands, no gates-run record.** S11 recorded release cut and shipment times and left the bands and proposals over them to S7 (release interval, lead time from approval to release, and a band on the hotfix rate that proposes a planning card). S7 here reads task history only, so none of those is built: release history is a separate table with its own bounds and privacy rules, and a hotfix rate needs shipped releases to have a baseline at all. The per-release "gates run" record is S11's explicit deferral to S6/S7 and stays deferred: a cut record is not gate-execution evidence, and recording gates needs the S6 authorization path. These are the next slice, and `[bands]` has no release metric names until then, so a typo cannot silently configure one.
4124
+ - **`first_pass_check` is the hub's completion check, not PR CI.** It reads `check passed` and `check failed` at `hub_task_done`, so it is `unknown` wherever completion checks are off (the default). The issue's "first-pass CI success" is the PR-based figure and is not available without the readback above.
4125
+ - **Stale-head re-reviews need head evidence.** A review round is stale when its verdict is bound to a different head than its request (`reviewCost.head` on the done against `reviewFindings.head` on the verdict). A task without both is `unknown`. A `done` after an `approved` is not a signal: the board's move table has no way out of `approved`.
4126
+ - Control bands are optional `[bands.<metric>]` tables of `sdlc.toml`, validated by `bandsConfig` with field-level paths. The newest value is compared with the mean and population standard deviation of the preceding `window` values, on the bad side only, with a per-metric deviation floor, a `min_samples` floor, a `min_delta` floor that every tier including the drift rule honours, and a drift rule (`drift_run` newest values all over one deviation on the bad side is at least `diagnose`).
4127
+ - Rework rounds and stale-head re-reviews are final only when a task is approved, so a task still in flight is `unknown` for both and a series places them at the approval. Otherwise a provisional value would move at each verdict and could start a second episode inside one shift after its card was closed. Residual ceiling: the verdict head prefers the lifecycle receipt's `head` or `localHead` over `refs.commit` (`findingHead`) while the request head is `reviewCost.head`, so a task whose receipt head and commit disagree can read one spurious stale round; one shared head rule for both sides is the upgrade.
4128
+ - A breach is an episode: the run of non-ok values for one metric. `BandResult.episode` is the task that started the run, derived from history alone, so a restart finds the same key. `Tasks.proposeFromBands` is the only writer. `log` is one hub notice, recorded in the board's `band_notices` table so it is announced once. `diagnose` is one read-only `triage` backlog task and `propose` one `plan` backlog card, keyed `<metric>:#<episode>:<tier>` in the card. A metric with an open band card writes nothing more, so a value growing from diagnose to propose inside one episode adds no second card, and a sustained shift is one card. Card text is fixed words, numbers and task ids, screened like the recurring-finding card; the hub opens no GitHub issue.
4129
+ - The daemon evaluates every five minutes, only when the board's status revision or the band configuration moved since the last evaluation that ran, and not while it is stopping, in recovery or has a storage error (the same holds as the task sweep). A pass that was held, in mode off or without bands consumes nothing, so a breach seen then is written when the hold clears and bands enabled by a reload on an idle board are evaluated at the next tick. A failed evaluation is one `hub.log` line. Ceiling: one evaluation reads every task with its history; a history window is the upgrade if a board passes several thousand tasks.
4130
+ - Bands write cards only in `advise` or `enforce`; in `off` the writer is idle while the report still shows each configured band's tier.
4131
+
4091
4132
  ## Antigravity optional implementer peer (#485 P1)
4092
4133
 
4093
4134
  Planner ownership ruling, 2026-10-19: the person-only `ahub agy` launcher owns exactly `plugin.json`, `mcp_config.json` and `hooks.json` inside `.agents/plugins/agent-hub/`. Apply symlink, hard-link and foreign-entry refusal and managed cleanup to all three files. Never write project-level `.agents/hooks.json`, another `.agents/` path or the person's `~/.gemini` settings. This amends the earlier two-file list in #485 AC3. AC0 must prove plugin-local hooks load without a global setting; failure stops implementation rather than falling back to manual configuration.
@@ -4406,21 +4447,22 @@ Declaration: `"claude-impl": { "kind": "claude", "roles": ["implementer"], "star
4406
4447
  - first turn of a task: `--session-id <new uuid>`, session persisted (no `--no-session-persistence`);
4407
4448
  - later turns: `--resume <that task's uuid>`; never `--continue`, never `--fork-session`, never another task's id;
4408
4449
  - the task-to-session map is a per-peer state file named through `claudeStateFile(<id>, ...)` (daemon.md rule), listed in `sessionPointers` (`src/cli/reset.ts`);
4450
+ - a newly reserved UUID becomes resumable only after a stream-json frame names that exact session; a failed start before such evidence retries with `--session-id`, never a speculative `--resume`;
4409
4451
  - a missing transcript (positive ENOENT through `transcriptPersisted`) starts a new session for that task and records `fresh` with a console notice; any other read error keeps the recorded id;
4410
4452
  - chat with no task reuses a task's session only when the instance owns exactly one `in_progress` task. With none, or with more than one (a raised `tasks.wip_limits.<id>`), it goes to a one-off child with `--no-session-persistence`, so unscoped chat never enters an arbitrary task's context or attribution.
4411
- - **argv.** `-p --output-format stream-json --verbose --strict-mcp-config --mcp-config <tools-mode server for <id>> --setting-sources "" --settings <0600 managed settings file with the PreToolUse facts hook, purpose permission> --permission-mode default --permission-prompts host --permission-prompt-tool mcp__agent-hub__hub_permission_request --allowedTools <implementer hub tools>`. No `--bare` (it skips hooks, `claude --help` 2.1.307). No `dontAsk`, no `bypassPermissions`. Unverified: `--permission-prompt-tool` is named by the `--permission-prompts` help text of 2.1.307 but has no entry of its own; the slice proves it with a fake and one live probe before claiming support, and refuses to start (reason recorded) on a Claude version where the probe fails.
4412
- - **Hub tools.** The implementer set (`hub_task_accept`, `hub_task_decline`, `hub_task_done`, `hub_checkpoint`, `hub_send`, `hub_task_show`, `hub_task_propose`, `hub_remember`, `hub_inbox`) through the ordinary tools-mode server with `AGENTHUB_PEER_ID=<id>` and a per-child `AGENTHUB_LAUNCH_ID`. The daemon accepts a `tools` hello for this id only with the current child's launch id; it is revoked when the child's group is confirmed gone.
4453
+ - **argv.** `-p --output-format stream-json --verbose --strict-mcp-config --mcp-config <tools-mode server for <id>> --setting-sources "" --settings <0600 managed settings file with the PreToolUse facts hook, purpose permission> --permission-mode default --permission-prompts host --permission-prompt-tool mcp__agent-hub__hub_permission_request --allowedTools <implementer hub tools>`. No `--bare` (it skips hooks, `claude --help` 2.1.307). No `dontAsk`, no `bypassPermissions`. Verified on Claude Code 2.1.309 with a real native binary and a deterministic loopback model-protocol/MCP fixture, without account authentication or model inference: `docs/verification/2026-10-23-headless-claude-permissions.md`. Only a compat-admitted native version and unchanged binary may start; the receipt is evidence for the contract, not a runtime receipt gate. Real provider/model/person-approval behavior remains a live leg.
4454
+ - **Hub tools.** The implementer set (`hub_task_accept`, `hub_task_decline`, `hub_task_done`, `hub_checkpoint`, `hub_send`, `hub_task_show`, `hub_task_propose`, `hub_remember`, `hub_inbox`, `hub_permission_request`) through the ordinary tools-mode server with `AGENTHUB_PEER_ID=<id>` and a per-child `AGENTHUB_LAUNCH_ID`. The daemon accepts a `tools` hello for this id only with the current child's launch id; it is revoked when the child's group is confirmed gone. `hub_inbox` may pull only a batch whose host-resolved task attribution matches the current child, using the same unique-task rule for unscoped chat as delivery. A mixed-task batch is refused before any queue removal, so other tasks reach their own child/session.
4413
4455
  - **Permissions.**
4414
4456
  - `ask`: the hook decides nothing; every write, edit and shell call reaches the prompt tool.
4415
4457
  - Both modes run Claude in its `default` permission mode, never `acceptEdits`: `acceptEdits` auto-approves in-worktree edits before the hook's path gate or the prompt tool is consulted, so protected paths (`.agenthub`, agent configuration, unsafe aliases) could be written without a person. Every automatic grant comes from the hook's explicit `allow`; other writes and shell calls require the person's answer through the prompt tool.
4416
4458
  - `ask-when-needed`: the hook answers `allow` only for `projectFileTool` targets with `projectRoot` = the execution root (worktree), so `.git`, `.agenthub` and agent configuration stay protected (`src/cli/facts-hook.ts:20-54`, `:91-96`). Shell, git writes and every path outside the worktree reach the prompt tool. This is the Pi/local ask-when-needed grant ("bash and mutating git retain console approval", operator permission modes amendment), without its sandbox, so `never-ask` is refused (R1).
4417
4459
  - The daemon puts `permission` in a `facts` reply for this instance's current child launch id, as it does for the recorded `ahub claude` launch (daemon.md facts-hook rule extended to this one case).
4418
- - **Person approvals.** `hub_permission_request` (tools-mode server, callable only by this variant's current child) puts the request on the existing approval path: console `permission` push, macOS notification (#5), `approvals.timeout_s`. Payload is the tool name plus a public rendering of the arguments made by a bounded generic sanitizer, not `Tasks.publicView` (that takes a `Task`): JSON-stringify, `sanitize()`, the PII screen used for public metadata text (a match replaces the value with `[withheld]`), path values through the same screen, then a cut at 600 characters at a character boundary (#72 rule). It never includes task text. The original arguments stay in the child's pending request and are what Claude's allow response refers to; they are not logged or shown. Options are allow-once and deny; no allow-always. While a request waits, the watchdog is kept alive (as ACP, #5).
4460
+ - **Person approvals.** `hub_permission_request` (tools-mode server, callable only by this variant's current child) puts the request on the existing approval path: console `permission` push, macOS notification (#5), `approvals.timeout_s`. Payload is the tool name plus a public rendering of the arguments made by a bounded generic sanitizer, not `Tasks.publicView` (that takes a `Task`): JSON-stringify, `sanitize()`, the PII screen used for public metadata text (a match replaces the value with `[withheld]`), path values through the same screen, then a cut at a character boundary with a `[cut, N chars]` marker inside the 600-character title bound (#72 rule). It never includes task text. The original arguments stay in the child's pending request and are what Claude's allow response refers to; they are not logged or shown. Options are allow-once and deny; no allow-always. While a request waits, the watchdog is kept alive (as ACP, #5).
4419
4461
  - **No answer.** At `approvals.timeout_s` the tool returns deny with "no person answered"; Claude continues the turn with the denial; the delivery is not failed by the timeout itself; a console and log line is written.
4420
- - **Hub down or refusing.** The prompt tool returns deny. If Claude cannot reach the MCP server at all, its behaviour is unverified; the fake test pins the hub side (deny), and the live smoke records Claude's.
4421
- - **Hook down.** Decides nothing (`facts-hook.ts:6`), so the call reaches the prompt tool: fail closed to the person, never to allow.
4462
+ - **Hub down or refusing.** The prompt tool returns deny. The native model-free fixture observed no write when the permission tool was missing, returned an error or disconnected. A pending native permission request was externally stopped after 25 seconds with no write; a native internal timeout was not established. The hub's own approval deadline returns deny and does not rely on that native timeout.
4463
+ - **Hook down.** Decides nothing (`facts-hook.ts:6`), so the call reaches the prompt tool. A valid person allow still authorizes the call; failure of the prompt, its deadline, its connection or its current-child fence supplies no allow. The native fixture observed both deny and allow after a failing hook.
4422
4464
  - **Lifecycle.** The #223 guarantees, shared code, not a copy: extract the spawn (`detached: true`, own process group, `src/adapters/reviewer-print.ts:315`), watchdog and group kill from `ReviewerPrintPeer` into one helper used by both. Stop fences the generation before any await, holds the delivery `needs_review`, keeps the peer busy until the group is confirmed gone and offline only after (`reviewer-print.ts:153-164`); a failed cleanup keeps it busy and refuses a start. A start failure before the child runs is `failed_safe`; after, `needs_review`. A watchdog timeout ends the turn, not the task: the task stays `in_progress` with this owner, and the next delivery resumes its session.
4423
- - **Delivery.** Ordinary implementer delivery (task and chat items). The #223 R2 review-only filter does not apply (it keys on roles exactly `["reviewer"]`).
4465
+ - **Delivery.** Ordinary implementer delivery (task and chat items). Inadmissible notices or foreign task refs are dropped without vetoing admissible items in the same batch; a batch with no releasable input settles `failed_safe`. Private envelopes remain refused before spawn. The #223 R2 review-only filter does not apply (it keys on roles exactly `["reviewer"]`).
4424
4466
  - **Usage.** Tokens from the child's `result` event `usage` (as `claude-print.ts:82-85`), added to the peer and attributed per task (#200). Account `claude`, shared pauses and readings (I2). Quota windows come only from the account's existing reader (the terminal statusline tee); none is classified from result text. A `result` with an error subtype is a failed turn, not a quota reading (unverified whether 2.1.307 emits a distinct rate-limit result).
4425
4467
  - **Done.** `hub_task_done` from the child completes the task through `Tasks` with the ordinary owner check (`by === task.owner`), then the reviewer is notified (I-2 excludes this instance from reviewing it).
4426
4468
  - **Recovery.** `session.freshOnRestore` is false; crash resume reuses the per-task map, and each task's session is checked by transcript as above. No terminal row. Controlled recovery requires target feature `role_stages` (below).
@@ -4430,7 +4472,7 @@ Declaration: `"claude-impl": { "kind": "claude", "roles": ["implementer"], "star
4430
4472
  Declaration: `"claude-impl-t": { "kind": "claude", "roles": ["implementer"], "worktree": ".worktrees/claude-impl-t" }`.
4431
4473
 
4432
4474
  - `ahub claude --as <id>` (`src/cli/launch.ts`) resolves the execution root with `resolveInstanceExecutionCwd`, starts Claude with that cwd, and records `executionCwd` in `claude-launch.<id>.json` beside `launchId`. The hub never starts it (daemon.md: channel extras stay out of `STARTABLE_PEERS`).
4433
- - The channel's hello gains an optional `cwd` (realPath of the MCP server's `process.cwd()`); additive, no `PROTOCOL` bump. Unverified: that Claude Code spawns stdio MCP servers in its own cwd; the slice verifies it with a live probe and, if false, uses the `cwd` field of the first PreToolUse hook input instead.
4475
+ - The channel's hello gains an optional `cwd` (realPath of the MCP server's `process.cwd()`); additive, no `PROTOCOL` bump. Verified model-free on Claude Code 2.1.309: an actual stream-json session initialized its stdio MCP server in the session cwd with no user/model turn. Receipt: `docs/verification/2026-10-23-terminal-claude-execution-root.md`. The optional hello uses that server cwd. Other native versions or intentional native cwd changes are not certified by this receipt; missing or different cwd never proves ownership.
4434
4476
  - **Admission as owner.** At attach the daemon sets `executionOwners[id]` only when all hold: the hello's `launchId` is this instance's current launch record (#551); the record's `executionCwd` equals `executionRoots[id]`; the reported `cwd` resolves to the same root; the root is not the canonical cwd. Otherwise the peer attaches as before but `ownerIsolation[id]` stays false, so `assign()` refuses it as a code owner (`routing.ts:402`) and status says why. Detach, a new launch id or a changed root clears it.
4435
4477
  - The facts reply's `projectRoot` for this instance is its execution root, so ask-when-needed grants are worktree-scoped. `never-ask` follows the terminal's existing rules.
4436
4478
  - Terminal recovery records `terminalRoot` = the verified execution root (I2 permits it); the I3 line "terminal recovery (project root, no worktree)" is amended for implementer channel instances only.
@@ -4493,7 +4535,7 @@ Additive optional fields, no `PROTOCOL` bump (daemon.md):
4493
4535
  - 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`.
4494
4536
  - Includes the `--permission-prompt-tool` probe gate.
4495
4537
  - ACs: AC-4, AC-3 (with 525-6 for `codex-plan` and #535 for `codex-review`).
4496
- 5. **525-5 Terminal Claude implementer.** Size M. Depends on 525-1.
4538
+ 5. **525-5 Terminal Claude implementer.** Size M. Depends on 525-1. (#735, before this slice: a person-started Claude channel instance owns no task of a write class. `ownerRoleOfClass(class)` in `src/hub/routing.ts` is the one derivation: `plan` is the planner's, `review` the reviewer's and every other class the implementer's, so a class added later is a write class by default. `assign()` refuses such an instance as owner with `owner admission: a person-started Claude instance owns no write-class task until #525-5 proves its execution root`, named or automatic and whatever `ownerIsolation` says, and `ahub route explain` prints it because it is the same function. This slice deletes that one line and lets the isolation check decide.)
4497
4539
  - Paths: `src/cli/launch.ts`, `src/adapters/claude-channel.ts` (hello `cwd`), `src/hub/daemon.ts` (admission, `executionOwners`, facts `projectRoot`), `src/cli/terminal-recovery.ts`, rebuild bundle.
4498
4540
  - ACs: terminal half of AC-1 at runtime; a test that a mismatched or missing root keeps `ownerIsolation` false and `assign()` refuses with `owner-admission`.
4499
4541
  6. **525-6 Codex planner and visibility.** Size M. Depends on 525-2, 525-3.
@@ -4502,6 +4544,11 @@ Additive optional fields, no `PROTOCOL` bump (daemon.md):
4502
4544
 
4503
4545
  Spec-first: each slice's PR amends this section where its implementation differs, together with `docs/operations.md`.
4504
4546
 
4547
+ ### Amendment: write-class owners and the configured ACP planner rule (#735)
4548
+
4549
+ - **Write classes are derived.** Before 525-5, a person-started Claude channel instance (`instances.mode: "external"`, declared with `worktree` and the implementer or verifier role) owns no task whose class has the implementer as owner role. That is `ownerRoleOfClass(class) === "implementer"`: every class except `plan` and `review`. Previously only `implement`, `test` and `bulk_edit` were refused (the classes that need tests), so `summarize` and `triage` could still reach such an instance. The role ceiling uses the same function, so the two cannot disagree. The refusal is code `owner-admission` in `RoutingRefusal`, with its reason in the explain trace. A headless instance with a verified root (`ownerIsolation`) is judged by the existing check, as before.
4550
+ - **Configured ACP agents and `planner`.** Decision: a configured ACP agent may hold `planner`. Its `adapters.<kind>.roles` or a `roles.<kind>` entry keeps `planner`, because the planner filter (`loadConductorPolicy`, `effectiveInstanceRoles`) asks the builtin registry for `projection.plannerEligible`, and a configured kind is not in that registry (`definitionOf` is undefined, so the filter passes). What it cannot do is be named by the `planner` setting or picked as the automatic planner: those read `PLANNER_PEERS`, which is derived from builtin projections, and the capability scope refuses `projection` on a configured definition (#625). No projection is given to configured agents: a projection fixes the host's role and help order and the planner choice, which a project file must not decide. The earlier statement that #725 drops `planner` for configured agents was wrong: it drops it only for the builtin kinds that cannot plan (pi, local, agy) and for instances of them.
4551
+
4505
4552
  ## Amendment: typed host ports for the remaining native branches (#625 phase 6, slice 2+)
4506
4553
 
4507
4554
  Audited against `main` @ `bab87cb2` (2026-10-22). Every `file:line` below is at that commit. Line numbers in `src/hub/daemon.ts` move with each slice; a slice cites the enclosing function, not the line.
@@ -4930,6 +4977,15 @@ Use `summarizeByTask`, `costMetrics` and bounded SDLC GitHub helpers. Bound file
4930
4977
  - The `usage` op now folds the log with `catchUp(250)` before it answers (one 8 MiB pass per poll left a hub that had just started on a big log answering from the first pass: found by the live panel on a copy of the real 47 MB log, where TODAY read `no records` for peers that had used tokens). When the budget runs out the reply has `behind: true`; the console says `still catching up` and the CLI warns on stderr.
4931
4978
  - Ceiling: the limits history is the newest 1000 events; the panel only needs each peer's last three resets. The dashboard panel is slice U4 and calls the same op.
4932
4979
 
4980
+ ## Amendment: the dashboard Usage view (#683 U4)
4981
+
4982
+ - The Usage view is part of the Agents view, under Usage budgets (`src/ui/index.html`, `renderUsage`), and reads the same roll-up as `ahub usage` and the console panel: lifetime totals with the peers whose figure is unknown, the last 14 UTC calendar days of stacked tokens per peer, the open budget pauses with the last 20 pause, resume and reset events, and a task table (state, owner, PR, input, output, cache read, cache write, total).
4983
+ - `uiSnapshot` builds `usage` only when the request names `view: "agents"` (the page sends its open view with every poll, like it sends `memory`), so a hidden view costs no roll-up work on the 1 s poll. It folds the log with `catchUp(100)`; when that returns false the snapshot carries `usage.behind: true` and the view says the figures are a lower bound until it catches up, never the folded part as the whole.
4984
+ - `UsageRollup.daily(days, now)` returns the last `days` calendar days ending today, clipped to the first day with a record. A day with no record is a row with `empty: true` and the view shows `no record`, never zero. Every counter keeps its source (`native`, `hub` for a count, `at least` for a partial sum) and `unknown` stays the word.
4985
+ - A private task is the public title `[pii]` on the board (`Tasks.publicView`) or a `pii` roll-up row; the view shows `#id [pii]` and never a title, and the snapshot carries ids and numbers only.
4986
+ - Colours are design tokens only; the view is one column at 600 px and its tables and chart scroll inside themselves. `test/ui-usage.test.ts` runs the page's own `renderUsage` over a snapshot fixture (PII marker absent, unknown not zero, the lower-bound note) and checks the view's token pairs for contrast in both themes.
4987
+ - Ceiling: the view recomputes the daily and task aggregates on each poll while the Agents view is open; a materialised daily table is the upgrade if that ever costs more than a few ms on a large log.
4988
+
4933
4989
  ## Amendment: the console changed-symbols block (#684 phase 2b)
4934
4990
 
4935
4991
  - `src/cli/console-symbols.ts` (pure data) and `taskFields` in `console-state.ts`: Enter on a task in Tasks adds `symbols` and `symbols note` after the plan field and drops the raw `codeSymbols` field. It reads the public `codeSymbols` that `task_show` already carries (#684 2a) and the task's `plan.symbols`; it asks the hub for nothing new.
@@ -4939,3 +4995,49 @@ Use `summarizeByTask`, `costMetrics` and bounded SDLC GitHub helpers. Bound file
4939
4995
  - Names go through the console's field renderer, so they are quoted; at most 40 are listed and the rest are counted. The note says deleted definitions are not reported.
4940
4996
  - Not in the original 2b slice: overlap lines (the read-only `symbolOverlaps` projection and console block follow in #379 above) and the related-notes count (phase 4, no hub op returns it).
4941
4997
 
4998
+ ## Amendment: reserved backlog pulls offer their exact owner (#718)
4999
+ - A planner pull of an existing backlog task with `reserved` set admits that exact peer before any task write. It cannot silently fall back to another peer when the reserved peer is excluded, unavailable, at planner WIP capacity or otherwise ineligible. Existing planning authority, revision, dependency, release, person-order and SDLC plan gates still apply. Authority, routing/WIP eligibility and plan/flow fences are rechecked after asynchronous screening or plan readbacks and immediately before the admitted write.
5000
+ - `Board.pullAssigned` records `backlog pulled` and the owner assignment in one atomic task transition. Both writes commit or roll back together; board reads and deferred callback payloads both expose the committed assigned row, while history retains both events. Pull increments the planning revision, clears any old `offer`, and the existing assignment path creates a fresh owner offer bound to the new assignment flow revision. A stale prior owner request is withdrawn through the existing delivery path rather than reused as the new offer.
5001
+ - The committed assignment remains fact if subsequent routing notice or task delivery fails. Existing typed undelivered-assignment handling reports that failure; an assignment superseded while awaiting delivery follows the existing supersession handling, without rollback or a second owner offer. This change adds no separate delivery mechanism.
5002
+ - An unreserved backlog pull retains its lazy, ownerless To Do transition and existing planning undo. A reserved pull now assigned/offered to its peer cannot be undone through planning; the existing planner-or-person untouched-return path remains the recovery route when its own conditions hold. Acceptance is still a separate peer action and does not occur as part of pull.
5003
+ - No new model call, role authority, configuration or privacy exception is introduced. Existing screened task/note views, PII routing and refusal boundaries apply. Dogfood starts after release and checks the same-peer `backlog pulled` -> `assigned` -> `accepted` sequence, fresh offer binding, stale/unowned offers and capacity refusals.
5004
+ ## Amendment: the dashboard task sheet's Code group (#684 phase 2c)
5005
+
5006
+ - `src/hub/task-code-group.ts` (`codeGroup`) classifies a task's public `codeSymbols` against `plan.symbols` with the review brief's own matcher (`symbolsOverlap`) and is the only place that does for the dashboard (the console classifies with `classifySymbols` in `console-symbols.ts`, over the same matcher); the UI snapshot carries the result as `codeGroup` on each task. The page (`codeGroupSection` in `src/ui/index.html`) draws it and never matches names.
5007
+ - A private task has no `codeGroup`, so the sheet shows no Code group for it and the snapshot carries none of its plan names. A stored receipt (one that still has `repositoryKey`), a missing one or an `unknown` one is `{ state: "unknown" }` and the sheet reads `unknown (no code facts)`. Symbol ids that fail `publicMetadataText` or the id shape (`symbol:<name>@<path>`) are dropped and the group is marked incomplete, so an untouched symbol may have changed. Every stored plan name is read (the stored plan allows 50 names of 300 characters, and the group uses the same bounds); one that is malformed or fails the screen is counted in `planWithheld`, the group is marked incomplete, and the sheet says a symbol marked unplanned may be in the plan, so a withheld name never shows a plan departure that did not happen. The counts on the sheet are the totals, not the listed part.
5008
+ - At most 40 symbols and 40 untouched names are listed, with the count of the rest. Advisory only: ripwire does not report deleted symbols.
5009
+ - A symbol button closes the sheet, sets the Memory view's `node` to the symbol's canonical id (`symbol:<name>@<path>`) and switches to the Memory tab. The daemon's memory view reads a node of up to 512 characters when it starts with `symbol:` (200 otherwise) so a receipt id is never truncated into another node. Memory entries attached to that symbol appear once phase 3 links them; until then the graph shows the symbol node alone.
5010
+ - Colours are design tokens only, and the group wraps at phone width with 44 px targets.
5011
+
5012
+
5013
+ ## Amendment: daemon responsiveness (#719)
5014
+
5015
+ Periodic roster and supervision reads use pending delivery metadata/rows rather than materializing terminal envelope history. Queue revisions are metadata reads; explicit queue/history and recovery snapshots retain their full semantics. Usage folding keeps its existing byte cap and atomic rows/offset transaction, with a 20 ms slice checked between whole records; one record remains indivisible. Native launch connects alone retry transient transport failures with bounded backoff up to 15 seconds and one busy notice. Ordinary connect defaults and identity/authentication/protocol refusals stay unchanged.
5016
+
5017
+ ## Amendment: terminal Claude execution-root admission (#525-5)
5018
+
5019
+ Slice 525-5 is implemented. The temporary #735 blanket refusal is replaced by the existing implementer-role and host owner-isolation gate for every write class. The bounded launch-proof reader binds the current daemon instance and launch id; owner proof additionally needs a private, single-link launch record, a matching recorded execution root, the MCP server's real cwd and the validated noncanonical branch worktree. The proof belongs to the exact attached claim generation and socket; a pending preface is not ownership, and detach, replacement or a changed launch record revokes it. Old/missing root evidence attaches without isolation. Facts grants for declared implementers and terminal-recovery publication require the current proof. Canonical planner/reviewer behavior and protocol 16 remain unchanged. The launcher preserves canonical AGENTHUB_PROJECT_DIR for hub identity while passing the execution root as native cwd. These are same-user process/record attestations, not a sandbox.
5020
+
5021
+ ## Amendment: planner walk and reviewer independence in `assign()` (#525-3)
5022
+
5023
+ Slice 525-3 is implemented, as the "Purity" bullet of the #525 amendment specifies; this records what exists and what does not yet.
5024
+
5025
+ - **Planner walk.** `plannerWalk` (`src/hub/routing.ts`) is a pure function of the sorted settled holders, the task id, an optional named planner, an optional kept planner and a per-holder `blocked` reason. A PII task has no planner (`planner: none (pii: planning stays with the person)`). A named planner is exact: a name that is not a holder is refused in the trace and never replaced by rotation, and a named holder that is paused or offline is still the choice, with the state shown. Otherwise a recorded planner is kept while it is still a holder. Otherwise rotation starts at `task.id mod n` over the holders sorted by id and walks forward past unavailable holders; the denominator is every holder, so availability never renumbers it (the #519 rule); when all are unavailable the seed holder is taken, `rotation (none available)`.
5026
+ - **Inputs and outputs.** `assign()` takes `planners`, `namedPlanner` and `keptPlanner` and returns `Assignment.planner` and `plannerReason` (`rotation`, `named`, `kept`, `none`). Without `planners` it walks no planner, so a caller that does not pass them is unchanged. A holder is unavailable when the adapter role ceiling excludes planning, it is off roster, not attached, offline, paused under `budget_paused = skip_peer`, or failing: the same state inputs the owner and reviewer walks read. The walk is independent of both: planner and owner, or planner and reviewer, may be the same peer.
5027
+ - **Explain parity.** The walk's lines (` planner candidate X: ...`, then `planner: X (reason)`) are part of `assign()`'s trace, after the owner and before the reviewer. `Tasks.weights()` supplies `planners` (the rostered holders of the settled role map, sorted) to every `assign()` path, so `ahub route explain` prints exactly what assignment ran.
5028
+ - **I-2.** A peer with an `accepted` or `done` history entry on the task never reviews it (`implementers`), whoever owns it now, a named reviewer and the declared reviewer pool included: refusal code `policy`, reason `implemented this task`. It is the last check in `blocked()`, so it only ever removes a candidate that was otherwise eligible: a former implementer that is offline, not attached, paused, failing or off roster is refused for that real reason and leaves the outcome as it was before I-2 (a task with no other reviewer still approves directly). When I-2 removes the last eligible candidate, the reviewer is the person and `reviewBlocked` is set (`every eligible reviewer implemented this task: the person reviews`); there is no fallback to an implementer. The person's own entries are not an implementation. A review-class and a PII task are unchanged.
5029
+ - **Not in this slice.** Nothing records the planner: no board column, no history entry, no notice, no authority check. `Tasks` passes `planners` and reads nothing back; 525-2 owns recording and the per-task authority. A task proposed today behaves exactly as before.
5030
+
5031
+ ## Amendment: deliverable records bound to content hashes (#509a)
5032
+
5033
+ The only artifact identity was GitHub (`repo`, `issue`, `pr`, `head`, `reviewedHead`); a report, translation or deck had no record of which bytes were reviewed and approved. Slice 509a adds one, with no dependence on git.
5034
+
5035
+ - **Claims.** `hub_task_done` and `hub_sdlc_record` (in `receipt`) take `deliverables: [{path, format, version?}]`, one to sixteen distinct paths. `parseDeliverableClaims` (`src/hub/deliverables.ts`, pure) refuses every field the hub does not own: a claimed `sha256`, `hash` or `bytes` is a refusal with the reason, never an input. Paths are relative, forward-slashed, with no `.`, `..`, empty or control segments.
5036
+ - **Root.** Paths resolve under the owner's execution root, the same one the owner writes in: `TasksDeps.executionRoot(peer)` (the daemon passes `ownerWriteRoot`), falling back to the project directory only when the port is absent. A declared worktree instance whose root is not verified refuses hashing (`DeliverableRefusal`), and a live read reports every file `unknown`, so an approval is refused and a file is never shown `unchanged` from the wrong tree.
5037
+ - **Hashing.** `src/hub/deliverable-files.ts` is the only code that opens a deliverable. It resolves the path with `realPath` and `guardPath` (read mode), refuses the project's secrets denylist and the hub's own state (`.git`, `.agenthub`, by `isHubWritePath`), a non-regular file, a missing file and sizes over 256 MiB each or 512 MiB together, then opens without following a final link (`O_NOFOLLOW`) and compares device, inode, size and mtime with what was vetted (Linux reuses an inode number at once, so the inode alone does not prove identity) and the size and mtime again after the read, so a swapped or concurrently changing file is refused. Chunked reads with an await between chunks keep the daemon responsive. Any refusal refuses the whole call before a board write.
5038
+ - **Reads and gates.** A live read (`checkDeliverables`) applies the aggregate limit to the current sizes: files that grew past it are read `unknown` and none is hashed, so `hub_task_show` and an approval attempt stay bounded. `specStageGate` reads a receipt without its `deliverables` field, as `recordSdlc` records it, so naming deliverables never skips the live spec edit-time check in enforce mode.
5039
+ - **Record.** `HistoryEntry.deliverables` holds `{id, path, format, version?, sha256, bytes, private?}` on `done`, `done (checking)` (carried to the completing `done` by `latestPendingDeliverables`), `lifecycle` and `approved`, validated on write (`parseDeliverableRecords`, which also checks the id against the path and format). `id` is opaque and stable across edits.
5040
+ - **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
+ - **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
+ - **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.
@@ -45,3 +45,17 @@ Central compatibility policy marks 0.146.0 and 0.163.0 as verified. Other versio
45
45
  ## Evidence boundaries
46
46
 
47
47
  The network setting remains configured disabled, without a separate network probe. Web search is requested disabled; 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/model review, TUI interaction or native Linux result is claimed. These runtime tests exercise the installed macOS binary; same-user filesystem/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. Hosted CI and the changed-source gate are separate code-verification evidence recorded on the PR.
48
+
49
+ ## Per-review start failure classes (#722)
50
+
51
+ Date: 2026-10-23. Platform: macOS arm64, Codex 0.163.0 (Homebrew `/opt/homebrew/bin/codex` and the fnm launcher), no model request, login or credential copy. Every CLI run used a temporary HOME and CODEX_HOME with only PATH, TMPDIR and LANG inherited; the person's real `~/.codex` was never passed to an agent CLI (its file names and table names were read, never values).
52
+
53
+ Reproduction of the reported `start_failed`, model-free, with the production `codexReviewerBackend.prepare`:
54
+
55
+ - Empty temporary homes: the receipt was produced (14 evidence entries) on both launchers, in about 0.3 to 0.4 s.
56
+ - Synthetic ambient configuration shaped like the real one (features, a marketplace, a plugin, four MCP servers with per-tool tables and an env table, a `hooks.json`): the receipt was produced in each of four variants.
57
+ - Each proof command (`--version`, `features list`, `mcp list --json`, `debug models --bundled`) took 54 to 82 ms, idle and with 12 busy loops running, against the 2 s per-command and 12 s total budgets.
58
+ - The proof commands did not rewrite `config.toml` (inode, size and mtime unchanged), so the config fence does not trip on them.
59
+ - This machine's credential roots: none is a dangling link, and the checkout is inside none of them.
60
+
61
+ So no hub bug was reproduced in the proof legs, and the first failure on 2026-10-22 happened in a daemon whose environment is not reproducible from the file system alone. The fix is the diagnosis: the refusal now carries one of the closed classes `version`, `binary`, `mcp_inventory`, `plugins_hooks_apps`, `sandbox_proof`, `credential_root`, `timeout`, `unknown`. It is in the unavailable reason, a `hub.log` line and the `reviewer` line of `ahub status`, and never holds a path, native CLI text or a token (a credential root is named by its path class). The next `start_failed` on this machine therefore names its leg. Fixture tests cover each class (`test/codex-review-isolation.test.ts`, `test/reviewer-pool-daemon.test.ts`, `test/output.test.ts`).
@@ -0,0 +1,69 @@
1
+ # 0.14.0 release verification
2
+
3
+ This record covers release preparation and source review. It does not claim publication, installation, a running-hub restart or a live TUI launch.
4
+
5
+ ## Candidate
6
+
7
+ Version 0.14.0, from merged main at 11623553. On 2026-10-23 the person directed the release. It includes features, so the minor version is bumped (AGENTS.md, Releasing step 1).
8
+
9
+ - Roles capability per kind and form (#525-1, #725). A Claude channel instance may implement or verify in its own worktree. `planner` needs a kind that can plan, both for instances and for `roles` entries keyed by an instance id. This repository's claude holds the implementer role.
10
+ - The dashboard Usage view (#683 U4, #726) and the task sheet's Code group (#684 phase 2c, #731).
11
+ - SDLC metrics and control bands (#481 S7, #730): `ahub sdlc metrics`, `hub_sdlc_metrics`, and `[bands.<metric>]` with the log, diagnose and propose tiers.
12
+ - The compact `ahub board` (#724, #728): one line per task, with `--full` keeping the wrapped layout.
13
+ - Fixes:
14
+ - `ahub codex --as` for a headless reviewer (#720, #721);
15
+ - reviewer start-failure cause classes (#722, #727);
16
+ - issue identity linked at propose and accept (#723, #729);
17
+ - reserved backlog pulls offered to their reserved peer (#718, #732).
18
+ - Still open:
19
+ - #689, config editing;
20
+ - #684 phases 3 and 4;
21
+ - agent-mem#13 P3 (board #391) and P4 (#392);
22
+ - #719, the post-upgrade daemon CPU (#386);
23
+ - the release-metric bands and gates-run record (#481, recorded in the S7 amendment).
24
+
25
+ ## Reviews
26
+
27
+ The planner reviewed each PR. Where a PR changed shared storage, concurrency, privacy or the review path, an independent read-only reviewer also checked it in a detached worktree. Every codex-connector thread was fixed or answered, and resolved, before merge. Each PR merged on green CI at its exact head; a rebase that touched only CHANGELOG or the spec was rechecked to keep the code identical to the approved head.
28
+
29
+ | PR | Final head | Review | CI |
30
+ | --- | --- | --- | --- |
31
+ | #721 (#720) | see PR | Headless `--as` returns after the start | exact head, success |
32
+ | #725 (#525-1) | a4d0c284 | Connector P1: planner kept for instance-id roles, now gated in `effectiveInstanceRoles`; stale bundle rebuilt | exact head, success |
33
+ | #726 (#683 U4) | 4ac69e30 | Three rounds: behind flag, render tests, docs, then the manager forwarding `view` through an allowlist | exact head, success |
34
+ | #727 (#722) | 43334eb0 | Closed cause classes, no paths or native text; live check deferred to after this release | exact head, success |
35
+ | #728 (#724) | 5ebf02a5 | Connector P2: long owner id kept the row single-line; person approved | exact head, success (two unrelated macOS flakes rerun) |
36
+ | #729 (#723) | 32aa4f32 | Three connector threads fixed by the planner: ambiguous sibling refused, hint silent in mode off, backfill cursor | exact head, success |
37
+ | #730 (#481 S7) | af0b7451 | Three rounds: stale-head evidence, episode cards, held timer, spec amendment, rename to `first_pass_check`, drift floor | exact head, success |
38
+ | #731 (#684 2c) | e284dac2 | Two rounds: withheld plan names mark the group incomplete | exact head, success |
39
+ | #732 (#718) | 6a782550 | Two rounds: seed rot and an invalid seed detection fixed; explain previews the reserved-only pull | exact head, success |
40
+
41
+ ## Document review
42
+
43
+ Twelve documents were stale against the 0.13.0 stamps (88caef80, and d4defd2c for tests.md); the agent-notes README went stale through daemon.md during this review. Read-only reviewers in four groups checked each one against `git log <stamp>..HEAD` over its paths and the nine PRs, and corrected every statement a merged change made wrong.
44
+
45
+ | Document | Result | Checked claims and correction |
46
+ | --- | --- | --- |
47
+ | `README.md` | corrected | Status 0.14.0; `ahub sdlc metrics` and bands; the dashboard Usage view; verification link. |
48
+ | `docs/quickstart.md` | corrected | Install pin `v0.14.0`. "Upgrading to 0.14.0" covers the compact board, Usage view, Code group, metrics and bands, issue links, reviewer cause classes, Claude instances, reserved pulls, `--as` and the Claude restart. The board row in the working-day table. |
49
+ | `docs/operations.md` | corrected | The Usage view `unavailable` state and omitted-limits note; the board column rule (min(30, longest title)), plus AFTER and OWNER at narrow widths; the Code group counts and `planWithheld`; the classes a Claude implementer instance can still be routed; the #525 refusals; `--as` for hub-started reviewers; cause classes; metrics and bands; S4 and S6 shipped, S5 pending; issue linking; reserved pulls. |
50
+ | `docs/security.md` | corrected | The snapshot data of the Usage view and Code group; the manager's `view` allowlist; Claude instance worktrees; cause classes; issue linking with no GitHub read; the metrics authority, band card text and `band_notices`. |
51
+ | `src/cli/help.ts` | corrected | `ahub sdlc metrics [--since <YYYY-MM-DD\|ISO time>]`. Every subcommand and flag in main.ts and launch.ts has a row. |
52
+ | `docs/agent-notes/adapters.md` | corrected | Version and native-policy refusals keep their own sanitized message; how `ReviewerPrintPeer` classes its own failures. |
53
+ | `docs/agent-notes/budget.md` | corrected | The roll-up's readers include the console panel and the dashboard; `UsageRollup.daily` rules. |
54
+ | `docs/agent-notes/bus.md` | no change | No bus source changed; identifiers spot-checked. |
55
+ | `docs/agent-notes/daemon.md` | corrected | Scope includes `task-code-group.ts`. Rules for: the `view` allowlist, the Usage view, the Code group, the band timer, the compact board (`STAGE_CODES` tracking sdlc.ts), Claude instance roles, and `--as` for reviewer variants. |
56
+ | `docs/agent-notes/tasks.md` | corrected | Scope includes `sdlc-metrics.ts`. The #718 reserved-pull rule; `planner` needs `plannerEligible`; the qualified-title rule rewritten to match the code. |
57
+ | `docs/agent-notes/README.md` | no change | Background only; the new daemon.md rules leave its rationale entries unchanged (stale through daemon.md, a listed path). |
58
+ | `docs/agent-notes/tests.md` | no change | The new tests follow the existing rules; the #732 seed sentence matches seeds.json. |
59
+
60
+ AGENTS.md area lists and the `docs/verified.json` paths gain `sdlc-metrics.ts`, `usage-rollup.ts`, `task-code-group.ts`, `spec-read.ts` and `status-lines.ts`.
61
+
62
+ Follow-ups noted by the reviewers, not changed in this release:
63
+ - A person-started Claude implementer instance is refused only for implement, test and bulk_edit tasks (`ownerIsolation`). Other classes can still route to it until #525-5. operations.md now says so.
64
+ - The #725 planner filter also drops `planner` for configured ACP agents (they have no `projection`). This is documented now, but the PR did not announce it.
65
+ - The #684 2c spec amendment says `codeGroup` screens symbol ids; the screening happens earlier, in `publicView`.
66
+
67
+ ## Checks
68
+
69
+ The `scripts/check.sh` full gate, `node scripts/check-docs.mjs --require-fresh`, and CI on the release head are recorded in the release PR.
@@ -0,0 +1,80 @@
1
+ # 0.15.0 release verification
2
+
3
+ This record covers release preparation and source review. It claims no publication, installation, running-hub restart or live TUI launch.
4
+
5
+ ## Candidate
6
+
7
+ Version 0.15.0, built from merged main at 12dd7b66. The person directed the release on 2026-10-23. It includes features, so the minor version is bumped (AGENTS.md, Releasing step 1).
8
+
9
+ - Daemon responsiveness (#719, #741): the hot paths read only pending delivery-journal state, and a launch connect retries within a bound with `hub busy, retrying`. On this machine a 0.14.0 daemon sat at about 100% CPU. That also made codex-review's 2 s isolation proof commands time out, and the hub's 2 s claude-mem saves.
10
+ - Structural config editing from the console and a dashboard settings session: validated, previewed, audited and restart-aware (#689, #734).
11
+ - Claude implementer instances:
12
+ - a terminal instance's execution root is verified before it owns write tasks (#525-5, #742);
13
+ - a headless implementer has per-task sessions and person approvals that fail closed (#525-4, #747).
14
+ - Always-ask action classes are enforced in the Claude PreToolUse hook and in ACP `request_permission`, with a session class budget (#636 slice 2, #743).
15
+ - Routing:
16
+ - the planner walk and I-2 reviewer independence in `assign()`, shown by `ahub route explain` (#525-3, #744);
17
+ - no write-class task for a person-started Claude instance before 525-5 (#735, #738).
18
+ - Review evidence:
19
+ - acceptance-criteria ids in the review request and on the verdict (#507, #745);
20
+ - deliverable records bound to content hashes (#509 AC1, AC2, AC4, #746).
21
+ - Kimi tokens counted from its session wire log (#285 phase 2, #740).
22
+ - A finding carried to a follow-up of the same PR or branch is not a recurrence (#736, #737).
23
+ - `ahub backlog list` prints a table (#399, #739).
24
+ - Still open:
25
+ - #636 slice 3 (#313, PR #750);
26
+ - #285 phase 4 (#315, PR #749);
27
+ - the memory sync log fix (#401, PR #748);
28
+ - agent-mem#13 P3 (#391, agent-mem PR #34) and P4;
29
+ - #525-2 and #525-6.
30
+
31
+ ## Reviews
32
+
33
+ The planner reviewed every PR. Where a PR changed permissions, process ownership, privacy or task authority, an independent read-only reviewer also checked it. Every codex-connector thread was fixed or answered, and resolved, before merge. Each PR merged on green CI at its exact head. Where a rebase touched only CHANGELOG, the result was checked to keep the code identical to the approved head.
34
+
35
+ | PR | Final head | Review | CI |
36
+ | --- | --- | --- | --- |
37
+ | #734 (#689) | see PR | Structural config editor, audit and person-only paths | exact head, success |
38
+ | #737 (#736) | see PR | Recurrence unions work units by PR and branch | exact head, success |
39
+ | #738 (#735) | see PR | Temporary write-class refusal for person-started Claude instances | exact head, success |
40
+ | #740 (#285 p2) | see PR | Kimi wire-log reader and counter | exact head, success |
41
+ | #741 (#719) | see PR | Pending-only journal reads; bounded launch-connect retry | exact head, success |
42
+ | #742 (#525-5) | 1c84e9d7 | Two rounds. A verifier instance gets no canonical fallback; best-effort hello cwd; seed re-anchored. Follow-up #400 records the status-path revalidation cost | exact head, success |
43
+ | #743 (#636 s2) | 66658255 | Two rounds plus a rebase onto #742. The hook contract allows `ask`; the class budget is revoked when the owner stops; concurrent approvals cannot exceed N; enforcement is claimed only for a live hook | exact head, success |
44
+ | #744 (#525-3) | 2fa94797 | Two rounds. I-2 is the last check, so only an eligible former implementer is removed; dogfood row | exact head, success |
45
+ | #745 (#507) | 45b2ca8e | One round, no findings | exact head, success |
46
+ | #746 (#509) | 1410aa21 | Two rounds. The identity check covers size and mtime (Linux reuses inodes); the spec gate sees the stripped receipt; reads apply the aggregate limit; the owner's verified root | exact head, success |
47
+ | #747 (#525-4) | eefc3899 | Two rounds, with an independent reviewer. Admissible envelopes are not held by inadmissible ones; approval cuts are marked; the inbox stays per task session; a session starts only on a native frame; `role_stages` recovery floor | exact head, success |
48
+ | #739 (#399) | b3ffd61c | Person-reviewed (implemented by the planner); rebased over CHANGELOG only | exact head, success |
49
+
50
+ ## Document review
51
+
52
+ Thirteen documents were stale against the 0.14.0 stamps (d21a9ccc, and 88caef80 for benchmarks.md). Read-only reviewers in four groups checked each one against `git log <stamp>..HEAD` over its paths and the twelve PRs. They corrected every statement a merged change had made wrong.
53
+
54
+ | Document | Result | Checked claims and correction |
55
+ | --- | --- | --- |
56
+ | `README.md` | corrected | Status 0.15.0 and the verification link. Task board: reviewer independence, AC ids, hash-bound deliverables, terminal and headless Claude implementers. Always-ask in the guard rails. Structural config editing. |
57
+ | `docs/quickstart.md` | corrected | Install pin `v0.15.0`. A new "Upgrading to 0.15.0" section: always-ask prompts and budgets, which peers are not enforced, relaunching terminal Claude implementers, declaring a headless implementer, deliverables, AC ids, `ahub settings edit`, the busy-retry message, and restarting the hub and codex-review. The 0.14.0 section is kept below it. |
58
+ | `docs/security.md` | corrected | What always-ask enforces and its limits. Classed calls are excluded from hub-tool auto-approval and `--unattended`. Structural `instances` editing. Terminal execution-root proof. The headless approval path (fails closed, allow-once only, `[withheld]` before `[cut, N chars]`). Deliverable hashing. Kimi wire-log counts only. |
59
+ | `docs/operations.md` | corrected | Kimi usage rows are `native`. Jcode is not enforced. The headless implementer hook asks. The startup ceiling is still open. `ahub settings edit` for never-editable keys. The console `c` and `R` keys and the dashboard Structure group. The deliverable refusal text. The backlog list owner count. The 525-3/4/5 slices are shipped; 525-2 and the plan-author rule are still planned. |
60
+ | `src/cli/help.ts` | corrected | Every command, subcommand and flag compared with main.ts, launch.ts, settings-edit.ts and the native and configured-ACP entries. Added `backlog list --color`; corrected the `ui --settings` and `route explain` descriptions. |
61
+ | `docs/agent-notes/adapters.md` | corrected | Fixed-builtin-only fields include `identity.currentChild`, `headlessVariant` and the implementer port. `definitionOf` picks the headless variant. `OwnedPrintChild` is the only print-child spawn path. A headless implementer section. Scope gains `action-enforce.ts` and `permission-payload.ts`. |
62
+ | `docs/agent-notes/daemon.md` | corrected | The writer declaration rule replaces "refused until #525-4". `peer_stop` covers print reviewers and the implementer. The `role_stages` floor. Facts for the implementer child and worktree instances. Reads go through `readClaudeLaunchProof`. Execution-root proof. Tools-socket admission. `retryLaunchConnect` limits. Scope gains the config-edit modules and `claude-launch-proof.ts`. |
63
+ | `docs/agent-notes/bus.md` | corrected | Hot paths read only pending journal state (#741). |
64
+ | `docs/agent-notes/budget.md` | corrected | Headless variants share the base kind's account; the roll-up's 20 ms slice. Scope gains the Kimi count files. |
65
+ | `docs/agent-notes/benchmarks.md` | no change | The driver passes no `actionGate`, so ACP benchmark behavior is unchanged. |
66
+ | `docs/agent-notes/tasks.md` | corrected | The #738 temporary refusal is removed. Declared-extra admission covers every implementer-owned class. I-2 reaches the person only when it removed every eligible reviewer. New rules for `unmetCriteria`, owned-child authority rechecks and deliverables. Scope gains `workflow.ts`, `deliverables.ts`, `deliverable-files.ts` and four listed files. |
67
+ | `docs/agent-notes/tests.md` | corrected | The hub-home preload replaces `KIMI_CODE_HOME`. Conformance enrolls `:implementer` variants. Headless implementer tests use the fake with a temporary HOME and never start the real `claude`. |
68
+ | `docs/agent-notes/README.md` | corrected | Background only: why I-2 is the last reviewer check. No instructions. |
69
+
70
+ The AGENTS.md area lists and the `docs/verified.json` paths gain the release's new files: `action-enforce.ts`, `permission-payload.ts`, `config-tier.ts`, `config-edit.ts`, `config-edit-host.ts`, `claude-launch-proof.ts`, `kimi-wire-usage.ts`, `kimi-usage.ts`, `token-window.ts`, `workflow.ts`, `deliverables.ts` and `deliverable-files.ts`.
71
+
72
+ ## Package contents
73
+
74
+ #746 committed a mutation-test backup, `src/hub/deliverable-files.ts.bak`, with the aggregate size limit disabled. The release branch removes it so it does not ship under `src/`.
75
+
76
+ ## Known gaps, not blocking
77
+
78
+ - `ahub status` reports always-ask on a headless Claude implementer as "adapter integration pending", although its hook and approvals enforce the classes. It under-reports and never over-claims. Follow-up task.
79
+ - `ahub projects` and `queue list` accept `--full` without effect.
80
+ - The live legs are a person's: a real Claude Code `ask` prompt (#743), a real-account headless implementer turn (#747) and a multi-instance CPU check (#742).
@@ -0,0 +1,44 @@
1
+ # Daemon responsiveness, 2026-10-23
2
+
3
+ ## Read-only live evidence
4
+
5
+ The running released 0.14.0 daemon was sampled for five seconds without restart, mutation or signalling unrelated processes. Of 3,898 main-thread samples, 2,860 were kernel waits. SQLite appeared in 504 samples, including 303 sorter/merge samples; guarded pwrite appeared in 240 leaf samples. These native stacks lack JavaScript symbol attribution. The initial process listing showed lifetime-average CPU 83.9% and RSS about 1.4 GB, while the sample reported a smaller current physical footprint; those are different measurements, not evidence of a sustained current 83.9% load or a GC cause.
6
+
7
+ Bounded read-only queries found 2,051 delivery rows (only three accepted), with about 8.7 MB of originals/out JSON. `list()` and `snapshot()` sorted full payload rows through a temporary B-tree. The released one-second supervision path called `queueList()` (list plus snapshot for revision) and `knownPeers()` (another full list). Held-row/status and superseded-delivery paths added further whole-history reads. A capped read-only SQL probe measured 16.49 ms for the sorted full read versus 5.56 ms unsorted, excluding JSON parsing.
8
+
9
+ The board had 398 tasks, seven active/non-backlog tasks and at most 28 history entries per task; planner-WIP probes were below 0.2 ms. Usage tables were small (39 native-rollup and 256 token-rollup rows), and the persisted log offset was only 472 bytes behind the 51.1 MB events file. These observations make those queries weaker explanations for the sampled sustained sorter activity. They do not rule out a cold-start fold stall.
10
+
11
+ ## Candidate and synthetic comparison
12
+
13
+ The candidate uses pending-state metadata/rows on periodic roster, supervision and held-delivery paths, with a pending-state index. Queue revision is read as metadata; integrity calculations that need queues omit the journal snapshot. Explicit queue/history and recovery snapshots retain terminal history. Usage passes retain their byte cap and atomic rows/offset transaction and stop at a 20 ms boundary between whole records. One record and the existing corrupt-line skip remain indivisible. Launch-only connections retry transport timeouts/unclean closes up to 15 seconds with one busy notice; ordinary three-second connect defaults and terminal authentication/protocol/identity refusals remain.
14
+
15
+ A fresh synthetic journal contained 2,048 terminal and three pending rows with 2 KiB envelope bodies. Ten measured iterations followed two warm-ups, using the same data and process:
16
+
17
+ | Query bundle | Median ms | Max ms |
18
+ | --- | ---: | ---: |
19
+ | Former roster/list/revision-snapshot reads | 67.413 | 69.536 |
20
+ | Pending metadata/rows and direct revision | 0.125 | 0.135 |
21
+
22
+ This is an isolated query-bundle comparison, not a claim about installed daemon CPU, RSS, total request latency or an improvement factor for the whole application. No live database was copied into the fixture.
23
+
24
+ ## Regression evidence
25
+
26
+ - Reintroducing full-history roster reads or recovery-snapshot revision reads fails a named no-throw assertion against a journal that refuses those hot-path calls. Explicit history still returns its 513 fixture rows.
27
+ - Removing the fold time boundary fails the named byte-offset assertion for a zero-budget slice. Complete Unicode record boundaries and rebuild parity remain checked.
28
+ - A real daemon with a cold log larger than 50 MiB starts a launch socket inside its actual catch-up pass and answers the authenticated status request. This end-to-end check is separate from the deterministic slice regression; it does not claim an old implementation always exceeded three seconds.
29
+ - A fake-clock test exhausts the launch budget at exactly 15 seconds, checks backoff and a single busy notice, and proves immediate unreachable-listener, authentication, wire, instance and unclassified refusals are not retried.
30
+ - Focused five-file verification passed 45 tests and 253 expectations on pinned Bun 1.4.2 after the fixture-size correction. The initial fixture-size assertion was retained; its data was increased above 50 MiB.
31
+
32
+ Installed-runtime CPU/RSS and account-backed native launches remain unverified for this candidate. The existing live daemon was left running unchanged.
33
+
34
+ The first complete changed gate passed 3,366 tests but exposed one real regression: retrying immediate connection refusal made three failed-launch ownership checks exceed their unchanged 20-second test deadline. The code now treats an unreachable listener as terminal and retries the busy timeout/unclean-close classes only. The original recovery test and timeout are unchanged.
35
+
36
+ After that fix, the native six-file focused run passed 63 tests/392 expectations using the gate's normal isolated identity/color/Git environment, including the unchanged three-failed-launch ownership test. A prior direct invocation retained agent markers and got two fixture-init refusals; those tests and product authority checks were not changed. The corrected invocation uses exactly the environment clearing already present in `scripts/check.sh`.
37
+
38
+ ## Native reviewer and final gate
39
+
40
+ On this macOS machine, the candidate daemon served a synthetic 56,500,109-byte event log and 2,048 terminal journal rows. Its actual authenticated console `start codex-review` path admitted an explicitly declared fresh reviewer: state `idle`, observed CLI 0.146.0, start 469.61 ms. A fresh production preparation using that isolated daemon's same context completed in 397.64 ms and returned 14 evidence entries with `readOnly`, `strictMcp`, `ephemeral` and `current` true. The production 2-second per-command and 12-second total proof budgets were unchanged.
41
+
42
+ The probe used an empty temporary HOME/CODEX_HOME and no credentials, login or model execution. It did not replace the installed daemon or demonstrate an account-backed model review. Initial probe-only setup mistakes (attempting to instrument the frozen backend object, then omitting the required fresh-instance declaration) were corrected without changing product code. All owned fixture processes and temporary homes were cleaned up.
43
+
44
+ Final native pinned-Bun gate on runtime `b0a2d4b1293fe39c6e4ac89b1a658fc35dba0247`: `scripts/check.sh --changed` ended `check: OK (changed)`, 3,368 pass, 0 fail, 166,362 expectations across 259 files in 747.15 seconds; no current-invocation daemon leaks. Typecheck, committed-bundle freshness and tarball checks passed. Remote Linux verification could not start because its SSH route was unreachable, so this receipt uses the permitted native fallback; no Linux pass is claimed for this candidate.
@@ -0,0 +1,47 @@
1
+ # Headless Claude permission transport receipt
2
+
3
+ Date: 2026-10-23. Scope: #525 R5a, slice 525-4. Native binary: Claude Code 2.1.309, 275,923,296 bytes, SHA-256 `5eadc0009cdcdbc0d06e58110d7b2baf88ced6088b1c33a60a21217fe410619c`.
4
+
5
+ ## Method
6
+
7
+ The installed native binary ran with an isolated HOME and CLAUDE_CONFIG_DIR, a disposable work directory, one fixture stdio MCP server, an empty settings-source selection and managed fixture settings. No personal authentication files or live sessions were used. A deterministic loopback HTTP server supplied a synthetic model-protocol response requesting one Write, then a completion response after a tool result. This exercised the real native permission dispatch without a provider, model inference, login or real credential. Synthetic provider usage in this fixture is not measured account usage.
8
+
9
+ The launch used this form, with fixture paths and a fixture permission tool:
10
+
11
+ ```sh
12
+ claude -p --output-format stream-json --verbose --strict-mcp-config \
13
+ --mcp-config <fixture-mcp.json> --setting-sources '' \
14
+ --settings <fixture-settings.json> --permission-mode default \
15
+ --permission-prompts host --permission-prompt-tool mcp__fixture__approve \
16
+ --model claude-sonnet-4-5 <fixture-prompt>
17
+ ```
18
+
19
+ No `--bare`, `acceptEdits`, `dontAsk` or `bypassPermissions` was used. Native and fixture children belonged to an owned process group, which was stopped and reaped after every case. The temporary probe driver's SHA-256 was `173c108ff5e0289434c7794e985a48b612c23df81e10d0fc2abe48a297de9ae5`. The recorded receipt and driver hashes agreed when this document was prepared. The driver is a local diagnostic, not a shipped runtime dependency or a startup receipt gate.
20
+
21
+ ## Observations
22
+
23
+ | Fixture case | Native result |
24
+ | --- | --- |
25
+ | Prompt tool allows the Write | One MCP permission call; target file written |
26
+ | Prompt tool denies | No write; one permission denial |
27
+ | Prompt tool missing from MCP inventory | No write; native exit 1 |
28
+ | PreToolUse command exits 1, prompt tool denies | Hook decides nothing; one MCP permission call; no write |
29
+ | PreToolUse command exits 1, prompt tool allows | Hook decides nothing; one MCP permission call; file written |
30
+ | MCP permission request returns a JSON-RPC error | No write; native tool-result error |
31
+ | MCP server disconnects during permission request | No write; native tool-result error |
32
+ | MCP permission reply remains pending | No write observed before the external 25-second owned-group stop; native internal timeout not established |
33
+ | Simulated approval deadline returns deny after 100 ms | One MCP permission call; no write; native exit 0; observed elapsed 844 ms |
34
+
35
+ The last row simulates the hub's deny response. It does not measure the real hub approval scheduler or a person's console interaction. Separate adapter fixtures cover allow, deny, externally bounded timeout, tool error and disconnect; those fixtures do not constitute native-client evidence by themselves.
36
+
37
+ The native prompt request contained `tool_name: "Write"`, the original `input` object and `tool_use_id`. The fixture allow response was an MCP text content item containing JSON with `behavior: "allow"` and `updatedInput`; the deny response used `behavior: "deny"` and `message: "no person answered"`. Fixture paths and text are omitted here.
38
+
39
+ ## Host behavior and ceilings
40
+
41
+ The production permission tool is `mcp__agent-hub__hub_permission_request`. Its current-child launch and generation fence, privacy screening, person-only approval and timeout denial remain host responsibilities. A failed facts hook supplies no decision: a subsequent valid person allow is permitted. A failed permission prompt, timeout, disconnect or mismatched child authority never supplies an automatic allow.
42
+
43
+ The native probe certifies the permission transport on this binary and version. It does not certify real provider authentication, a real model's tool selection, a person's approval interaction, every native tool type, native internal permission timeout, or filesystem sandbox isolation. The pending fixture was bounded externally. No model turn using a real account was run. Native version admission and binary fencing remain runtime checks; this dated receipt is evidence for the supported contract, not a per-start runtime receipt check.
44
+
45
+ The exact-head package gate, daemon integration and both-OS CI are recorded separately in the PR handoff. This document claims no gate or CI result for the current candidate.
46
+
47
+ The MCP wire does not authenticate whether a schema-valid permission call came from native permission dispatch or was selected directly by the model. `tool_use_id` is a shape check, not such a certificate. Either path still requires a fresh person allow-once for its original arguments; it changes no permission default and creates no reusable grant. A model-selected call can prompt the person but cannot turn its returned text into authority for a later native call. Strong dispatch-origin provenance would need a separately proven native protocol, rather than treating a caller-supplied id as evidence.