@staix/agent-hub 0.14.0 → 0.16.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.
- package/CHANGELOG.md +42 -0
- package/README.md +23 -9
- package/docs/agent-notes/README.md +8 -0
- package/docs/agent-notes/adapters.md +21 -4
- package/docs/agent-notes/budget.md +6 -4
- package/docs/agent-notes/bus.md +1 -0
- package/docs/agent-notes/daemon.md +20 -9
- package/docs/agent-notes/local-worker.md +2 -2
- package/docs/agent-notes/tasks.md +21 -4
- package/docs/agent-notes/tests.md +6 -3
- package/docs/dogfood.md +19 -5
- package/docs/events.md +21 -7
- package/docs/memory.md +78 -10
- package/docs/operations.md +197 -39
- package/docs/profiles.md +1 -0
- package/docs/quickstart.md +16 -6
- package/docs/sdlc.md +17 -0
- package/docs/security.md +110 -21
- package/docs/smoke.md +7 -3
- package/docs/specs/2026-09-19-agent-hub-design.md +156 -16
- package/docs/verification/2026-10-23-0.15.0.md +80 -0
- package/docs/verification/2026-10-23-daemon-responsiveness.md +44 -0
- package/docs/verification/2026-10-23-headless-claude-permissions.md +47 -0
- package/docs/verification/2026-10-23-terminal-claude-execution-root.md +41 -0
- package/docs/verification/2026-10-24-always-ask-adapters.md +52 -0
- package/docs/verification/2026-10-24-amem-runtime.md +69 -0
- package/docs/verification/2026-10-24-codex-0164-reviewer-isolation.md +55 -0
- package/docs/verification/2026-10-24-delivery-holds-ac8.md +264 -0
- package/docs/verification/2026-10-24-hardcoded-values.md +2108 -0
- package/docs/verification/2026-10-25-0.16.0.md +82 -0
- package/docs/verified.json +40 -19
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +3109 -2046
- package/src/adapters/acp.ts +46 -7
- package/src/adapters/claude-channel.ts +26 -4
- package/src/adapters/claude-implementer.ts +243 -0
- package/src/adapters/claude-print-child.ts +90 -0
- package/src/adapters/claude-task-sessions.ts +80 -0
- package/src/adapters/codex-appserver.ts +69 -1
- package/src/adapters/compat.ts +6 -2
- package/src/adapters/conformance/enrollment.ts +2 -1
- package/src/adapters/conformance/fakes/acp-server.ts +3 -1
- package/src/adapters/definitions/builtin-metadata.ts +17 -5
- package/src/adapters/definitions/native-channel-state.ts +1 -1
- package/src/adapters/definitions/native-cli-entry-claude.ts +13 -8
- package/src/adapters/definitions/native-cli-entry-types.ts +2 -0
- package/src/adapters/definitions/native-factories.ts +8 -0
- package/src/adapters/kimi-wire-usage.ts +303 -0
- package/src/adapters/local-worker.ts +5 -4
- package/src/adapters/registry-types.ts +8 -4
- package/src/adapters/registry.ts +14 -2
- package/src/adapters/reviewer-print.ts +30 -53
- package/src/cli/console-config.ts +123 -0
- package/src/cli/console-state.ts +48 -3
- package/src/cli/console-symbols.ts +3 -2
- package/src/cli/console-usage.ts +5 -2
- package/src/cli/console.ts +30 -2
- package/src/cli/facts-hook.ts +9 -5
- package/src/cli/help.ts +10 -7
- package/src/cli/identity.ts +3 -1
- package/src/cli/launch.ts +10 -4
- package/src/cli/main.ts +91 -19
- package/src/cli/memory.ts +119 -7
- package/src/cli/output.ts +40 -4
- package/src/cli/reset.ts +1 -1
- package/src/cli/settings-edit.ts +89 -0
- package/src/cli/terminal-recovery.ts +1 -6
- package/src/hub/action-class.ts +211 -17
- package/src/hub/action-enforce.ts +90 -0
- package/src/hub/action-policy.ts +40 -17
- package/src/hub/ask.ts +16 -4
- package/src/hub/board.ts +110 -31
- package/src/hub/budget-notes.ts +65 -0
- package/src/hub/budget.ts +44 -7
- package/src/hub/bus.ts +6 -6
- package/src/hub/claude-launch-proof.ts +33 -0
- package/src/hub/code-facts.ts +56 -16
- package/src/hub/conductor.ts +5 -2
- package/src/hub/config-apply.ts +12 -0
- package/src/hub/config-edit-host.ts +173 -0
- package/src/hub/config-edit.ts +282 -0
- package/src/hub/config-tier.ts +89 -0
- package/src/hub/control-client.ts +24 -3
- package/src/hub/daemon.ts +614 -104
- package/src/hub/deliverable-files.ts +131 -0
- package/src/hub/deliverables.ts +125 -0
- package/src/hub/delivery-journal.ts +18 -2
- package/src/hub/envelope.ts +1 -1
- package/src/hub/events.ts +5 -3
- package/src/hub/hub-tools.ts +22 -6
- package/src/hub/instances.ts +7 -4
- package/src/hub/kimi-usage.ts +156 -0
- package/src/hub/lifecycle.ts +10 -5
- package/src/hub/manager.ts +1 -1
- package/src/hub/memory-code-head.ts +44 -0
- package/src/hub/memory-code-overlay.ts +119 -0
- package/src/hub/memory-plan-guard.ts +10 -1
- package/src/hub/memory-stale-anchors.ts +159 -0
- package/src/hub/memory-symbols.ts +74 -0
- package/src/hub/peers.ts +7 -0
- package/src/hub/permission-payload.ts +15 -0
- package/src/hub/planning.ts +10 -4
- package/src/hub/read-tools.ts +21 -3
- package/src/hub/routing.ts +118 -3
- package/src/hub/sdlc.ts +34 -0
- package/src/hub/settings.ts +18 -3
- package/src/hub/settled-roles.ts +15 -3
- package/src/hub/task-sweep.ts +25 -3
- package/src/hub/tasks.ts +551 -75
- package/src/hub/token-window.ts +46 -0
- package/src/hub/usage-rollup.ts +16 -6
- package/src/hub/window-kind.ts +13 -0
- package/src/hub/workflow.ts +16 -0
- package/src/local/tools.ts +5 -5
- package/src/memory/brief.ts +22 -12
- package/src/memory/capture.ts +26 -40
- package/src/memory/client.ts +137 -65
- package/src/memory/recall.ts +13 -22
- package/src/ui/index.html +215 -19
|
@@ -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.
|
|
435
|
-
|
|
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
|
|
@@ -922,11 +926,21 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
|
|
|
922
926
|
|
|
923
927
|
This slice provides the pure classifier, machine-local policy, host-only per-session budget ledger and per-peer status. Adapter pre-tool enforcement is a later slice. Until a runtime actually consumes the policy before execution, its status says `always-ask not enforceable: adapter integration pending`; native modes, sandboxes and approval frequency retain their existing behavior. The default-on configuration is not a claim of active enforcement.
|
|
924
928
|
|
|
925
|
-
- `action_policy` may be set only in untracked `.agenthub/config.local.json`, never in `config.json` even when that file is untracked. `ALL_MACHINE_LOCAL` trust drops a tracked/unknown local source. Default: `enabled: true`, all four classes in `always_ask`, budgets `publish: 1`, `destroy: 3`, `external-write: 5`, `install: 2`. Budgets are integers 0..1000; zero caches no follow-up grant. Empty `always_ask` selects no classes. The settings registry exposes enabled and each budget; the closed class set is changed in the local file with a person-confirmed reload. Weakening on live reload requires existing person confirmation; shared-file policy edits are refused before any confirmation offer. Startup
|
|
929
|
+
- `action_policy` may be set only in untracked `.agenthub/config.local.json`, never in `config.json` even when that file is untracked. `ALL_MACHINE_LOCAL` trust drops a tracked/unknown local source. Default: `enabled: true`, all four classes in `always_ask`, budgets `publish: 1`, `destroy: 3`, `external-write: 5`, `install: 2`. Budgets are integers 0..1000; zero caches no follow-up grant. Empty `always_ask` selects no classes. The settings registry exposes enabled and each budget; the closed class set is changed in the local file with a person-confirmed reload. Weakening on live reload requires existing person confirmation; shared-file policy edits are refused before any confirmation offer. Startup adopts the untracked local policy without a retained confirmation and surfaces attention in status when it disables enforcement, omits a class or increases a default budget (ponytail: retain a person-bound startup confirmation). Reload never imports code or changes a sandbox.
|
|
926
930
|
- `classifyAction(tool, args, executionRoot)` returns all matching classes for bounded literal tool/argv or shell forms. Quotes, command/group separators, fd duplication and plain input/output redirections are parsed without evaluation; known wrapper prefixes and literal shell -c forms are unwrapped. Multi-class chains retain every class, so narrowing the ask set cannot hide another class. Publish covers remote Git/package/release publication, external-write covers known non-read HTTP/gh/mail/chat calls, destroy covers destructive Git and recursive deletion/force-overwrite outside the lexical execution root, and install covers known global installs. Unsupported syntax stops parsing while retaining classes from earlier complete segments; the unsupported current segment remains unknown. Hard input/segment limits still return no classification. Unknown forms retain native policy, never an automatic permission. Literal cwd/workdir, home spellings conservatively treated as outside, and conservative literal cd candidates are covered; aliases, functions, expansion, scripts and symlink targets are not attested. The ceiling is bounded syntax/lexical evidence; the upgrade path is typed native tool arguments and canonical path evidence from each adapter.
|
|
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`). Live local/Pi tool boundaries and a managed headless Claude implementer with a current busy child generation are enforceable. Codex remains conservatively not enforceable because native policy may omit a request; every other peer without a hub pre-tool path says `always-ask not enforceable: <reason>`.
|
|
940
|
+
|
|
941
|
+
- **Remaining adapters (#636 slice 3).** Codex command approval RPCs (v2 command execution, excluding writeStdin, and legacy exec argv) consult the shared gate before native forwarding. A classed request is answered only once, bound to its link/thread/turn; cancellation, disconnect and stale answers deny. Unknown payloads and other approval schemas retain native handling. This proxy cannot intercept a native call that emits no request, so status never claims complete Codex enforcement. Local/Pi tools pass parsed arguments into their existing permit boundary and classify before mode or tool-wide grants. Pi's persisted call receipt encloses the gate, so replay spends no allowance twice. Only verified person-authored idle shell requests retain their existing bypass.
|
|
942
|
+
- **Rendered approvals.** Public/native command views are privacy-screened before rendering. A cut or withheld view offers deny only and cannot authorize even one invocation or a class budget. Local/Pi use their exact full host-generated tool title with a class line on the person's on-prem console, PII turns included; logs, public status, other peers and dashboard projections still withhold it. The same explicit class-budget choice, session/owner revocation and final-answer fence apply to received Codex, local and Pi approvals. Managed Claude forms keep asking through their native prompt path.
|
|
943
|
+
|
|
930
944
|
## Amendment: approval notifications and timeout (issue #5)
|
|
931
945
|
|
|
932
946
|
- A waiting permission request raises one macOS notification (`osascript`) with
|
|
@@ -2463,6 +2477,19 @@ acts on the stopped state directory.
|
|
|
2463
2477
|
- 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
2478
|
- `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
2479
|
|
|
2480
|
+
## Amendment: structural configuration from the console and the dashboard (issue #689)
|
|
2481
|
+
|
|
2482
|
+
- 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.
|
|
2483
|
+
- 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.
|
|
2484
|
+
- 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.
|
|
2485
|
+
- 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.
|
|
2486
|
+
- 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.
|
|
2487
|
+
- 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.
|
|
2488
|
+
- 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`.
|
|
2489
|
+
- 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.
|
|
2490
|
+
- 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.
|
|
2491
|
+
- Unverified: a live `ahub ui --settings` browser session and a real restart that picks up an edited `sdlc.toml` (the person's leg).
|
|
2492
|
+
|
|
2466
2493
|
## Amendment: TUI by default, headless as opt-in (issue #269, part 2)
|
|
2467
2494
|
|
|
2468
2495
|
- `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 +2726,7 @@ the queue.
|
|
|
2699
2726
|
- 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
2727
|
- 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
2728
|
- 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.
|
|
2729
|
+
- 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
2730
|
- 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
2731
|
|
|
2704
2732
|
|
|
@@ -4392,7 +4420,7 @@ Stage to role, fixed:
|
|
|
4392
4420
|
- **I-1 (existing).** The reviewer is never the current owner, by id (`routing.ts:494`, `:541`).
|
|
4393
4421
|
- **I-2 (new).** The reviewer is never a peer that implemented this task: any peer with an `accepted` or `done` history entry `by` it on this task, read from `task.history` inside `assign()`. Refusal code `policy`, reason `implemented this task`. When this empties the candidates the reviewer is the person (`reviewBlocked`), never a fallback to an implementer.
|
|
4394
4422
|
- **I-3 (existing, #519).** Other family first; own family only when no other is eligible, recorded as `sameFamilyFallback=true` (`routing.ts:547-550`).
|
|
4395
|
-
- **I-4 (new, enforced in `Tasks.recordPlanReview`, not in `assign()`).** The plan author does not record the plan review of that plan. Plan author: `by` of the latest history entry with `planRevision
|
|
4423
|
+
- **I-4 (new, enforced in `Tasks.recordPlanReview`, not in `assign()`).** The plan author does not record the plan review of that plan. Plan author: `by` of the latest plan-authoring history entry with `planRevision`, then a legacy authoring entry whose `planning.fields` includes `plan`. Exclude `plan reviewed` entries from both paths: review/CI scope bookkeeping never changes the plan-content author. When only the initial proposal remains, use its author only if intent list fields are nonempty; review/CI scope alone leaves the author unknown. A later actual plan edit or explicit intent removal records its own author. If the caller is the author and another planner holder is rostered and not offline, refuse pre-effect ("an independent planner reviews this plan"). If the caller is the author and no other holder is eligible, record the receipt with `selfReview: true` (the single-planner case keeps working).
|
|
4396
4424
|
- **Independent reviewer authority.** Recording a plan review is the one planner act that is not limited to the task's planner: any current settled planner holder other than the plan author may record it, and so may the person. That is what keeps I-4 from deadlocking against R3's per-task authority. The review is the only power this grants: the reviewing holder still cannot plan, split, pull, undo, cancel, reassign or release the task (R3). When the effective planner is the author and an independent holder is eligible, the hub sends that holder a plan-review request (ids only, through `publicView`), chosen by the same rotation as R3 and excluding the author. Unknown author records `planAuthor: null` and does not refuse. The receipt gains `planAuthor`.
|
|
4397
4425
|
- Planner and owner may be the same peer; planner and reviewer may be the same peer (today's default `claude: [planner, reviewer]`). I-4 covers the only conflict between them.
|
|
4398
4426
|
- **Purity.** The planner walk runs inside `assign()` with the same inputs it already takes plus `planners` (sorted settled holder ids) and `namedPlanner`. `Assignment` gains `planner?: PeerId` and `plannerReason?: "rotation" | "named" | "kept" | "none"`. Every caller that routes a new or pulled task reads the planner from this result; `ahub route explain` prints the same walk (`planner candidate X: ...` lines, then `planner: X (reason)`), so explain and assignment cannot drift. `Tasks` never chooses a planner outside `assign()`.
|
|
@@ -4405,9 +4433,10 @@ Stage to role, fixed:
|
|
|
4405
4433
|
- a person-named planner (`namedPlanner`) is exact, refused like a named owner if not a holder (#601 rule);
|
|
4406
4434
|
- otherwise rotation: holders sorted by id, start at `task.id mod holders.length`, walk forward skipping off-roster, offline, paused (`budget_paused = skip_peer`), failing and `routing.allowedRoles`-excluded holders; do not renumber by availability (the #519 seed rule). If all are skipped, take the seed holder with reason `rotation (none available)`.
|
|
4407
4435
|
- PII tasks get `planner: null` and trace `planner: none (pii)`; their planning stays as today (stubs only, person acts).
|
|
4408
|
-
- The choice is written on the `assigned` (or `backlog pulled`) history entry as `planner` and `plannerReason`, through the same metadata path as `reviewerFamily`/`reviewerReason`.
|
|
4409
|
-
- **Effective planner.** `Tasks.taskPlanner(task)`: `task.planner` when it is still a settled planner holder and rostered; otherwise all current holders (legacy). A pause or offline state does not move it; the person moves it.
|
|
4436
|
+
- The choice is written on the `assigned` (or `backlog pulled`) history entry as `planner` and `plannerReason`, through the same metadata path as `reviewerFamily`/`reviewerReason`. A fixed pure host port binds every new proposal, split child and generated backlog card in its original insert transaction, with planner metadata on the initial `proposed`/`backlogged` entry. Notifications never expose an unbound intermediate new row, and split undo keeps its original child snapshot.
|
|
4437
|
+
- **Effective planner.** `Tasks.taskPlanner(task)`: `task.planner` when it is still a settled planner holder and rostered; otherwise all current holders (legacy). A pause or offline state does not move it; the person moves it. Routing explain uses the bound planner as `keptPlanner` and names it as `kept`; a draft or legacy null binding still runs rotation. Explaining either form writes no binding or history.
|
|
4410
4438
|
- **Authority.** `planningAuthority(by)` (`tasks.ts:833`) becomes `planningAuthority(by, task?)`: with a task, `by` must be the person or the task's effective planner; without one (backlog list, release list, new release, grooming read, import) any holder, as today. Every call site that has a task in hand passes it: pull (`:892`), plan (`:1035`), undo (`:1089`), split (`:1148`), assign/configure/cut release (`:1218-1286`, against `releases.planner`), plan review (`:2461`, which follows I-4's independent reviewer rule instead of the task-planner check), planner cancel (`:2247`), and the custom stage actor (`:2511`). Rechecks after awaits keep their places. Grooming reads stay project-wide; grooming, sweep and stall notices (`:397-401`, `:426`, `:2236`, `:938-971`) go to the task's effective planner only.
|
|
4439
|
+
- **Planner lifecycle stage identity.** The effective task planner may record only `stage`, `repo` and `issue` when an explicitly supplied stage has actor `planner` or `owner/planner`. Existing enforce relink and stage/spec gates still apply; advise retains its existing identity relink behavior. This grants no reviewer evidence, checklist, head or deliverable authority.
|
|
4411
4440
|
- **Naming.** `ahub task assign <id> <peer> --role planner` (person only; `TaskAssignmentRole` gains `planner`, `src/hub/tasks.ts:42`; parsers `src/cli/main.ts:1292`, `:2109`; help `src/cli/help.ts:86`). A planner cannot reassign planning to another peer. The write is a `planner changed` entry (reason `named`), which neither moves nor settles the task, so it never counts as supersession of an owner delivery (tasks.md assignment rule). `hub_task_assign` with role `planner` from a peer is refused pre-effect.
|
|
4412
4441
|
- **Release planner.** `newRelease` records `by` as the release's planner (`null` for the person); cut/configure/assign check it like a task's.
|
|
4413
4442
|
- **Migration.** No `planner` key and one planner holder: rotation always yields it. A single pinned planner: same. Old rows and old releases: `null`, legacy authority. Tests that assert a planner tool succeeds for the only planner stay green unchanged.
|
|
@@ -4422,21 +4451,22 @@ Declaration: `"claude-impl": { "kind": "claude", "roles": ["implementer"], "star
|
|
|
4422
4451
|
- first turn of a task: `--session-id <new uuid>`, session persisted (no `--no-session-persistence`);
|
|
4423
4452
|
- later turns: `--resume <that task's uuid>`; never `--continue`, never `--fork-session`, never another task's id;
|
|
4424
4453
|
- 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`);
|
|
4454
|
+
- 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`;
|
|
4425
4455
|
- 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;
|
|
4426
4456
|
- 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.
|
|
4427
|
-
- **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`.
|
|
4428
|
-
- **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.
|
|
4457
|
+
- **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.
|
|
4458
|
+
- **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.
|
|
4429
4459
|
- **Permissions.**
|
|
4430
4460
|
- `ask`: the hook decides nothing; every write, edit and shell call reaches the prompt tool.
|
|
4431
4461
|
- 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.
|
|
4432
4462
|
- `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).
|
|
4433
4463
|
- 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).
|
|
4434
|
-
- **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
|
|
4464
|
+
- **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).
|
|
4435
4465
|
- **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.
|
|
4436
|
-
- **Hub down or refusing.** The prompt tool returns deny.
|
|
4437
|
-
- **Hook down.** Decides nothing (`facts-hook.ts:6`), so the call reaches the prompt tool
|
|
4466
|
+
- **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.
|
|
4467
|
+
- **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.
|
|
4438
4468
|
- **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.
|
|
4439
|
-
- **Delivery.** Ordinary implementer delivery (task and chat items). The #223 R2 review-only filter does not apply (it keys on roles exactly `["reviewer"]`).
|
|
4469
|
+
- **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"]`).
|
|
4440
4470
|
- **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).
|
|
4441
4471
|
- **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).
|
|
4442
4472
|
- **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).
|
|
@@ -4446,7 +4476,7 @@ Declaration: `"claude-impl": { "kind": "claude", "roles": ["implementer"], "star
|
|
|
4446
4476
|
Declaration: `"claude-impl-t": { "kind": "claude", "roles": ["implementer"], "worktree": ".worktrees/claude-impl-t" }`.
|
|
4447
4477
|
|
|
4448
4478
|
- `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`).
|
|
4449
|
-
- The channel's hello gains an optional `cwd` (realPath of the MCP server's `process.cwd()`); additive, no `PROTOCOL` bump.
|
|
4479
|
+
- 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.
|
|
4450
4480
|
- **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.
|
|
4451
4481
|
- 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.
|
|
4452
4482
|
- 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.
|
|
@@ -4480,7 +4510,7 @@ Additive optional fields, no `PROTOCOL` bump (daemon.md):
|
|
|
4480
4510
|
- **Planner WIP cap (#655).** Unchanged: `reservedHold`/`acceptanceCapacity` key on the reserved owner and `wipLimit(owner)` (`daemon.ts:1350`, default 1). Each new instance has its own `tasks.wip_limits.<id>`. Planner pull requires the card's effective planner (R3); `plannerRefusal` text stays ids and counts.
|
|
4481
4511
|
- **Budget pauses.** The Claude implementer shares the `claude` account's pauses and readings (I2). A pause holds new deliveries; a running child finishes its turn (not killed by the pause). Rotation skips paused planners at choice time only; a later pause does not move a task's planner. Reviewer pool behaviour is unchanged.
|
|
4482
4512
|
- **Always-ask (#636).** Not a dependency. When #636 lands, its classifier runs in the same PreToolUse hook; an `ask` from the hook in `-p` mode reaches `hub_permission_request`, which carries the class and applies the class budget. Since the headless variant has no `never-ask`, #636 only adds the class label there. #636's adapter table must list both new Claude forms; both have the hook, so neither is "always-ask not enforceable".
|
|
4483
|
-
- **Recovery.**
|
|
4513
|
+
- **Recovery.** Target feature `role_stages` retains its Claude implementer and Codex planner-only instance meaning. Slice 525-2 adds `task_planners` to `DAEMON_FEATURES` and `incompatibleRecoveryTarget` (daemon.md floor rule), required while a `planner` setting is a list or names an instance, or a declared instance holds planner, and while any task or release has a non-null `planner` or plan-review authorship markers remain in its receipt/history. A 0.15.0 daemon already advertises `role_stages` but ignores task/release planner bindings; recovery to it must refuse before any effect. The floor reads bounded SQL `EXISTS` evidence, not only config. Old board parsing tolerates the additive nullable columns without changing legacy null authority (verified with the published 0.15.0 Board parser).
|
|
4484
4514
|
- **Settings.** `planner` stays a guarded settled-holder key (`roleHolders`, `daemon.ts:4431-4435`); a list change goes through the same confirmation.
|
|
4485
4515
|
|
|
4486
4516
|
### Acceptance (from the issue, mapped)
|
|
@@ -4503,13 +4533,13 @@ Additive optional fields, no `PROTOCOL` bump (daemon.md):
|
|
|
4503
4533
|
- Planner walk, `Assignment.planner`/`plannerReason`, I-2, explain lines. No board write of the planner yet (result unused until 525-2).
|
|
4504
4534
|
- ACs: AC-2.
|
|
4505
4535
|
3. **525-2 Per-task planner.** Size L. Depends on 525-3.
|
|
4506
|
-
- Paths: `src/hub/board.ts` (columns, migration), `src/hub/tasks.ts` (`taskPlanner`, `planningAuthority(by, task)`, notices, I-4 in `recordPlanReview`, release planner), `src/hub/settled-roles.ts`, `src/hub/settings.ts`, `src/hub/daemon.ts` (loader, status, `
|
|
4536
|
+
- Paths: `src/hub/board.ts` (columns, migration), `src/hub/tasks.ts` (`taskPlanner`, `planningAuthority(by, task)`, notices, I-4 in `recordPlanReview`, release planner), `src/hub/settled-roles.ts`, `src/hub/settings.ts`, `src/hub/daemon.ts` (loader, status, `task_planners` floor for list/instance planner), `src/cli/main.ts`, `src/cli/help.ts`, verify parser/help parity; leave `docs/verified.json` stamps to the release PR, because this feature PR is squash-merged.
|
|
4507
4537
|
- ACs: AC-5.
|
|
4508
4538
|
4. **525-4 Headless Claude implementer.** Size L. Depends on 525-1 (and 525-2 only for the daemon test's planner).
|
|
4509
4539
|
- 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`.
|
|
4510
4540
|
- Includes the `--permission-prompt-tool` probe gate.
|
|
4511
4541
|
- ACs: AC-4, AC-3 (with 525-6 for `codex-plan` and #535 for `codex-review`).
|
|
4512
|
-
5. **525-5 Terminal Claude implementer.** Size M. Depends on 525-1.
|
|
4542
|
+
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.)
|
|
4513
4543
|
- 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.
|
|
4514
4544
|
- 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`.
|
|
4515
4545
|
6. **525-6 Codex planner and visibility.** Size M. Depends on 525-2, 525-3.
|
|
@@ -4518,6 +4548,11 @@ Additive optional fields, no `PROTOCOL` bump (daemon.md):
|
|
|
4518
4548
|
|
|
4519
4549
|
Spec-first: each slice's PR amends this section where its implementation differs, together with `docs/operations.md`.
|
|
4520
4550
|
|
|
4551
|
+
### Amendment: write-class owners and the configured ACP planner rule (#735)
|
|
4552
|
+
|
|
4553
|
+
- **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.
|
|
4554
|
+
- **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.
|
|
4555
|
+
|
|
4521
4556
|
## Amendment: typed host ports for the remaining native branches (#625 phase 6, slice 2+)
|
|
4522
4557
|
|
|
4523
4558
|
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.
|
|
@@ -4977,3 +5012,108 @@ Use `summarizeByTask`, `costMetrics` and bounded SDLC GitHub helpers. Bound file
|
|
|
4977
5012
|
- At most 40 symbols and 40 untouched names are listed, with the count of the rest. Advisory only: ripwire does not report deleted symbols.
|
|
4978
5013
|
- 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.
|
|
4979
5014
|
- Colours are design tokens only, and the group wraps at phone width with 44 px targets.
|
|
5015
|
+
|
|
5016
|
+
|
|
5017
|
+
## Amendment: daemon responsiveness (#719)
|
|
5018
|
+
|
|
5019
|
+
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.
|
|
5020
|
+
|
|
5021
|
+
## Amendment: terminal Claude execution-root admission (#525-5)
|
|
5022
|
+
|
|
5023
|
+
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.
|
|
5024
|
+
|
|
5025
|
+
## Amendment: planner walk and reviewer independence in `assign()` (#525-3)
|
|
5026
|
+
|
|
5027
|
+
Slice 525-3 is implemented, as the "Purity" bullet of the #525 amendment specifies; this records what exists and what does not yet.
|
|
5028
|
+
|
|
5029
|
+
- **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)`.
|
|
5030
|
+
- **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.
|
|
5031
|
+
- **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.
|
|
5032
|
+
- **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.
|
|
5033
|
+
- **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.
|
|
5034
|
+
|
|
5035
|
+
## Amendment: deliverable records bound to content hashes (#509a)
|
|
5036
|
+
|
|
5037
|
+
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.
|
|
5038
|
+
|
|
5039
|
+
- **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.
|
|
5040
|
+
- **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.
|
|
5041
|
+
- **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.
|
|
5042
|
+
- **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.
|
|
5043
|
+
- **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.
|
|
5044
|
+
- **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.
|
|
5045
|
+
- **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`.
|
|
5046
|
+
- **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.
|
|
5047
|
+
- **Not here.** `ahub deliverables` and the dashboard marker (#509 AC3) are the next slice (done in the #509b amendment below). `PROTOCOL` is unchanged: the new tool fields are optional. Known limit: tool input schemas are not enforced on the way in, and a daemon from before this slice reads only `summary` and `refs` from `hub_task_done`, so a newer plugin talking to an older daemon has its `deliverables` silently dropped and the task is recorded without them. The plugin is installed apart from the daemon (docs/agent-notes/daemon.md), so after an upgrade restart the hub before relying on deliverables; a floor like `role_stages` is the upgrade path if that ever matters.
|
|
5048
|
+
|
|
5049
|
+
## Amendment: `ahub deliverables` and the dashboard badge (#509b, #317)
|
|
5050
|
+
|
|
5051
|
+
The second slice of #509 (AC3): a person-facing list of what tasks delivered and what a reviewer approved, and a dashboard marker. Nothing new is stored: both read the records 509a already writes.
|
|
5052
|
+
|
|
5053
|
+
- **Command.** `ahub deliverables [--task <id>] [--approved] [--check] [--json]` is the person's (`identity.ts` classifies it `console`; the daemon's `deliverables` task op sits behind the console gate, so an agent's tools connection is refused; `liveTestRefusal` refuses the delegated form). Each row is a task that has delivered files, newest task first: `#id title [delivered|approved]`, then each file's id, path (only where `publicDeliverables` allows), format, version, size and full sha256. A PII task or a `pii.paths` file shows id, hash and size only, and the title is `publicTitle`. `--task` takes one positive integer (a repeated `--task` is refused rather than keeping the last); `--approved` lists only tasks whose approval bound records, and shows those approved bytes.
|
|
5054
|
+
- **Measured or not.** Without `--check` no file is opened: the list is the board's records, `checked` is false and no row says anything about change. With `--check` every listed file is measured now through `Tasks.deliverableStatus` (the 509a reader, with its limits and its `unknown` for a file the hub cannot vouch for), each file shows `unchanged`, `changed`, `missing` or `unknown`, and the row says `changed since review` only for a measured difference. The row's own reading (`result`) follows the same order: it says `(unchanged)` only when every file did. Consumers of `--json` read `result`: `changedSinceReview` stays false next to `result: "missing"` or `"unknown"`, because it is true only for a measured difference. `Tasks.deliverableList` is the one reader.
|
|
5055
|
+
- **Dashboard.** The snapshot's task rows gain an optional `deliverables: {state, files, checked?}` (`Tasks.deliverableBadge`): the same public projection of the same records, never a file read, because the page polls every second and hashing up to 512 MiB per task on the poll is not acceptable. `checked: {at, result}` appears only when a live read (`task_show`, `ahub deliverables --check`) measured exactly these records; `result` is `changed`, `missing`, `unknown` or `unchanged` for the whole task, and `unchanged` only when every file measured unchanged (a change outranks a missing file, which outranks one the hub could not read, such as every file under an unverified execution root). The page and the CLI say `a file is missing` or `could not be measured` for those, never `unchanged`; `Tasks.deliverableResult` is the one reducer (the cache is keyed by the record ids and hashes, so a redelivery or a new approval clears it). The card shows a `Deliverables n` chip (with `approved` and `changed` when known); the task sheet gains a Deliverables group with each file's path (or `private file`), format, version, size and a twelve-character hash prefix, and says `not measured now: run ahub deliverables --check` when nothing was measured. `PROTOCOL` is unchanged: the field is optional and an older page ignores it.
|
|
5056
|
+
- **Interpretation.** The issue says "a dashboard column". The dashboard's board is kanban columns by task state with no per-task table, so a new column of its own has no place; the chip on the card and the group in the task sheet are the equivalent surface. Say so in review if a table was meant.
|
|
5057
|
+
- **Ceilings.** The dashboard's `changed` is as fresh as the last live read in this daemon's memory (a restart clears it, and a file edited after the last read shows nothing until the next); the command, not the page, is the way to measure now. The list reads the whole board each call (as `ahub board` does); there is no pagination.
|
|
5058
|
+
|
|
5059
|
+
## Amendment: window kinds, source precedence and per-peer budget notes (#285 phase 4)
|
|
5060
|
+
|
|
5061
|
+
- **Kind.** `UsageWindow` gains an optional `kind: "native" | "count" | "manual"` next to `source` (`src/hub/window-kind.ts`). `windowKind` is the one classifier: a named kind wins; an untagged window is `count` when its id is `tokens`, `manual` when its source is `ahub budget set`, else `native`, so a reader that predates the field keeps working. The Kimi count and `ahub budget set` name their kind; the Claude and Codex readers stay untagged and read as native. `PROTOCOL` is unchanged: `kind`, `superseded` and the reply's `peers` and `kimiSource` are optional fields an older client ignores.
|
|
5062
|
+
- **Storage and precedence.** `Budget` keeps readings per window id and kind, so a person's number and the provider's for one window never overwrite each other (and `quota_reset` baselines are per kind). `Budget.gating` decides what the gate and `headroom()` act on: a fresh manual reading replaces the other kinds for its window id; while any native or manual reading is fresh, the count is information only. A stale or reset reading leaves `fresh`, so the count gates again when nothing better is left. `status()` marks such a window `superseded`. A hard provider refusal clears the person's number for its window. `forget(peer, id, kind?)` lets the Kimi count drop only its own reading.
|
|
5063
|
+
- **Notes.** `src/hub/budget-notes.ts` (pure) builds one line per attached peer that shows no number, from the definition and counters, never from a log: `budget.noQuotaReason` (Pi, local) gives `no quota: ...` plus the hub's own 5 h token count for that peer (a `TokenWindow` fed by `addTokens` and seeded from `tokens` events at startup, information only); a peer with a source that has not reported says which; Kimi says by its count state (`KimiUsage.status()`: off, waiting, counting, unreadable with a reason code). The `budget` control reply carries `peers` (the notes) and `kimiSource`; the dashboard snapshot adds `note` and `noteKind` per peer.
|
|
5064
|
+
- **Surfaces.** `ahub budget` prints KIND, SOURCE and MEASURED per window, `info only` for a superseded count, and each note; the console's Peers detail and Usage detail and the dashboard's Usage budgets row show kind and source. The dashboard keeps its own copy of `windowKind` (`windowKindOf`, the page imports nothing); `test/ui.test.ts` runs it against the source. `ahub doctor` adds `kimi usage source` (Hub section) from `kimiSource`; an older hub without it adds no row.
|
|
5065
|
+
- **Not here.** The OAuth probe for Kimi's plan quota (AC9) stays undecided, and the live checks (AC11) stay open.
|
|
5066
|
+
|
|
5067
|
+
## Amendment: amem runtime (agent-mem#13 P4)
|
|
5068
|
+
|
|
5069
|
+
The hub uses exact-pinned @staix/agent-mem 0.7.0 for runtime recall/capture, with no HTTP to the claude-mem worker. The canonical alias-resolved project and selected-profile fence govern every operation. Curated notes remain the evidence for ask and briefs; raw deterministic capture and pending candidates do not become evidence. Explicit task notes use the native store once; auxiliary checkpoint/model answers are candidates. Legacy worker project labels and numeric observation ids are replaced by canonical keys and authentic amem entry ids. Recent recall is curated short-term data, not model-generated tool summaries. Claude-mem is import-only and off by default. Host PII/private-turn/path screening is retained. The supplemental curated view's 4096-row ceiling refuses an oversized view; policy-aware pagination is its upgrade path.
|
|
5070
|
+
|
|
5071
|
+
## Amendment: backlog issue planning and phase grooming (#759)
|
|
5072
|
+
|
|
5073
|
+
- Duplicate candidates exclude two tasks related by directed `deps`/`after` reachability in either direction. No follow-up field or inference from titles, notes or `supersededBy` is introduced. Overlap findings remain unchanged. A bounded public ID/dependency context may include approved intermediate phases; missing/private context is never read as an invented edge.
|
|
5074
|
+
- `hub_backlog_plan` accepts an omitted `fields` or `{}` with the existing bounded reason. It records a note-only `planned` revision with an empty field list; authority, stale revision, pins and frozen-release guards remain.
|
|
5075
|
+
- `fields.sdlc` accepts only `repo`/`issue` on an unstarted, ownerless backlog card, by its effective planner or the person. Null identity members restore a previous absence during undo. Other lifecycle/evidence fields remain unavailable through planning, and ordinary `hub_sdlc_record` still refuses backlog cards.
|
|
5076
|
+
- Issue relinks use the same identity invalidation as `sdlcReceipt`: old spec acceptance and identity-bound evidence cannot survive a different identity. Existing stage is preserved, or the host's initial stage is used when the card had no receipt. The atomic planning write records the new revision and only previous identity members; undo cannot revive an old acceptance receipt. Authority and task/release/privacy state are rechecked after screening.
|
|
5077
|
+
- CLI `ahub backlog plan ... <fields-json> <reason>` uses `{}` for a note and nested `sdlc` for an issue relink. Tool descriptions expose the same contract. Actual cards #138 and #287 are relinked to #525 and #754 only after landing and availability in the running hub, through an authorized planner/person identity.
|
|
5078
|
+
|
|
5079
|
+
## Amendment: the planner board-changed notice (#758)
|
|
5080
|
+
|
|
5081
|
+
The notice "The board changed. Before offering or accepting build work, read `hub_planner_groom` ..." went to every attached planner at every board status revision, including revisions the planner wrote itself, and queued a new copy each time. One planner session received it 12 times in 44 minutes. With the lifecycle on, `flushPlannerNotices` now reads what was added to each card's history since its last scan and decides per planner:
|
|
5082
|
+
|
|
5083
|
+
- **Author skipped.** A history entry whose `by` is the planner raises nothing for that planner.
|
|
5084
|
+
- **Scoped.** Only the card's planners are told (`taskPlanner`, #525-2): a card bound to one planner does not reach another.
|
|
5085
|
+
- **Task weight only when a plan review is needed.** A history entry that carries `planning` (a planning revision or dependency change), a card's creation (a peer's propose, with or without `after`, a split child, a generated card: new work and its dependencies are what a review consolidates) and any change of a card's `deps` since the last scan, whatever wrote it, is a `task` notice. An earlier copy that is still queued for that planner (never attempted or delivered) is withdrawn and replaced, so there is at most one unsettled notice per planner and it names the latest board revision. A settled notice is not replaced.
|
|
5086
|
+
- **The hub's own bookkeeping tells nobody.** An entry by `hub` without `planning` (lifecycle links, idle sweeps, holds, plan observations) neither raises a notice nor spends the `fyi` window.
|
|
5087
|
+
- **`fyi` weight otherwise.** A state move, review or assignment is recorded as an `fyi` task notice (console and log, no turn), at most once per planner per ten minutes (`GROOM_FYI_WINDOW_MS`, bounded by the injected clock `TasksDeps.now`).
|
|
5088
|
+
- **Restart and offline.** The first notice a planner gets after a hub start is task weight, as before. Changes made while a configured planner was offline stay pending for it.
|
|
5089
|
+
|
|
5090
|
+
Ceiling: a scan lists the board once per status revision (at most one per flush); an indexed history read is the upgrade. `PROTOCOL` is unchanged.
|
|
5091
|
+
|
|
5092
|
+
## Amendment: canonical memory symbol anchors (#684 phase 3)
|
|
5093
|
+
|
|
5094
|
+
- Normalize symbol signatures with the phase 1b normalizer and preserve explicit file qualification in the existing `symbol:<qualified>@<path>` format. No amem operation, schema or code index is added.
|
|
5095
|
+
- "Link that task's memory entries" means the new completion note carries screened, current-head, actually changed symbols with positive unambiguous code-facts `exists` evidence. Earlier notes remain immutable and connect through their shared reset-safe task node while the existing short-term completion finding is live. `related(... [symbol] ...)` reaches an earlier note at memory hop two; the package graph's literal two-edge cap reaches the task node. No read-time overlay is part of this phase.
|
|
5096
|
+
- Non-completion notes retain their existing normalized plan-intent associations. Completion notes with unknown observations write no symbols and say unknown, never the plan's guesses. At most 16 candidates are checked; no new check starts after a two-second scheduling allowance, and an in-flight check retains the existing code-facts execution/cleanup bounds. Recheck current privacy, repository/head provenance and the admitted completion observation before writing. PII tasks query and store nothing.
|
|
5097
|
+
- `ahub doctor --stale-anchors [--json]` is read-only, bounded and advisory. Only positive absence at the reported current head means stale. Private, ambiguous, unverified and exhausted evidence stays unknown with explicit coverage/truncation. It never mutates notes or grants authority.
|
|
5098
|
+
|
|
5099
|
+
## Amendment: phase 3 follow-up query and cleanup bounds (#684)
|
|
5100
|
+
|
|
5101
|
+
- Reuse a filled exact-tree symbol-preparation key across prepare-only and completion. Within a pass, memoize unknown and known exists results by query name; preserve privacy/provenance rechecks and optional fail-open results.
|
|
5102
|
+
- Query explicit raw and normalized symbol forms together for historical recall; never copy, supersede or rewrite old notes.
|
|
5103
|
+
- Unsupported `#`, `->` and `:` name forms remain unknown. Final absence rows are labelled "absent from indexed definitions", retaining existing state/reason compatibility; no imported-package existence claim is inferred.
|
|
5104
|
+
- The doctor may use transient detached worktrees. Interrupted cleanup can leave prunable registrations; no automatic prune is added. Repeated owned cleanup failure remains neutral and borrowed handles retain caller ownership.
|
|
5105
|
+
|
|
5106
|
+
## Amendment: memory code-neighbour projection (#684 phase 4)
|
|
5107
|
+
|
|
5108
|
+
- Optional caller/callee edges are computed at read time through code-facts, one hop only, and stamped with the exact current HEAD. Require a unique path-qualified seed; hide ambiguous and name-only edges. Store no code structure or edges in amem.
|
|
5109
|
+
- Dashboard Memory's default is the stored graph. An explicit Code neighbours toggle adds dashed calls and highlights attached notes, fails first; the accessible edge list includes direction and SHA. Prepare asynchronously outside the synchronous snapshot budget, then recheck current policy/profile/HEAD.
|
|
5110
|
+
- Symbol-start `hub_memory_graph` merges the overlay inside the existing node/edge and 6144-byte response caps. Code coverage and truncation are explicit, including unknown evidence. Authored about/rules_out associations remain distinct from derived calls.
|
|
5111
|
+
- Preparation bounds: four seeds, 32 distinct neighbours, 64 call edges, 16 screened notes and a two-second query-start allowance; existing in-flight code-facts timeouts remain. PII tasks/turns query nothing, and names pass publicMetadataText before disclosure.
|
|
5112
|
+
- Native task briefs may prepare asynchronously, merge direct and neighbour-attached fails/decisions first, and preserve their current combined item/line cap, deduplication and fail-open policy. Snapshot the item cap once before selection; reloads apply to the next brief. The console detail reports the bounded selected-note count, not a complete inventory.
|
|
5113
|
+
|
|
5114
|
+
### Round-1 optional-read scheduling bounds
|
|
5115
|
+
|
|
5116
|
+
- Keep screened direct task advice independent of overlay eligibility and validity. Board movement, foreign scope or a private owner turn may drop optional code context but must not drop the existing direct brief. PII tasks still make no optional query.
|
|
5117
|
+
- Default CodeFacts review/completion queries have priority over optional neighbours. Interrupt an active optional CLI, await all existing owned cleanup and then admit priority work; do not run native tree preparations concurrently. Optional unknowns have a bounded five-second TTL and never poison priority cache lookups.
|
|
5118
|
+
- Dashboard code work requires the Memory view to be open and visible. Derive starts share a five-second floor; known topology lives at most thirty seconds, with current note rows refreshed before publication. A single hardened native HEAD probe establishes a bounded HEAD/ref proof; cache checks do not launch Git.
|
|
5119
|
+
- Console task counts use cache-only optional context and return unknown on a miss within the existing task-detail deadline. Snapshot the native brief cap before awaits. Preserve stored nodes when adding derived edges and apply tier/kind/peer filters to neighbour notes; a non-symbol selection resets the toggle.
|
|
@@ -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.
|