@staix/agent-hub 0.12.45 → 0.14.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 +31 -0
- package/README.md +85 -116
- package/docs/agent-notes/README.md +6 -0
- package/docs/agent-notes/adapters.md +10 -7
- package/docs/agent-notes/budget.md +7 -1
- package/docs/agent-notes/bus.md +6 -4
- package/docs/agent-notes/daemon.md +14 -2
- package/docs/agent-notes/local-worker.md +2 -1
- package/docs/agent-notes/tasks.md +16 -3
- package/docs/agent-notes/tests.md +5 -3
- package/docs/dogfood.md +95 -0
- package/docs/events.md +14 -1
- package/docs/operations.md +290 -27
- package/docs/profiles.md +2 -2
- package/docs/quickstart.md +13 -3
- package/docs/sdlc.md +59 -8
- package/docs/security.md +26 -13
- package/docs/smoke.md +9 -4
- package/docs/specs/2026-09-19-agent-hub-design.md +797 -8
- package/docs/verification/2026-10-19-reviewer-isolation.md +2 -0
- package/docs/verification/2026-10-22-code-facts.md +78 -0
- package/docs/verification/2026-10-22-codex-reviewer-isolation.md +61 -0
- package/docs/verification/2026-10-22-kimi-qwen-compat.md +79 -0
- package/docs/verification/2026-10-23-0.13.0.md +89 -0
- package/docs/verification/2026-10-23-0.14.0.md +69 -0
- package/docs/verification/dogfood/2026-10-22.md +3109 -0
- package/docs/verified.json +22 -17
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +702 -347
- package/src/adapters/agy/entry.ts +5 -4
- package/src/adapters/claude-channel.ts +20 -9
- package/src/adapters/codex-appserver.ts +18 -2
- package/src/adapters/codex-review-isolation.ts +29 -11
- package/src/adapters/compat.ts +140 -0
- package/src/adapters/definitions/builtin-metadata.ts +3 -2
- package/src/adapters/definitions/native-cli-doctor.ts +12 -5
- package/src/adapters/definitions/native-cli-entry-codex.ts +13 -0
- package/src/adapters/definitions/native-cli-entry-jcode.ts +9 -6
- package/src/adapters/definitions/native-cli-jcode-mcp.ts +9 -2
- package/src/adapters/definitions/native-cli-preview-common.ts +2 -0
- package/src/adapters/definitions/native-cli-preview-jcode.ts +8 -2
- package/src/adapters/definitions/native-cli-preview-qwen.ts +2 -1
- package/src/adapters/definitions/qwen-native.ts +7 -3
- package/src/adapters/definitions/qwen.ts +2 -1
- package/src/adapters/jcode.ts +72 -31
- package/src/adapters/kimi-review.ts +6 -3
- package/src/adapters/registry-types.ts +3 -1
- package/src/adapters/registry.ts +1 -0
- package/src/adapters/reviewer-print.ts +34 -7
- package/src/cli/agents.ts +40 -0
- package/src/cli/console-review.ts +54 -0
- package/src/cli/console-state.ts +124 -29
- package/src/cli/console-symbols.ts +74 -0
- package/src/cli/console-usage.ts +89 -0
- package/src/cli/console.ts +26 -1
- package/src/cli/help.ts +17 -6
- package/src/cli/identity.ts +29 -6
- package/src/cli/init.ts +109 -11
- package/src/cli/main.ts +127 -11
- package/src/cli/output.ts +91 -3
- package/src/cli/preview.ts +1 -1
- package/src/cli/sdlc-release-guard.ts +82 -0
- package/src/cli/status-lines.ts +1 -1
- package/src/hub/action-class.ts +362 -0
- package/src/hub/action-policy.ts +106 -0
- package/src/hub/board.ts +65 -9
- package/src/hub/budget.ts +71 -2
- package/src/hub/bus.ts +43 -3
- package/src/hub/checks.ts +80 -31
- package/src/hub/code-facts.ts +223 -0
- package/src/hub/config-apply.ts +10 -1
- package/src/hub/config-trust.ts +7 -4
- package/src/hub/daemon.ts +319 -34
- package/src/hub/dogfood-read.ts +199 -0
- package/src/hub/dogfood.ts +163 -0
- package/src/hub/envelope.ts +1 -1
- package/src/hub/events.ts +11 -2
- package/src/hub/hub-tools.ts +26 -3
- package/src/hub/inbox-polling.ts +25 -0
- package/src/hub/instances.ts +87 -5
- package/src/hub/manager.ts +2 -2
- package/src/hub/profiles.ts +7 -2
- package/src/hub/release-worktree.ts +56 -0
- package/src/hub/review-brief.ts +36 -0
- package/src/hub/sdlc-metrics.ts +260 -0
- package/src/hub/sdlc-queue.ts +31 -9
- package/src/hub/sdlc-release-authorization.ts +173 -0
- package/src/hub/sdlc.ts +54 -14
- package/src/hub/settings.ts +5 -2
- package/src/hub/snapshots.ts +93 -8
- package/src/hub/spec-read.ts +31 -0
- package/src/hub/symbol-normalizer.ts +26 -0
- package/src/hub/task-code-group.ts +52 -0
- package/src/hub/task-code-symbols.ts +90 -0
- package/src/hub/task-sweep.ts +3 -2
- package/src/hub/tasks.ts +455 -67
- package/src/hub/test-integrity.ts +162 -0
- package/src/hub/ui.ts +3 -0
- package/src/hub/usage-rollup.ts +419 -0
- package/src/local/sandbox.ts +5 -3
- package/src/ui/index.html +274 -2
- package/templates/AGENTS.block.md +1 -1
- package/templates/profiles/dev.json +6 -1
- package/templates/profiles/general.json +6 -1
- package/templates/profiles/research.json +6 -1
- package/templates/profiles/writing.json +6 -1
- package/templates/release-guard.block.md +6 -0
|
@@ -918,6 +918,15 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
|
|
|
918
918
|
- Live benign Kimi/Claude results and unverified Codex/Pi limits are recorded
|
|
919
919
|
in docs/smoke.md. No extra account prompts accompany these review corrections.
|
|
920
920
|
|
|
921
|
+
## Always-ask action foundation (#636 slice 1)
|
|
922
|
+
|
|
923
|
+
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
|
+
|
|
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 currently adopts the untracked local policy without a retained confirmation (ponytail: bind weakened startup policy to person confirmation or surface attention before adapter enforcement in #312/#313). Reload never imports code or changes a sandbox.
|
|
926
|
+
- `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
|
+
- 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
|
+
- 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
|
+
|
|
921
930
|
## Amendment: approval notifications and timeout (issue #5)
|
|
922
931
|
|
|
923
932
|
- A waiting permission request raises one macOS notification (`osascript`) with
|
|
@@ -968,8 +977,12 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
|
|
|
968
977
|
starts. A check ends when its command exits: what it left in its process group
|
|
969
978
|
is killed then, and a descendant that left the group gets 0.5 s more to flush
|
|
970
979
|
output and is not waited for. A check killed at the timeout or on shutdown
|
|
971
|
-
settles 1 s after the kill even if its exit and
|
|
972
|
-
(#558).
|
|
980
|
+
settles 1 s after the kill even if its exit and pipes never report
|
|
981
|
+
(#558). The runner starts the command with `Bun.spawn` and reads its exit and both
|
|
982
|
+
pipes as values, not `node:child_process` events: under `bun test` on a loaded
|
|
983
|
+
macOS host the node layer can reap a quick child inside `spawn()` and emit its exit
|
|
984
|
+
and close before any listener exists (oven-sh/bun#45483), which made a trivial
|
|
985
|
+
check report a timeout and an empty tail after its limit plus the grace. Exit 0: the task
|
|
973
986
|
completes as before, the review envelope carrying `Check: <command> -> exit 0`
|
|
974
987
|
and the output tail; shared memory gets the summary and the outcome line, never
|
|
975
988
|
the output. Any other outcome: the task stays with its owner, who gets a task
|
|
@@ -1162,6 +1175,22 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
|
|
|
1162
1175
|
failed dependent reads, ready writes or assignments do not escape a saved
|
|
1163
1176
|
approval. Their notices are best effort because notification can read the
|
|
1164
1177
|
same unavailable board. An ownerless task stays eligible once a peer can take it.
|
|
1178
|
+
- Readiness respects the reserved owner's planner WIP cap (#655). A ready task is
|
|
1179
|
+
handed to its reserved owner only while that owner's `wip()` (plus claims in flight)
|
|
1180
|
+
is under `wipLimit()`, the count `accept` refuses at. Otherwise the task keeps its
|
|
1181
|
+
reservation, stays ready in To Do (no owner, no offer, no stall) and the hub records
|
|
1182
|
+
one quiet `ready held` history entry naming the owner and its count; later passes add
|
|
1183
|
+
no second entry. Dependents are offered in priority (none is P2, `urgent` is P0), then
|
|
1184
|
+
rank, then id order, so a free slot goes to the first of them. When an owner's slot
|
|
1185
|
+
frees (an approval of its task, a cancellation or an untouched return to backlog), its
|
|
1186
|
+
held ready tasks are offered in that order and the cap stops the offers; the 60 s
|
|
1187
|
+
release timer does the same for a slot freed any other way. Planner-pulled work was
|
|
1188
|
+
admitted by its pull and is not held, a reservation the task excludes is not
|
|
1189
|
+
honoured by routing and holds nothing, and the idle sweep's automatic reassignment
|
|
1190
|
+
never overrides a hold. An offer takes its reserved owner's slot in the same
|
|
1191
|
+
synchronous step as the cap check, before assignment can yield, so concurrent
|
|
1192
|
+
offers cannot exceed the cap; the release timer also offers a held task whose
|
|
1193
|
+
latest move is a planner edit.
|
|
1165
1194
|
- A recovery commit also waits for task operations in flight and for completion
|
|
1166
1195
|
checks queued or running: both can write the board after its integrity digest
|
|
1167
1196
|
(a check the commit's stop kills records `check interrupted`).
|
|
@@ -1336,7 +1365,7 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
|
|
|
1336
1365
|
- `hub_delivery_retry { delivery_id }` lets an authenticated recipient retry only its own current `needs_review` row. This option is offered only to transports with an independent tool turn (Claude channel/pull, Codex and Pi). The delivery-only local worker and headless ACP peers cannot start a tool turn through their own hold, so they do not advertise this tool and the daemon refuses a manual call before mutation. Their held hints ask the person to resolve the row; ordinary messages never bypass the hold. Kimi TUI remains pull-only and keeps the recipient option. An ephemeral delivery-only MCP marker controls exposure and hint copy only; the current adapter class is the authority. There is no action or reason input: `completed` and `discard` remain person-only. Any explicit retry in that recipient's lineage, including a person's retry, consumes this allowance; a redelivery held again stays for the person. The existing resolution transaction records `peer:<id>` and a fixed reason, keeps original envelopes unchanged and requeues them through the existing put-back path. A busy recipient may consent to this queue-only operation, but delivery waits for idle and all pause/recovery and active-delivery fences remain. The rendered clone reserves the existing journal body cap for the warning, clipping only the clone at a Unicode character boundary with an explicit cut marker when needed; originals remain unchanged. Jcode project MCP entries carry the same delivery-only exposure marker as other headless ACP peers, preserving their own configuration and peer identity rules. Final rendered output, after condensation and on pull or steer, starts with the fixed sanitized header `Hub redelivery warning: An earlier attempt of this delivery is uncertain; check for its effects before acting.` Explicit retry history is scoped by recipient and original envelope ids. A separate monotone consumed-proof projection records one proof id and warning provenance per original, leaving resolution audit rows unchanged. Explicit resolution and coalesced handoff inherit that proof atomically for every grouped non-context original; a rejected checkpoint consumes no fresh allowance. Hub presence originals carry mutable recall/shared context whose id survives merging, so their ids never participate in retry proof lookup, inheritance, warnings or unknown marking. A preface shared with earlier work cannot consume an unrelated new message's retry allowance; actual task/chat originals retain their consumed lineage. Safe-failure requeue, withdrawal of an earlier original, terminal-state changes, pruning, restart or older-writer audit replacement cannot renew the allowance. Older attempt-bearing rows backfill this projection on return. Recovery snapshots without history and unmappable pruned pre-feature retry records mark only their affected recipient/original ids unknown: those lineages require a person, while unrelated new messages keep their allowance. Tool results expose held metadata only (total count, ids, senders, kinds and age), at most 32 rows/4 KiB with explicit truncation; no body or task text enters the hint. These hints work independently of channel mode and do not wait behind the held queue (#434).
|
|
1337
1366
|
- Local attach and idle model/route replacement require a reachable authenticated gateway, served models and successful minimal availability calls. Busy workers refuse replacement; validation failure leaves the current worker intact. Controlled recovery can reconstruct a manually paused local worker without gateway availability, but manual resume validates its restored choice before lifting the pause. Doctor checks the fixed model against the inventory.
|
|
1338
1367
|
- Reviewer routing appends attached peers holding `roles.reviewer` after the review class's candidates. Without a reviewer, task results and console notices explain direct approval and the skipped candidates.
|
|
1339
|
-
- Conflict detection includes `in_review` work. Completed overlapping snapshots that share files compute one concurrent-edit change set across the completed overlapping turns and publish at most one conditional notice per affected task, containing both turn ids and attributing the changes to neither peer. Per-task-pair events remain research evidence, while notice persistence grows linearly in the affected tasks (#576). A bounded, per-worktree HEAD reflog cursor classifies moves appended during the turn. Incoming checkout, merge, pull and proven fast-forward reset paths are excluded from attributed changes and undo paths; normal peer commit, amend and cherry-pick paths remain attributed, as do sequencer pick/reword/edit/squash/fixup/continue/finish paths under native command prefixes. The start checkout of rebase or pull --rebase
|
|
1368
|
+
- Conflict detection includes `in_review` work. Completed overlapping snapshots that share files compute one concurrent-edit change set across the completed overlapping turns and publish at most one conditional notice per affected task, containing both turn ids and attributing the changes to neither peer. Per-task-pair events remain research evidence, while notice persistence grows linearly in the affected tasks (#576). A bounded, per-worktree HEAD reflog cursor classifies moves appended during the turn. Incoming checkout, merge, pull and proven fast-forward reset paths are excluded from attributed changes and undo paths; normal peer commit, amend and cherry-pick paths remain attributed, as do sequencer pick/reword/edit/squash/fixup/continue/finish paths under native command prefixes. The start checkout of rebase or pull --rebase classifies incoming paths from `merge-base(old, upstream)..upstream` instead of `old..upstream`, so replayed own commits do not overlap and true upstream paths are excluded (#612); any remaining path overlap or ambiguity stays unknown, never attributed as upstream work. Fast-forward reset stays incoming even if its descendant target contains commits made by the peer in another worktree during the turn, safely excluding external commits from local turn attribution and undo (#612). Paths still dirty between end HEAD and the end workspace snapshot are never excluded (including autostash and checkout -m). Independent working-tree edits remain attributed. Missing, rotated, truncated, unrecognized or ambiguous move evidence is unknown, never an exclusion; paths shared by own and incoming moves are ambiguous. A HEAD change during snapshot capture makes that capture unknown. Status probes the checkout branch once per synchronous burst, then screens the cached raw branch through the current privacy policy for every rendering; the cache ends before the next control turn. Pi terminal reconciliation reads an owner observation maintained by the existing 500 ms monitor and expiring after 1 s; fresh signature checks remain mandatory at tool/effect admission and teardown. Private tasks and sensitive file names remain redacted.
|
|
1340
1369
|
- Pi assistant usage is forwarded through the authenticated bridge and recorded before settlement, without counting the same message both at `message_end` and `agent_end`.
|
|
1341
1370
|
- An idle peer with no open owner/reviewer task and no queued or active delivery skips the quota checkpoint turn; the pause proceeds immediately and the log records the skip.
|
|
1342
1371
|
- Control protocol 11 adds queue hold metadata; the current coordinator supports authenticated protocol 9, 10 and 11 recovery sources.
|
|
@@ -2576,6 +2605,17 @@ the queue.
|
|
|
2576
2605
|
- The private socket name ends in `.sock`: jcode names its debug socket by replacing that suffix. Every owned jcode process runs with `JCODE_NO_TELEMETRY=1` (#529).
|
|
2577
2606
|
- Native ACP load (verified on 0.94.0 and 0.98.1) lacks identity proof. Controlled recovery refuses before effects, crash recovery is manual and a load without an exact echoed id refuses before a prompt or restored claim. Real provider/tool/teardown evidence and comparative measurement remain unverified.
|
|
2578
2607
|
|
|
2608
|
+
## Amendment: extra Jcode instances (#685, #368)
|
|
2609
|
+
|
|
2610
|
+
- `instances.<id>` accepts `kind: "jcode"` only from the untracked `config.local.json`, up to 8 Jcode instances per project. Instance-only, machine-local fields: `jcode_home` (required: absolute, control-free, at most 4096 bytes), and optional `provider`, `model` and `provider_profile` (one bounded printable word each, passed as `-p`, `--model` and `--provider-profile` exactly as written). Any other field is refused. The definition declares `instances.nativeHome`; no other kind accepts these fields, and settings never edit them. `checkJcodeInstanceHomes` runs once at daemon startup and refuses a `jcode_home` equal to the base `jcode_home`, equal to another instance's, or nested inside or around either (their native stores would overlap). Homes are compared by the directory they name: resolved with `realPath` (symlinks and system aliases such as `/tmp` and `/private/tmp`, the nearest existing ancestor for a home not created yet), compared case-folded on a volume that folds case, and by device and inode when both exist. `instances.nativeHome` is a fixed-builtin capability: `assertNativeCapabilityScope` refuses it on any configured or external definition. `ahub jcode --as <id> --print-command` and `--dry-run` preview the same `-p`, `--model` and `--provider-profile` flags the daemon passes (values redacted).
|
|
2611
|
+
- The person authenticates inside Jcode against each home. The hub handles no model credential, key, token or provider URL, and there is no hub model relay for Jcode (the on-prem model is a provider profile the person configured). The start preflight is read-only: `jcode_home` must exist as a directory owned by the person (`jcodeHomeRefusal`), otherwise the start is refused with the instance id and no path. The hub's argv for an instance is the base Jcode argv plus its own three values, with its own `JCODE_HOME` and socket. It reads or writes nothing under `jcode_home`; only Jcode and the existing owned-state paths do. Usage is reported only if Jcode itself reports it, otherwise `unknown`, never 0.
|
|
2612
|
+
- Each instance is one `JcodePeer` under its own id: its private `jcode serve` and `jcode acp` pair (never standalone `jcode acp`, #559), socket, roster row, launch record and `--server-name agent-hub-<id>`. Its owned children run with `peerChildEnv(<id>)`, and `recoveryMetadata` names the id. `ahub jcode --as <id>` starts it headless; an instance has no TUI. Stopping it stops only its own group. An extra implementer needs the same managed worktree as every extra implementer (`resolveInstanceExecutionCwd`).
|
|
2613
|
+
- Hub tool identity (#368). The project's `.jcode/mcp.json` entry is shared by every Jcode in the project, and jcode gives an entry's own env precedence over the process env (probed on 0.99.0). The entry therefore names no `AGENTHUB_PEER_ID`; every owned launch path supplies its own (serve and ACP through `peerChildEnv`, the TUI launch env) with an `AGENTHUB_LAUNCH_ID` that `JcodePeer` generates per launch. The tools-mode MCP server fails closed: without `AGENTHUB_PEER_ID` it never connects and refuses every tool with a reason, and never defaults to `jcode` or any other id. The daemon binds the `tools` hello of a peer whose definition runs the owned ACP engine to its current owned launch: the hello must carry that launch id (`JcodePeer.ownsLaunch`, a launch that is stopping admits none), so a process that forges `AGENTHUB_PEER_ID` has no admission. An entry written before this still names `jcode`: `jcodeMcpReady` is false for it once after the upgrade, with the existing remedy text, until `ahub jcode` rewrites it.
|
|
2614
|
+
- Every instance stays a cloud peer (`trust.onPrem: false`): whatever provider it was configured with, its native policy stays unmanaged with no hub sandbox, and it never owns PII work. Status carries `modelSource` for an instance (the declared provider, model and profile names, or `native default`), shown in `ahub status` and the dashboard; `ahub doctor` lists the MCP servers each rostered instance's own home would load. None of them carries a URL, token, key or home path.
|
|
2615
|
+
- Evidence: on jcode 0.99.0 an MCP entry naming neither id gives its child `AGENTHUB_PEER_ID` and `AGENTHUB_LAUNCH_ID` from the owned process env (probed under an isolated home with a probe entry, not the hub bundle).
|
|
2616
|
+
- Evidence: the committed bundle ran as the MCP child of a real jcode 0.99.0 started by the daemon for an extra instance, with that instance's peer id and launch id in its env and one established connection to the hub (docs/smoke.md).
|
|
2617
|
+
- Ceiling: a real Jcode sign-in, provider and model, and a model turn that calls a hub tool, are not verified by source or fixtures (`docs/smoke.md`, "Extra Jcode instances").
|
|
2618
|
+
|
|
2579
2619
|
# ACP watch amendment, 2026-10-11 (#286)
|
|
2580
2620
|
|
|
2581
2621
|
- Headless Kimi/Qwen expose `watch` to console-role clients: bounded memory replay followed by activity pushes, no protocol bump, no peer input or approvals from the view. The CLI displays safe wrapped text; private/PII turns display a stub.
|
|
@@ -2655,11 +2695,11 @@ the queue.
|
|
|
2655
2695
|
|
|
2656
2696
|
# Per-project profile data amendment, 2026-10-12 (#259)
|
|
2657
2697
|
|
|
2658
|
-
- Pure `profile_overrides` resolution supplies per-project class maps, supported plan-key selection, flow-role guidance, catalog defaults, managed AGENTS content
|
|
2698
|
+
- Pure `profile_overrides` resolution supplies per-project class maps, supported plan-key selection, flow-role guidance, catalog defaults, managed AGENTS content, advisory example checks and review checklist lines (#507). Shipped defaults are not mutated. Map fields merge across shared/local layers; lists/text replace.
|
|
2659
2699
|
- 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.
|
|
2660
2700
|
- 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.
|
|
2661
2701
|
- 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.
|
|
2662
|
-
- 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. Native ingestion/retention/measurement remains unverified; configuration changes apply after daemon/MCP restart.
|
|
2702
|
+
- 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.
|
|
2663
2703
|
|
|
2664
2704
|
|
|
2665
2705
|
## Amendment: bounded data policy globs (#259)
|
|
@@ -3811,7 +3851,7 @@ Kimi 2.2.0 print stream-json does not expose token counts. Shared account quota/
|
|
|
3811
3851
|
|
|
3812
3852
|
ahub builds the review brief that the workspace gate used to take from `ocr delegate preview` and `rule`, and gives the same brief to every reviewer.
|
|
3813
3853
|
|
|
3814
|
-
- **Range.** The task's commit (`refs.commit`, resolved to its full SHA) against its merge base with the remote default branch (`origin/HEAD`, else `origin/main`, else `origin/master`). There is no fallback to the checkout's `HEAD`, which may be the head itself or another session's branch: without a default branch, or when the commit has no changes against it, the brief is unavailable. Any failed git read makes it unavailable too, never partial.
|
|
3854
|
+
- **Range.** The task's commit (`refs.commit`, resolved to its full SHA) against its merge base with the remote default branch (`origin/HEAD`, else `origin/main`, else `origin/master`). There is no fallback to the checkout's `HEAD`, which may be the head itself or another session's branch: without a default branch, or when the commit has no changes against it, the brief is unavailable. Any failed git read makes it unavailable too, never partial. the Git inventory is bounded (500 files, 8 rule files of at most 32 KB each); the brief never touches the board.
|
|
3815
3855
|
- **Files.** Each change comes with its status and line counts; a rename or copy lists both sides. A change touching a `local.deny` or `pii.paths` path, or a path that fails the private-data screen, is withheld whole (both sides of a move) and only counted. Excluded only when every side of a change is (a move keeps its source's obligation), with the reason, status and both sides shown: files marked `linguist-generated` in `.gitattributes`, lockfiles (a fixed list) and binary files. They are listed so a reviewer can still read them. Unlike `ocr delegate preview`, markdown and test files stay reviewable: the workspace gate required reading them anyway, and coverage now records it. Compared on PR #506 (head e305a17e against 3f258a41), OCR excluded 3 markdown and 3 test files and kept the generated bundle; ahub keeps those 6 and excludes the bundle once the `.gitattributes` this PR adds is on the default branch.
|
|
3816
3856
|
- **Rules.** `REVIEW.md` applies to every reviewable file. A path whose `.gitattributes` entry sets `ahub-review-rules=<repository path>` also gets that file, and a move gets the rule files of both sides. Files are grouped by the rule files that apply. Attributes and rule files are read at the merge base, not the head, and every diff the brief runs uses `git --attr-source=<merge base>`, so a checkout at the head cannot reclassify files either (for example `* -diff` making them binary): a change cannot lift its own review obligations (marking everything generated, weakening `REVIEW.md`); its own edits apply from the next change after it merges. A rule file's name and whole body pass the private-data screen before any cut; a file that fails is named but not shown. A configured rule file the brief cannot load (missing at the merge base, or beyond the 8-file cap) is listed with the reason; a private or non-nameable rule path is left out unnamed. Deviation from the issue: per-path settings are git attributes, versioned with the change, instead of new `review.exclude` and `review.rules` config keys.
|
|
3817
3857
|
- **Cross-check.** The plain `--name-only --no-renames` list of the same range is compared with the brief. A path it shows that the brief does not list is named for a direct read.
|
|
@@ -3819,6 +3859,31 @@ ahub builds the review brief that the workspace gate used to take from `ocr dele
|
|
|
3819
3859
|
- **Delivery.** `sendReview` appends the rendered brief to the review request (after #215's admission, body only). `hub_task_show` adds a `brief` field while the task is `in_review`. `ahub review brief <id>` prints it for a person. A task without a commit, or a private task, has no brief.
|
|
3820
3860
|
- **Coverage gate.** `hub_review` takes `coverage: { reviewed: [paths], skipped: [{ path, reason }] }`. An approval must account for every reviewable path. The gate follows the lifecycle mode (deviation from the issue, which refuses always): `enforce` refuses before any write, naming the paths, and also refuses when the brief is unavailable (an unknown inventory is never an empty one); `advise` records `Coverage gap (N): ...` or `Coverage unknown: ...` in the verdict note; `off` and a person's own verdict skip it. The commit under review is the task's commit, else the head the verdict names, else the head its lifecycle receipt records. Code work (a task with a commit, branch or PR) for which none of these is known, or whose brief cannot be built, has an unavailable brief. Work with no commit, branch or PR is exempt, with nothing to account for. So is a `Tasks` built without a brief provider (library use and tests; the daemon always provides one). One head throughout: the head the verdict names, or the `reviewedHead` it would inherit from an earlier review, must equal the brief's head (with a PR, the lifecycle approval gate already binds the reviewed head to the PR head). The task's commit and its lifecycle receipt (which holds the inherited `reviewedHead`) are pinned when coverage is checked and rechecked before every write after an await, so a commit or receipt that moves during the verdict is refused.
|
|
3821
3861
|
|
|
3862
|
+
|
|
3863
|
+
### Optional changed symbols (#682, #684 phase 1a)
|
|
3864
|
+
|
|
3865
|
+
`src/hub/code-facts.ts` is the sole runtime caller of optional ripwire. The initial verified CLI contract is 0.6.7: strict `ripwire.pr-context/v1` and `ripwire.at/v1` XML (these verbs do not support JSON), and validated callers/callees JSON keys. Unknown versions, schemas, malformed/capped output, tool failures and timeouts give `unknown`, never a passing gate. The rendered advice has a 16000-byte ceiling (oversize is unknown). Parse metadata values only; discard raw text, owners, author email and source fragments. Calls have a 5 s execution deadline and an owned-process cleanup bound; cache successful values by canonical repo, SHA and query (including the base for a diff). A busy reader gives new distinct queries unknown instead of making review wait in a queue. Name-only graph edges are marked ambiguous and hidden; graph count floors are retained.
|
|
3866
|
+
|
|
3867
|
+
The reader always creates a temporary detached worktree with no checkout, populates its index and materializes bounded raw regular-file Git blobs. It never runs ripwire in main or reuses a tracked-clean owner tree: untracked/ignored analysis inputs would contaminate SHA evidence. Disable native Git filters, hooks, external diff and fsmonitor too. The ceiling is 4096 regular files, 4 MiB/file and 32 MiB total; symlink/gitlink trees or unavailable cleanup stay unknown. Temporary worktree removal follows owned CLI teardown. No heavy-job slot or runtime dependency is added.
|
|
3868
|
+
|
|
3869
|
+
Prepare optional code facts asynchronously before the review request, under the existing task/head/owner/privacy admission fences. Failure never refuses the request. PII tasks perform no query; other tasks' names and paths pass `publicMetadataText` at display time. The synchronous file/rule brief and its coverage gate remain authoritative and unchanged.
|
|
3870
|
+
|
|
3871
|
+
**0.6.7 ceiling and amended acceptance:** `--pr-context` reports head symbols and omits deleted definitions; it has no stable both-tree identity or new/modified/removed marker. Show per-file observed name, kind and span with `change: "unknown"`. Label plan lists `changed and planned`, `changed, not planned`, and `planned, not seen as changed`, and state that ripwire 0.6.7 does not report deleted symbols. These are advisory observations, never evidence that a plan is satisfied or untouched. The upgrade is a ripwire both-tree inventory, or bounded base/head `--at` probes if real use shows that the advice is too noisy (person/planner ruling, 2026-10-22).
|
|
3872
|
+
|
|
3873
|
+
### Task/head symbol observations (#684 phase 2a)
|
|
3874
|
+
|
|
3875
|
+
The data slice may follow phase 1a independently of phase 1b's plan-name normalization. It stores one current, explicitly head-bound `codeSymbols` receipt per task, rather than a code index or historical source inventory. The receipt carries the full head and merge-base SHAs, an internal repository place key, known/unknown and observed-completeness flags, and at most 500 `{id,path,name,kind}` entries (64 KiB total; each string at most 512 bytes). IDs use `symbol:<name>@<path>`. The separate `task_code_symbols` table prevents an older daemon's `SELECT * FROM tasks` from exposing an unrecognized payload on recovery; the task update and payload write share one transaction. No spans, raw output, source fragments, plan text or model input are stored. The native 0.6.7 deleted-definition/count-floor ceiling above still applies: `complete` certifies that the observed envelope survived screening, never that every changed definition was found.
|
|
3876
|
+
|
|
3877
|
+
`Tasks` obtains the already prepared brief at an admitted `done` or `done (checking)` write and saves metadata in that same Board update, with a flow-revision fence. Optional preparation is bounded and fenced in every lifecycle mode, including completion without a reviewer; preparation-only adds no remote review gate. Refusals save no metadata. A failed or unavailable observation never reuses another head's data. A head, repository-ref/path change or PII transition drops the old receipt; PII tasks store nothing. The current brief analyzes a validated project execution root, so a foreign task repository remains unknown until independent repository provenance is carried by the brief.
|
|
3878
|
+
|
|
3879
|
+
Console task detail and the UI task snapshot carry the same public metadata. Every public read binds the current head and repository and re-screens IDs, paths, names and kinds through `publicMetadataText`; internal repository keys are omitted. Filtering marks the observed envelope incomplete. Status and rendering remain read-only. The formatted Code group and symbol-to-memory navigation are a later phase 2 presentation slice.
|
|
3880
|
+
|
|
3881
|
+
Completed-change notices and the console task detail (#379 follow-up) may say `same file, different symbols`: both tasks must have current, public, known and complete observed envelopes, and each exact shared path must have non-empty symbol sets with no intersection. Unknown, stale, filtered, ambiguous or empty evidence retains the ordinary notice. The wording explicitly says observed names only and that deleted definitions may be missing, and still asks the recipient to check the completed work. No notice is suppressed, no authority or coverage gate changes, and plan-time overlap/routing remain unchanged.
|
|
3882
|
+
|
|
3883
|
+
The read-only `task_show` reply also carries `symbolOverlaps: [{task,files,sameFileDifferentSymbols}]` (#379). It uses the existing open-task/other-owner overlap selection and the same `taskCodeSymbolsDisjoint` predicate with the current `publicMetadataText` screen as completion notices. Both sides exclude tasks marked `pii` or `pii_path`, even when routing locality is disabled; shared paths are screened before display. Unknown, incomplete, stale or filtered observations keep a plain overlap, never a different-symbol claim. No pair result is stored and no task mutation occurs on a read.
|
|
3884
|
+
|
|
3885
|
+
The console consumes this server verdict without computing a second rule from plans or receipts. Task detail replaces the raw pair field with an `overlaps` block beside its symbols block and the ceiling `observed names only, deleted definitions may be missing`. A missing field on an older hub shows no block; an empty server list says `none`; a private or non-task detail shows no block. Task IDs and shared paths use the existing quoted field renderer. At most 40 pairs and 40 paths per pair are listed, with omitted counts. Related-note counts remain phase 4.
|
|
3886
|
+
|
|
3822
3887
|
## Claude peer instances I3 amendment (#450)
|
|
3823
3888
|
|
|
3824
3889
|
- Superseded by A2b `instances.mode` and explicit role policy (MCP shape grants no instance admission): a declared instance of a channel-plugin kind (Claude) was admitted when its declaration names `roles`, reviewer and/or planner only. `start_mode` and `worktree` are refused: the instance starts only from the person's terminal with `ahub claude --as <id>`, so the hub never starts, watches or owns writes for it, and it is neither startable nor reserved from external admission.
|
|
@@ -3841,8 +3906,8 @@ cannot publish their derived defaults ahead of that restart.
|
|
|
3841
3906
|
|
|
3842
3907
|
I2 accepts definitions with a native headless factory and environment or ACP
|
|
3843
3908
|
session MCP identity. Additional Claude/channel peers belong to I3. Jcode's
|
|
3844
|
-
project-file MCP identity
|
|
3845
|
-
instances. Additional instances start headlessly; the existing Codex native TUI
|
|
3909
|
+
project-file MCP identity was not isolated by this phase and was refused for extra
|
|
3910
|
+
instances; #685 lifts that, see "Extra Jcode instances" below. Additional instances start headlessly; the existing Codex native TUI
|
|
3846
3911
|
still initializes and attaches to its instance's own app-server proxy. The hub
|
|
3847
3912
|
never initializes that app-server on the TUI's behalf. `ahub <kind> --as <id>`
|
|
3848
3913
|
selects the declared identity; selection is removed from native passthrough.
|
|
@@ -4023,6 +4088,22 @@ These rules are settled here and land with hub tasks #284-#286; until then the S
|
|
|
4023
4088
|
|
|
4024
4089
|
- S11 follow-up invariants: contiguous planning revisions changing exactly `["release"]` retain the S9 reviewed execution plan; other or missing planning history invalidates it. Cut-time CI cost reads are limited to the scoped PR ids (at most 16 in one repository); read errors or unbounded/mixed scope remain unknown and cannot abort a valid cut. Train history is capped at 256 events: configuration no-ops append nothing, meaningful configuration reserves cut/shipment slots, and capacity refusals are pre-effect.
|
|
4025
4090
|
|
|
4091
|
+
### S7 metrics and control bands amendment (#481, hub task #200)
|
|
4092
|
+
|
|
4093
|
+
- `ahub sdlc metrics [--since]` and the planner tool `hub_sdlc_metrics` are a pure projection of board history (`src/hub/sdlc-metrics.ts`: no clock, no I/O, no model). `Tasks.sdlcMetrics` is the one read, for a planner or the person, refused during a private turn; private tasks are counted as withheld and read for nothing else. Anything the board cannot derive is `null`/`unknown` and counted, never 0.
|
|
4094
|
+
- Scope cuts, each deliberate:
|
|
4095
|
+
- **No PR readback.** The report reads only the board. Time to merge and same-head CI reruns need a merged PR and its check runs, which `ahub sdlc costs` already reads under its own bounds (16 PRs, 20 s, public tasks only); a second reader of the same GitHub surface would duplicate those bounds and their privacy revocation checks. Both read `unknown` here, and the pure functions accept them as optional inputs so a later slice can supply them.
|
|
4096
|
+
- **S10's `costBandProposals` is not consumed.** It returns per-PR upper-limit breaches over complete merged-PR rows, a different data set (review rounds, CI runs and reruns per merged PR) from board-history metrics, and it takes absolute limits rather than a rolling baseline. S10 left the baseline, configuration and automatic card scheduler to S7 and delivered the pure breach data only; S7 builds the scheduler for its own metrics and leaves the PR cost metrics' bands for the slice that also reads PRs, so one configuration and one dedupe rule cover both.
|
|
4097
|
+
- **No security-scan baseline.** The last S7 line (a scheduled security scan with a recorded baseline feeding the propose tier) is not built. No scan source exists in the repository to baseline, and inventing one would claim a security control nothing enforces.
|
|
4098
|
+
- **No release metrics, no release bands, no gates-run record.** S11 recorded release cut and shipment times and left the bands and proposals over them to S7 (release interval, lead time from approval to release, and a band on the hotfix rate that proposes a planning card). S7 here reads task history only, so none of those is built: release history is a separate table with its own bounds and privacy rules, and a hotfix rate needs shipped releases to have a baseline at all. The per-release "gates run" record is S11's explicit deferral to S6/S7 and stays deferred: a cut record is not gate-execution evidence, and recording gates needs the S6 authorization path. These are the next slice, and `[bands]` has no release metric names until then, so a typo cannot silently configure one.
|
|
4099
|
+
- **`first_pass_check` is the hub's completion check, not PR CI.** It reads `check passed` and `check failed` at `hub_task_done`, so it is `unknown` wherever completion checks are off (the default). The issue's "first-pass CI success" is the PR-based figure and is not available without the readback above.
|
|
4100
|
+
- **Stale-head re-reviews need head evidence.** A review round is stale when its verdict is bound to a different head than its request (`reviewCost.head` on the done against `reviewFindings.head` on the verdict). A task without both is `unknown`. A `done` after an `approved` is not a signal: the board's move table has no way out of `approved`.
|
|
4101
|
+
- Control bands are optional `[bands.<metric>]` tables of `sdlc.toml`, validated by `bandsConfig` with field-level paths. The newest value is compared with the mean and population standard deviation of the preceding `window` values, on the bad side only, with a per-metric deviation floor, a `min_samples` floor, a `min_delta` floor that every tier including the drift rule honours, and a drift rule (`drift_run` newest values all over one deviation on the bad side is at least `diagnose`).
|
|
4102
|
+
- Rework rounds and stale-head re-reviews are final only when a task is approved, so a task still in flight is `unknown` for both and a series places them at the approval. Otherwise a provisional value would move at each verdict and could start a second episode inside one shift after its card was closed. Residual ceiling: the verdict head prefers the lifecycle receipt's `head` or `localHead` over `refs.commit` (`findingHead`) while the request head is `reviewCost.head`, so a task whose receipt head and commit disagree can read one spurious stale round; one shared head rule for both sides is the upgrade.
|
|
4103
|
+
- A breach is an episode: the run of non-ok values for one metric. `BandResult.episode` is the task that started the run, derived from history alone, so a restart finds the same key. `Tasks.proposeFromBands` is the only writer. `log` is one hub notice, recorded in the board's `band_notices` table so it is announced once. `diagnose` is one read-only `triage` backlog task and `propose` one `plan` backlog card, keyed `<metric>:#<episode>:<tier>` in the card. A metric with an open band card writes nothing more, so a value growing from diagnose to propose inside one episode adds no second card, and a sustained shift is one card. Card text is fixed words, numbers and task ids, screened like the recurring-finding card; the hub opens no GitHub issue.
|
|
4104
|
+
- The daemon evaluates every five minutes, only when the board's status revision or the band configuration moved since the last evaluation that ran, and not while it is stopping, in recovery or has a storage error (the same holds as the task sweep). A pass that was held, in mode off or without bands consumes nothing, so a breach seen then is written when the hold clears and bands enabled by a reload on an idle board are evaluated at the next tick. A failed evaluation is one `hub.log` line. Ceiling: one evaluation reads every task with its history; a history window is the upgrade if a board passes several thousand tasks.
|
|
4105
|
+
- Bands write cards only in `advise` or `enforce`; in `off` the writer is idle while the report still shows each configured band's tier.
|
|
4106
|
+
|
|
4026
4107
|
## Antigravity optional implementer peer (#485 P1)
|
|
4027
4108
|
|
|
4028
4109
|
Planner ownership ruling, 2026-10-19: the person-only `ahub agy` launcher owns exactly `plugin.json`, `mcp_config.json` and `hooks.json` inside `.agents/plugins/agent-hub/`. Apply symlink, hard-link and foreign-entry refusal and managed cleanup to all three files. Never write project-level `.agents/hooks.json`, another `.agents/` path or the person's `~/.gemini` settings. This amends the earlier two-file list in #485 AC3. AC0 must prove plugin-local hooks load without a global setting; failure stops implementation rather than falling back to manual configuration.
|
|
@@ -4185,6 +4266,714 @@ import. Builtins keep their existing applicable contract tests and explicit
|
|
|
4185
4266
|
unsupported-leg reasons; an external module exercises the same contracts in
|
|
4186
4267
|
its own CI.
|
|
4187
4268
|
|
|
4269
|
+
### S4 test integrity boundary (#481)
|
|
4270
|
+
|
|
4271
|
+
S4 applies only when the linked issue's complete label metadata contains `bug` (case-insensitive); known non-bug issues are exempt. Unknown labels never establish exemption: enforce blocks entering review and approval, advise reports unknown. Off retains its receipt/schema/readback behavior. No issue/PR body, patch or test output is retained.
|
|
4272
|
+
|
|
4273
|
+
The optional enabled-lifecycle `redFirst` receipt contains red/green legs, each `{source, head, check, result}`. Sources are `local`, `container` or `ci`; local/container results remain labelled claims, CI results require the exact named completed FAILURE/SUCCESS CheckRun at that commit, never an agent-posted StatusContext. `checkEvidence` labels each leg claimed or observed; verified describes receipt structure plus supplied results. Red is failed, green is passed, check names match, full lowercase SHAs differ, and green equals current PR head. Red must belong to the PR and have one parent with a nonempty commit diff adding only recognized test paths. Its single-parent ancestry within the complete PR commit set must touch only tests; earlier implementation or ambiguous/merge ancestry refuses the proof. Relinking repository, issue or PR drops an unsupplied red-first receipt. In enforce, only USER may change an already-established repository or issue; peers may initially fill missing identity and still relink the PR. This durable identity rule prevents owner-controlled bug-to-nonbug relinks before a first observation or after restart, without status writes; advise/off retain their prior authority.
|
|
4274
|
+
|
|
4275
|
+
Complete PR file metadata flags modified/deleted pre-existing test paths; newly added tests are permitted. Test paths have a test/tests/__tests__ directory component or a supported .test/.spec language suffix, plus conventional *_test.go, test_*.py, *_test.py, *_spec.rb and *Test.java names. Missing types, unsafe paths, rename/copy identities and truncation remain unknown. Missing/invalid/unknown red-first proof and changed existing tests block both review and approval in enforce, and appear as advisory findings in advise. Current task/privacy/authority fences hold after asynchronous reads and before effects.
|
|
4276
|
+
|
|
4277
|
+
Readbacks use bounded metadata calls within the existing shared 20 s deadline: at most 100 labels, 100 PR commits, fewer than 100 red-commit files and 100 contexts per CI leg; beyond those ceilings proof is unknown (upgrade path: bounded pagination). The merge queue's last remote snapshot revalidates applicability for every linked issue, up to 32 distinct issues (upgrade path: bounded snapshot pagination). Status/queue reports carry source labels and current-host-screened path identities; private metadata becomes neutral unknown, and receipt metadata is screened again on public output. Metadata cannot detect later gutting of newly added test assertions; content-aware review is the upgrade path. These gates never execute checks, merge, publish or confer authority, and source/fixture verification does not certify the separate native lifecycle receipt.
|
|
4278
|
+
|
|
4188
4279
|
### S2 plan conformance boundary (#481)
|
|
4189
4280
|
|
|
4190
4281
|
Optional proof/risks lists are bounded to 32 strings of 512 characters. Literal file/directory-prefix coverage uses complete current PR file metadata, never patches or glob execution. Root plan entries never cover all files; renamed/copied files whose old identity is unavailable make the inventory unknown. Untouched paths are advisory; uncovered changed paths block enforce approval until plan amendment or person acceptance. Host content revisions are separate from backlog planningRevision. "Plan revisions after the first observed push" binds the first observed remote head to plan fingerprint/revision; earlier pushes are unknown. Pending observations persist only at successful owner/reviewer transitions; status and queue reads never append history, and off mode captures no S2 observations or content revisions. Lost pending intervals remain unknown. The ceiling is no true push-event source; the upgrade path is a webhook or Actions push receipt. Person-only `ahub sdlc accept-departure <task> <head> <reason...>` records complete current repo/PR/head inventory and plan fingerprint/revision, with bounded sanitized privacy-screened rationale; changed identity, plan or inventory invalidates it, including plan edits while reporting is off. Departure consent expires on host restart to prevent unpersisted invalidations from reviving; the upgrade path is a durable host observation journal. Private projections reveal only existence, and unavailable private inventories stay unknown.
|
|
4282
|
+
|
|
4283
|
+
## Amendment: general-purpose intake, sources, drafts, schedules and deliverables (#516)
|
|
4284
|
+
|
|
4285
|
+
The owner gave the #516 track its go-ahead on 2026-10-22. This section closes the design gaps that blocked coding #508, #509, #511 and #512. Each child issue keeps its own ACs; where this section is stricter, it wins.
|
|
4286
|
+
|
|
4287
|
+
### Sources (#508)
|
|
4288
|
+
|
|
4289
|
+
- `ahub source add <path|url>` is person-only, refused in agent shells like the other console commands. The id is `src:` plus the first 16 hex digits of the snapshot's SHA-256, so adding the same bytes twice returns the existing id.
|
|
4290
|
+
- The snapshot lives in the hub state directory under `sources/<id>/`: `meta.json` (`{id, kind, origin, sha256, bytes, added_at, private}`), the raw bytes, and the projected text. The raw cap is the text-projection input cap (`MAX_PROJECTION_INPUT`, 16 MiB), and the text comes from the existing text_projection converters with their caps and sandbox. Larger inputs are refused with the cap in the reason; nothing is truncated silently.
|
|
4291
|
+
- A URL is fetched once, by the person's CLI process, over https only, following at most 5 redirects, within the same 16 MiB cap. No agent, sandbox or hub tool fetches a URL. Because the person issues the fetch, a PII project does not refuse it; the snapshot is screened like any file.
|
|
4292
|
+
- A path under `pii.paths`, or a snapshot whose text the PII screen flags, is stored with `private: true`.
|
|
4293
|
+
- Privacy is monotonic per id. Because equal bytes share one id, `private` is the OR over every registration of those bytes. Re-adding them from a private path, or with screened text that is now flagged, upgrades the existing record to private. Re-adding from a public path never downgrades it. `meta.json` keeps every origin; a private record shows no origin to anyone but the person. Readers, the CLI and dashboard lists, overlap notices and plan validation always use the record's current flag, never the flag of the registration that created it. Implementation tests cover both orders, public then private and private then public, and show that the second add returns the same id with `private: true`. `hub_source_read` returns the text of a private source only to the on-prem worker inside a PII task. Every other caller gets `{id, kind, private: true}` with no text, origin or excerpt.
|
|
4294
|
+
- `hub_source_read {id, range?}` takes `range: {offset, length}` in characters but bounds the response in bytes: after `sanitize()` and the PII screen, the text is at most 32 KiB of UTF-8, cut at a character boundary with an explicit cut marker, and the reply returns the `next` character offset. It is excluded from capture.
|
|
4295
|
+
- The plan's `sources` field is validated against the registry: an unknown id is refused at propose, accept and plan. Overlap notices name registry ids, never origins.
|
|
4296
|
+
|
|
4297
|
+
### Intake drafts (#511)
|
|
4298
|
+
|
|
4299
|
+
- A draft is a backlog card carrying `draft: true`. Backlog work is never offered, `hub_backlog_pull` refuses a draft, and the draft's children inherit the mark, so AC3 holds by construction.
|
|
4300
|
+
- `ahub intake "<text>" [--source <id>...]` (person-only) creates one parent draft holding the request, with its sources validated as above.
|
|
4301
|
+
- With a planner, the hub sends it a `task` envelope of class `plan` that names the draft id. The request text goes through `publicView`. The planner answers with `hub_backlog_split` on the draft, giving each child a profile class, its sources, dependencies (`after`/`afterParts`) and acceptance criteria in the #507 criteria-id format.
|
|
4302
|
+
- Without a planner, the hub model (`inference.ts`) proposes the same split as JSON. The hub validates it against the `hub_backlog_split` schema and writes it; invalid output leaves the parent draft unsplit, with the reason.
|
|
4303
|
+
- PII in the request never goes to a planner peer, because PII turns refuse `hub_backlog_split` (`taskOpBody`). The hub's on-prem model proposes the split instead, under the existing triage screen, and the hub writes it itself through `Tasks` as private (PII) drafts, which show as stubs everywhere. Without an on-prem model, the draft stays unsplit for the person.
|
|
4304
|
+
- `ahub intake approve <id>` (person-only) clears `draft` on the parent and its children, which then follow the normal backlog pull. `ahub intake discard <id>` cancels them through `Tasks.cancel`.
|
|
4305
|
+
|
|
4306
|
+
### Schedules and folder triggers (#512)
|
|
4307
|
+
|
|
4308
|
+
- `schedules` is a machine-local key: add it to `MACHINE_LOCAL` in `config-trust.ts`, so only `config.local.json` can set it. A committed config that creates tasks is refused, like the other machine-local keys.
|
|
4309
|
+
- Entry: `{id, every, class, title, detail?, sources?, owner?, queue?: "backlog" | "ready"}`. `every` is `<n>m|h|d` with a 15-minute minimum. `cron` is accepted by the schema but refused with "cron not supported yet; use every", because a cron parser is a new runtime dependency or new code; it is a later slice.
|
|
4310
|
+
- Due checks run on their own interval timer, started whenever `schedules` or folder triggers are configured, independent of `task_sweep.enabled` (the task sweep's timer exists only when that option is on, and it defaults to off). The timer runs no task sweep, and it is re-armed on config reload. `queue` defaults to `backlog`. Missed runs coalesce: a schedule proposes at most one task per tick, and none while its previous task is still open. A global pause or hold skips the tick and coalesces it. Last and next run times live in `hub.db`, and status and the dashboard show the next run.
|
|
4311
|
+
- Folder trigger: `{id, folder, glob?, class, title, ...}`. `folder` is resolved with `realPath`, must lie inside the project or a declared data path, and is polled on the schedules timer, not watched. Symlinks that leave the folder are skipped. A new file (by `realPath`, size, mtime and SHA-256 recorded in `hub.db`) is registered as a source (#508) and proposes one task that names it. The trigger therefore lands after the source registry.
|
|
4312
|
+
- `ahub schedule list|run <id>|pause <id>|resume <id>` is person-only.
|
|
4313
|
+
|
|
4314
|
+
### Deliverables (#509)
|
|
4315
|
+
|
|
4316
|
+
- In a PII task, or for a deliverable under `pii.paths`, the record keeps the id, format, version and SHA-256 only, never a path or an excerpt, in every view, log and memory call. Hashing still runs on the real path under the existing path guard.
|
|
4317
|
+
|
|
4318
|
+
### Knowledge corpus (#510), owner decision 2026-10-22
|
|
4319
|
+
|
|
4320
|
+
- The hub indexes plain notes folders only, read-only. `corpus.paths` is a machine-local key.
|
|
4321
|
+
- A corpus path that lies in, is, or contains an Obsidian vault is refused. The check runs on the `realPath` of the configured root, so a symlink into a vault is caught. It looks for a `.obsidian/` directory in every ancestor of that real path (a root inside a vault, such as `/vault/notes` when `/vault/.obsidian` exists), at the root itself, and below it. The index walk also skips no `.obsidian/` it meets; it refuses the root instead. Evidence it cannot read (an unreadable ancestor or subdirectory, or a realPath failure) refuses the path; it never certifies a plain folder. The reason names MCP: a vault is reached only through the Obsidian MCP tools, never read by the hub from disk.
|
|
4322
|
+
- AC3 (vault candidates) stays with agent-mem (tasks 112 and 122); the hub adds no vault reader.
|
|
4323
|
+
|
|
4324
|
+
### On-prem web reading (#515), owner decision 2026-10-22
|
|
4325
|
+
|
|
4326
|
+
- `web_fetch` is refused for the whole turn of a PII task, whatever the host. The outbound URL or query could carry PII to the web, and `pii.paths` and `sanitize()` do not cover outbound requests.
|
|
4327
|
+
- Outside PII turns it runs only through the egress proxy, to hosts in a machine-local per-project allowlist, and returns bounded text only.
|
|
4328
|
+
- `doc_read` is unaffected by this rule; it keeps its own `pii.paths` and `data.readonly` checks.
|
|
4329
|
+
|
|
4330
|
+
## Amendment: roles per SDLC stage (#525)
|
|
4331
|
+
|
|
4332
|
+
Written against main `8efdba48` (v0.12.45 + #671). Claims about current code carry `file:line` at that commit. "Probe" means a read-only `bun` run of `validateInstances` / `resolveSettledRoles` against that commit on 2026-10-22. Anything marked unverified was not run or read.
|
|
4333
|
+
|
|
4334
|
+
This amendment extends, and where stated replaces, the I2/I3 instance amendments (#450), Reviewer separation (#223), Reviewer pool (#519), Designated planner (#455) and S9 plan review (#481). Everything not named here keeps its current rule.
|
|
4335
|
+
|
|
4336
|
+
### Current state (what changes and what does not)
|
|
4337
|
+
|
|
4338
|
+
- Claude channel instances accept only roles inside `instances.roles: ["reviewer", "planner"]` (`src/adapters/definitions/builtin-metadata.ts:92`, checked at `src/hub/instances.ts:92-93`) and refuse `start_mode` and `worktree` (`src/hub/instances.ts:91`). A headless Claude instance must be exactly `["reviewer"]` and refuses `worktree` (`src/hub/instances.ts:87-89`). Probe: `claude-impl` with `["implementer"]` and a worktree is refused in both forms.
|
|
4339
|
+
- Codex has no `instances.roles` ceiling (`builtin-metadata.ts:109`), so the headless branch skips the role check (`src/hub/instances.ts:95-98`). Probe: Codex instances with `["planner"]` (headless or no mode), `["reviewer"]` headless (native app-server), `["reviewer"]` + `review_mode: "fresh"`, `["implementer"]` + worktree, and `["conductor"]` are all accepted today. Probe also: a `local-*` instance with `["planner"]` is accepted, although `local` is excluded from `PLANNER_PEERS` (`builtin-metadata.ts:145`, `src/adapters/registry.ts:55`).
|
|
4340
|
+
- Planner authority today is "holds the `planner` role in the settled map": `planningAuthority` (`src/hub/tasks.ts:833-835`), custom stage actor `planner` (`src/hub/tasks.ts:2511`), planner notices (`src/hub/tasks.ts:397-401`, `:426`, `:2236`). Several planners already work when no `planner` key is set (probe: `resolveSettledRoles` returns `planners: ["claude", "codex-plan"]`, source `roles`). The `planner` key pins one peer and only from `PLANNER_PEERS` (`src/hub/settled-roles.ts:18-23`); the loader throws on anything else (`src/hub/daemon.ts:289-291`) and the setting is an enum of `PLANNER_PEERS` (`src/hub/settings.ts:58`). Probe: `planner: "codex-plan"` is ignored and the source stays `roles`.
|
|
4341
|
+
- S9 records `planReview: { planner: by, ... }` (`src/hub/tasks.ts:2491`) with no check that `by` did not write the plan (`src/hub/tasks.ts:2460-2491`).
|
|
4342
|
+
- `assign()` picks owner and reviewer, never a planner (`src/hub/routing.ts:325`, `Assignment` at `:302-319`). The reviewer is never the current owner (`not` = `reviewOwner`, `src/hub/routing.ts:494`, `:541`). Former owners of the task are not excluded (no history read for that purpose; unverified beyond `routing.ts:483-560`).
|
|
4343
|
+
- A declared extra owns `implement`/`bulk_edit`/`test` only with the implementer role and `ownerIsolation[id] === true` (`src/hub/routing.ts:400-403`); the daemon sets it only for an adapter the hub started in a verified noncanonical root (`src/hub/daemon.ts:1346-1349`). A person-started channel instance never gets it.
|
|
4344
|
+
|
|
4345
|
+
### R1. Role capability per kind and form
|
|
4346
|
+
|
|
4347
|
+
One capability rule, read by `validateInstances`, `loadConductorPolicy` and `assign()`:
|
|
4348
|
+
|
|
4349
|
+
- **Planner** requires the base kind's `projection.plannerEligible === true` (the field `PLANNER_PEERS` is derived from, `src/adapters/registry.ts:55`). True for claude, codex, kimi, qwen, jcode; false for pi, local, agy.
|
|
4350
|
+
- **Every other flow role** requires membership in the resolved runtime definition's `instances.roles` (`src/adapters/registry-types.ts:205`) when it is set, and in `routing.allowedRoles` (`registry-types.ts:261`) when that is set. Unset `instances.roles` keeps today's behaviour (no instance ceiling).
|
|
4351
|
+
- The form is the runtime definition: the base definition (terminal/channel form for claude; app-server for codex) or a variant chosen by `resolvedDefinition` (`src/adapters/registry.ts:110-118`). Each variant carries its own `instances.roles`.
|
|
4352
|
+
|
|
4353
|
+
Capability table after this amendment (P planner, I implementer, V verifier, R reviewer):
|
|
4354
|
+
|
|
4355
|
+
| Kind | Form | Roles | Notes |
|
|
4356
|
+
|---|---|---|---|
|
|
4357
|
+
| claude | channel/terminal (base, `instances.mode: "external"`) | P, I, V, R | I or V requires `worktree` (R5 terminal) |
|
|
4358
|
+
| claude | headless reviewer variant (#223) | R only, exactly `["reviewer"]` | unchanged |
|
|
4359
|
+
| claude | headless implementer variant (new) | I, V; must include I | requires `worktree`; refuses P and R |
|
|
4360
|
+
| codex | app-server (headless start, TUI attach) | P, I, V, R, conductor | unchanged; no ceiling added |
|
|
4361
|
+
| codex | fresh reviewer variant (#519) | R only | unchanged, `review_mode: "fresh"` |
|
|
4362
|
+
| kimi, qwen, jcode | unchanged | unchanged | planner allowed only through `plannerEligible` (already true) |
|
|
4363
|
+
| pi, local, agy | unchanged | planner refused | new refusal, see below |
|
|
4364
|
+
|
|
4365
|
+
Validator changes (`src/hub/instances.ts`):
|
|
4366
|
+
|
|
4367
|
+
1. `builtin-metadata.ts:92`: claude base `instances.roles` becomes `["planner", "implementer", "verifier", "reviewer"]`.
|
|
4368
|
+
2. New `CLAUDE_IMPLEMENTER_VARIANT` beside `CLAUDE_REVIEWER_VARIANT` (`builtin-metadata.ts:72-81`), exposed as `headlessVariant` on the claude definition, with `instances: { mode: "headless", roles: ["implementer", "verifier"] }`, `transport: "hub-native"`, `start.forms: ["headless"]`, `start.create: createClaudeImplementer`, `budget: { account: "claude" }`, no `routing.reviewOnly`. External and configured definitions cannot declare `headlessVariant` (same rule as `reviewerVariant`, `registry.ts:219`, `:235`).
|
|
4369
|
+
3. `resolvedDefinition` (`registry.ts:110-118`): order is reviewer variant (unchanged condition), then `headlessVariant` when the declaration is a non-builtin id with `start_mode: "headless"` and roles do not equal `["reviewer"]`, then the base.
|
|
4370
|
+
4. External branch (`instances.ts:84-94`) becomes:
|
|
4371
|
+
- `start_mode: "headless"` with roles exactly `["reviewer"]`: reviewer variant, `worktree` refused (unchanged, `:87-89`).
|
|
4372
|
+
- `start_mode: "headless"` otherwise: implementer variant. Roles must be a nonempty subset of `["implementer", "verifier"]` containing `implementer`; `worktree` required; `permission_mode` limited to `ask` or `ask-when-needed` (default `ask-when-needed`); `never-ask` refused with "a headless Claude implementer has no hub sandbox; never-ask is unavailable". `review_mode` refused.
|
|
4373
|
+
- No `start_mode` (terminal): roles nonempty and inside the base ceiling. `worktree` required when roles include `implementer` or `verifier`, refused otherwise (planner/reviewer channel instances stay on the project checkout, as I3). `permission_mode` stays refused (Claude's terminal mode is the launcher's, as today).
|
|
4374
|
+
5. Planner check, all branches: a declared or policy role `planner` on a kind with `plannerEligible !== true` is refused at validation ("this kind cannot plan"). This is new for `local-*` and `pi-*` instances (probe shows `local-x` `["planner"]` accepted today). `loadConductorPolicy` drops a `roles.<peer>` planner entry for such a kind with the existing ceiling notice (`src/hub/daemon.ts:283-286`), never throws.
|
|
4375
|
+
6. Error text in `:89`, `:91`, `:93` is replaced; no other branch changes.
|
|
4376
|
+
|
|
4377
|
+
Still refused: a headless Claude planner (plan from a channel instance); a headless Claude instance mixing reviewer with implementer/verifier (a fresh reviewer is read-only by construction; a writing session never reviews); `worktree` on any reviewer or planner-only Claude instance; external/configured kinds as instance kinds (`instances.ts:68-69`); `start_mode` other than `"headless"` (`instances.ts:107-108`); everything `routing.allowedRoles` excludes (agy).
|
|
4378
|
+
|
|
4379
|
+
### R2/R4. Stage holders and independence
|
|
4380
|
+
|
|
4381
|
+
Stage to role, fixed:
|
|
4382
|
+
|
|
4383
|
+
| Stage base | Holder | Picked by |
|
|
4384
|
+
|---|---|---|
|
|
4385
|
+
| intent, spec, plan | the task's planner (R3) | `assign()` planner walk, or the person |
|
|
4386
|
+
| build, test | the owner: implementer (verifier for `test` class may hold it as today) | `assign()` owner walk (`routing.ts:471-479`) |
|
|
4387
|
+
| review | the reviewer | `assign()` reviewer walk (`routing.ts:483-560`), pool #519 |
|
|
4388
|
+
| merge, release | the person, or the task's/release's planner | unchanged authority, now per task/release (R3) |
|
|
4389
|
+
|
|
4390
|
+
- Explicit holders per stage are the existing knobs, not a new lifecycle store: `[classes.<class>].peers` in `routing.toml` for owners, `[classes.review].peers` and `review.pool` for reviewers, the `planner` setting (now a list, R3) for planners. Custom stage `actor` (#257, checked at `tasks.ts:2511`) keeps its four values; `planner` there now means the task's planner (R3), `owner/planner` likewise. No per-stage holder list is added to lifecycle config (ponytail: add `stages[].holders` only if a project needs a holder set that differs from the role's holders).
|
|
4391
|
+
- Each holder is chosen by its own walk over its own candidates. A multi-role peer appears in each walk where it holds the role. The walks share only these independence rules:
|
|
4392
|
+
- **I-1 (existing).** The reviewer is never the current owner, by id (`routing.ts:494`, `:541`).
|
|
4393
|
+
- **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
|
+
- **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` (`src/hub/board.ts:69`, `:348`); when none (lifecycle off), the latest entry whose `planning.fields` includes `plan`; when none, `history[0].by` if `plan` is nonempty; otherwise unknown. If the caller is the author and another planner holder is rostered and not offline, refuse pre-effect ("an independent planner reviews this plan"). If the caller is the author and no other holder is eligible, record the receipt with `selfReview: true` (the single-planner case keeps working).
|
|
4396
|
+
- **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
|
+
- 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
|
+
- **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()`.
|
|
4399
|
+
|
|
4400
|
+
### R3. Per-task planner
|
|
4401
|
+
|
|
4402
|
+
- **Field.** `tasks.planner TEXT NULL` (board migration beside `src/hub/board.ts:223`). `releases.planner TEXT NULL` likewise. `null` is legacy: any current planner holder acts, exactly as today. Existing rows keep `null`; no backfill.
|
|
4403
|
+
- **Setting.** `planner` accepts a string (one pinned planner, unchanged) or a list of up to 8 distinct ids. Each id is a builtin in `PLANNER_PEERS` or a declared instance id whose capability (R1) includes planner. The listed ids are the only planner holders (the exclusive rule of `settled-roles.ts:20-23`, applied to each). `SettingDef` type changes from `enum` to `peers` (the type exists, `src/hub/settings.ts:27`). A one-element list is written back as a string so an older daemon still loads it. `loadConductorPolicy` (`daemon.ts:289-291`) validates against the startup-bound catalog instead of `PLANNER_PEERS` alone. `resolveSettledRoles` takes `planner: string | readonly string[] | undefined`; `source` values are unchanged; automatic fallback (`settled-roles.ts:25-31`) still picks one builtin.
|
|
4404
|
+
- **Choice.** At `propose` (after the PII screen and the board insert, in the same flow that records the owner assignment) and at backlog pull when `planner` is `null`:
|
|
4405
|
+
- a person-named planner (`namedPlanner`) is exact, refused like a named owner if not a holder (#601 rule);
|
|
4406
|
+
- 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
|
+
- 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.
|
|
4410
|
+
- **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.
|
|
4411
|
+
- **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
|
+
- **Release planner.** `newRelease` records `by` as the release's planner (`null` for the person); cut/configure/assign check it like a task's.
|
|
4413
|
+
- **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.
|
|
4414
|
+
|
|
4415
|
+
### R5a. Headless Claude implementer
|
|
4416
|
+
|
|
4417
|
+
Declaration: `"claude-impl": { "kind": "claude", "roles": ["implementer"], "start_mode": "headless", "worktree": ".worktrees/claude-impl" }`, optional `permission_mode: "ask" | "ask-when-needed"`.
|
|
4418
|
+
|
|
4419
|
+
- **Worktree.** The I2 rule: an explicit registered worktree at `<project>/.worktrees/<id>` with an attached branch, resolved by `resolveInstanceExecutionCwd`. The hub creates no worktree or branch (I4 orchestration stays a follow-up); the implementer switches task branches inside it. `executionOwners` is set for the started peer, so `ownerIsolation` holds (`daemon.ts:1346-1349`).
|
|
4420
|
+
- **Start.** `ahub claude --as <id>`, the console and the dashboard ask the hub to start it (as #223). It holds no process while idle.
|
|
4421
|
+
- **One session per task.** For a delivery the peer groups envelopes by `refs.task` and runs one child at a time, in the worktree:
|
|
4422
|
+
- first turn of a task: `--session-id <new uuid>`, session persisted (no `--no-session-persistence`);
|
|
4423
|
+
- later turns: `--resume <that task's uuid>`; never `--continue`, never `--fork-session`, never another task's id;
|
|
4424
|
+
- 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`);
|
|
4425
|
+
- 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
|
+
- 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`. Unverified: `--permission-prompt-tool` is named by the `--permission-prompts` help text of 2.1.307 but has no entry of its own; the slice proves it with a fake and one live probe before claiming support, and refuses to start (reason recorded) on a Claude version where the probe fails.
|
|
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.
|
|
4429
|
+
- **Permissions.**
|
|
4430
|
+
- `ask`: the hook decides nothing; every write, edit and shell call reaches the prompt tool.
|
|
4431
|
+
- 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
|
+
- `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
|
+
- 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 600 characters at a character boundary (#72 rule). It never includes task text. The original arguments stay in the child's pending request and are what Claude's allow response refers to; they are not logged or shown. Options are allow-once and deny; no allow-always. While a request waits, the watchdog is kept alive (as ACP, #5).
|
|
4435
|
+
- **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. If Claude cannot reach the MCP server at all, its behaviour is unverified; the fake test pins the hub side (deny), and the live smoke records Claude's.
|
|
4437
|
+
- **Hook down.** Decides nothing (`facts-hook.ts:6`), so the call reaches the prompt tool: fail closed to the person, never to allow.
|
|
4438
|
+
- **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"]`).
|
|
4440
|
+
- **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
|
+
- **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
|
+
- **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).
|
|
4443
|
+
|
|
4444
|
+
### R5b. Terminal Claude implementer
|
|
4445
|
+
|
|
4446
|
+
Declaration: `"claude-impl-t": { "kind": "claude", "roles": ["implementer"], "worktree": ".worktrees/claude-impl-t" }`.
|
|
4447
|
+
|
|
4448
|
+
- `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. Unverified: that Claude Code spawns stdio MCP servers in its own cwd; the slice verifies it with a live probe and, if false, uses the `cwd` field of the first PreToolUse hook input instead.
|
|
4450
|
+
- **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
|
+
- 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
|
+
- 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.
|
|
4453
|
+
- Ceiling: the root evidence is reported by same-user processes the hub launched, not kernel isolation; it carries the same trust as launch records (#240 boundary).
|
|
4454
|
+
|
|
4455
|
+
Headless and terminal forms of one kind coexist as separate ids. Neither reviews a task it implemented (I-2).
|
|
4456
|
+
|
|
4457
|
+
### R6. Codex planner and reviewer instances
|
|
4458
|
+
|
|
4459
|
+
- Reviewer: done (#535, `review_mode: "fresh"`); native continuing `["reviewer"]` instances keep #519's rule. No change.
|
|
4460
|
+
- Planner: a Codex instance whose roles hold `planner` and neither `implementer` nor `verifier` is a planner instance. It is accepted today (probe); this amendment adds:
|
|
4461
|
+
- no worktree (refused, it plans on the project checkout);
|
|
4462
|
+
- its app-server threads start with the read-only sandbox. Unverified: the adapter's thread start sandbox parameter (`src/adapters/codex-appserver.ts` has no `sandbox` match at `8efdba48`); the slice adds it and refuses the start when the thread's reply does not confirm read-only (unknown is never read-only). The attached TUI is the person's and may change it; recorded as a ceiling;
|
|
4463
|
+
- owner admission already refuses it for code classes (no implementer role, `routing.ts:401`); it may own `plan`-class tasks.
|
|
4464
|
+
- Claude planner instances stay channel instances (I3). For a declared planner-only Claude instance the facts hook grants nothing for mutating file tools or shell, whatever its permission mode; Claude's own prompt goes to the person in that terminal. The builtin `claude` is unchanged.
|
|
4465
|
+
- Planning writes go through `Tasks` as today.
|
|
4466
|
+
|
|
4467
|
+
### R7. Visibility
|
|
4468
|
+
|
|
4469
|
+
Additive optional fields, no `PROTOCOL` bump (daemon.md):
|
|
4470
|
+
|
|
4471
|
+
- Status `roleInfo[peer]` (`daemon.ts:2795`) gains `form` (`channel` | `headless` | `terminal-implementer` | builtin form) and `capableRoles`; `planner` (`daemon.ts:2796`) keeps `{ peers, source }`.
|
|
4472
|
+
- `ui_snapshot` task rows and `ahub task show` gain `planner` and `plannerReason`; owner and reviewer rows show the reason already recorded (`reviewerReason`, `sameFamilyFallback`, reservation, named).
|
|
4473
|
+
- Dashboard task detail (`src/ui/index.html`) and the console Tasks panel show planner, owner and reviewer with reasons.
|
|
4474
|
+
- `ahub route explain` prints the planner walk (R2 purity).
|
|
4475
|
+
- Status lists `ownerIsolation` per extra as `isolated` / `not isolated: <reason>` (ids and fixed reasons only).
|
|
4476
|
+
|
|
4477
|
+
### Interactions
|
|
4478
|
+
|
|
4479
|
+
- **PII.** Unchanged gate in `assign()`: PII owners only `trust.onPrem` (`routing.ts:408`), reviewer the person. Both new Claude forms are cloud peers and never own PII work. PII tasks get no rotated planner. A headless Claude child never receives a private envelope (refused before spawn, as `reviewer-print.ts` does for reviews). PII-turn refusals (`hub_remember`, `hub_task_propose`, release tools, memory reads) apply to the on-prem worker as today; no new tool is callable in a PII turn. `hub_permission_request` payloads go through the bounded generic sanitizer defined under R5a Person approvals.
|
|
4480
|
+
- **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
|
+
- **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
|
+
- **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.** New target feature `role_stages` in `DAEMON_FEATURES` and `incompatibleRecoveryTarget` (daemon.md floor rule), required while the project declares a Claude implementer instance, a Codex planner-only instance with the read-only sandbox, or a `planner` setting that is a list or names an instance, and also while any task or release row has a non-null `planner` or any plan-review receipt has `planAuthor` or `selfReview`, even with no new configuration (rotation alone can bind planners). An older daemon would ignore those bindings and fall back to "any holder acts", which widens planner authority, so recovery to it is refused, never silently allowed. The floor check reads the board (a bounded `EXISTS` on each column), not only config. An older daemon would throw on the list or instance planner (`daemon.ts:289-291`) and on the new declarations (`instances.ts:89`, `:91`). An older daemon reading the board ignores the `planner` column (`SELECT *` + `parse`; unverified that `parse` tolerates unknown columns, check in 525-2).
|
|
4484
|
+
- **Settings.** `planner` stays a guarded settled-holder key (`roleHolders`, `daemon.ts:4431-4435`); a list change goes through the same confirmation.
|
|
4485
|
+
|
|
4486
|
+
### Acceptance (from the issue, mapped)
|
|
4487
|
+
|
|
4488
|
+
- AC-1 Config: Claude `["implementer"]` + worktree accepted as terminal and as headless; Codex `["planner"]` and Codex `["reviewer"]` headless accepted (already true, kept by regression tests); planner on pi/local refused; headless Claude `["planner"]`, `["implementer","reviewer"]`, implementer without worktree, `never-ask` refused.
|
|
4489
|
+
- AC-2 Pure `assign()` matrix: planner, owner, reviewer picked by separate walks; a multi-role holder never reviews its own or a former implementation (I-1, I-2); planner rotates by `task.id mod n` over three holders; route explain output equals the assignment trace.
|
|
4490
|
+
- AC-3 Daemon test: one task planned by `codex-plan`, built by headless `claude-impl` in its worktree with `ownerIsolation` true, reviewed by `codex-review` (fresh); each history entry names its holder and reason.
|
|
4491
|
+
- AC-4 Fake `claude -p`: same-task turns pass `--resume <same id>`, a new task gets a new `--session-id`; writes outside the worktree and to protected paths get no `allow` and reach the prompt tool, which denies on timeout and on hub failure; watchdog, stop and group cleanup match the reviewer tests; `hub_task_done` from the child completes the task; a stale child's launch id is refused.
|
|
4492
|
+
- AC-5 Planner tools refuse a peer that is not the task's planner, the person still acts, a single pinned planner and `null`-planner rows still work; I-4 refuses own-plan review with another eligible planner and records `selfReview` without one.
|
|
4493
|
+
- AC-6 Live six-instance run (#138 I4 leg) recorded in `docs/smoke.md`.
|
|
4494
|
+
|
|
4495
|
+
### Implementation slices (dependency order)
|
|
4496
|
+
|
|
4497
|
+
1. **525-1 Roles capability and validator.** Size M.
|
|
4498
|
+
- Paths: `src/adapters/definitions/builtin-metadata.ts`, `src/adapters/registry.ts`, `src/adapters/registry-types.ts`, `src/hub/instances.ts`, `src/hub/daemon.ts` (`loadConductorPolicy` planner drop only), tests `test/instances*.test.ts`, registry conformance.
|
|
4499
|
+
- Adds the capability rule, the claude base ceiling, a `headlessVariant` slot (definition stub refused at start until 525-4: "headless Claude implementer is not available in this release"), the new branches and refusals.
|
|
4500
|
+
- ACs: AC-1. Registry seeds and baseline unchanged except named reanchors.
|
|
4501
|
+
2. **525-3 Stage mapping and independence in `assign()`.** Size M. Depends on 525-1.
|
|
4502
|
+
- Paths: `src/hub/routing.ts`, `src/hub/tasks.ts` (call sites pass `planners`/`namedPlanner`, explain), `test/routing*.test.ts`.
|
|
4503
|
+
- Planner walk, `Assignment.planner`/`plannerReason`, I-2, explain lines. No board write of the planner yet (result unused until 525-2).
|
|
4504
|
+
- ACs: AC-2.
|
|
4505
|
+
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, `role_stages` floor for list/instance planner), `src/cli/main.ts`, `src/cli/help.ts`, `docs/verified.json` restamp of help.
|
|
4507
|
+
- ACs: AC-5.
|
|
4508
|
+
4. **525-4 Headless Claude implementer.** Size L. Depends on 525-1 (and 525-2 only for the daemon test's planner).
|
|
4509
|
+
- 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
|
+
- Includes the `--permission-prompt-tool` probe gate.
|
|
4511
|
+
- 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.
|
|
4513
|
+
- 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
|
+
- 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
|
+
6. **525-6 Codex planner and visibility.** Size M. Depends on 525-2, 525-3.
|
|
4516
|
+
- Paths: `src/adapters/codex-appserver.ts` (read-only thread sandbox, refusal on unconfirmed), `src/cli/facts-hook.ts` (no grants for planner-only Claude instances), `src/hub/daemon.ts` (status fields), `src/ui/index.html`, `src/cli/console.ts`, `docs/operations.md`, `docs/smoke.md` (six-instance live leg).
|
|
4517
|
+
- ACs: R7 fields in status, snapshot, dashboard, console, explain; AC-3 complete; AC-6 live run.
|
|
4518
|
+
|
|
4519
|
+
Spec-first: each slice's PR amends this section where its implementation differs, together with `docs/operations.md`.
|
|
4520
|
+
|
|
4521
|
+
## Amendment: typed host ports for the remaining native branches (#625 phase 6, slice 2+)
|
|
4522
|
+
|
|
4523
|
+
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.
|
|
4524
|
+
|
|
4525
|
+
### Scope and decisions
|
|
4526
|
+
|
|
4527
|
+
- Slice 1 (#667, `src/hub/peers.ts:15-22`) made `sessionGeneration`, `checkpointUsesSessionGeneration`, `permissionModeState` and `setPermissionMode` optional `PeerAdapter` ports. This amendment ports the rest of the native-class dispatch in `src/hub/daemon.ts`, or names each remaining site as an intentional native branch.
|
|
4528
|
+
- Port semantics, as in slice 1: an absent port keeps today's non-native path. A port that is present but `undefined` is unknown and refuses wherever it gates a safety check. Presence is tested with `in`, as `requestCheckpoint` already does (`daemon.ts:1702`).
|
|
4529
|
+
- **Proofs stay with their evidence.** A port exposes state, never a verdict. The daemon keeps every refusal.
|
|
4530
|
+
- Host-held evidence stays in the daemon: launch records in the state dir checked against `instanceId`, the daemon-owned socket claim generation, transcripts, Orca rows.
|
|
4531
|
+
- In-protocol evidence stays in the adapter: Codex `turn/start` and `thread/resume` frames, captured native approval policies (`codex-appserver.ts:73-76`, `:521-537`).
|
|
4532
|
+
- Why: a port that answered "proven" would make the adapter the authority. A configured or external adapter could then claim a proof it cannot hold.
|
|
4533
|
+
- **Generic `PeerAdapter` engine: not in scope.** A kind-neutral driver that runs any transport from a definition is a separate design.
|
|
4534
|
+
- Reasons:
|
|
4535
|
+
- A4 admits only headless ACP that returns the public `AcpPeer` (spec §A4; `daemon.ts:3791`), so an engine would have no second consumer.
|
|
4536
|
+
- The branches that remain are proofs or fixed-builtin native lifecycles (pi-rpc relay, Kimi TUI admission, Jcode TUI tuple). An engine would not remove them.
|
|
4537
|
+
- Running external code behind an engine needs an isolated host-port boundary that does not exist yet (`external-modules.ts:200` ponytail).
|
|
4538
|
+
- Ceiling:
|
|
4539
|
+
- External modules stay headless ACP only.
|
|
4540
|
+
- A new non-ACP transport needs a fixed-builtin adapter class plus its definition.
|
|
4541
|
+
- Every remaining native branch is listed in a committed ratchet table (slice 2a).
|
|
4542
|
+
- Upgrade path: an engine issue when a second non-ACP external transport is requested, starting from the port set below.
|
|
4543
|
+
- No slice changes persisted or recovery shape (restart snapshot, receipts, `claude-launch*.json`, `status.json` fields read by older coordinators, control messages). So no new `DAEMON_FEATURES` entry (`control-client.ts:12`). A slice that has to change one of these first adds a floor to `incompatibleRecoveryTarget` and `DAEMON_FEATURES` (daemon.md floor rule) and amends this section.
|
|
4544
|
+
|
|
4545
|
+
### Ports
|
|
4546
|
+
|
|
4547
|
+
`src/hub/peers.ts`, added to `PeerAdapter` after the slice-1 members. `StartForm` is a type-only import from `src/adapters/registry-types.ts:22`. `PermissionMode` is already imported (`peers.ts:2`).
|
|
4548
|
+
|
|
4549
|
+
```ts
|
|
4550
|
+
// #625 phase 6 slice 2+. Absent: today's non-native path. Present but undefined: unknown, refuses.
|
|
4551
|
+
/** Never offered hub_delivery_retry or its hint; the person resolves held deliveries. */
|
|
4552
|
+
readonly deliveryOnly?: boolean;
|
|
4553
|
+
/** The hub spawned this runtime and stops it itself; peer_stop admits only these. */
|
|
4554
|
+
readonly hubOwned?: boolean;
|
|
4555
|
+
/** A stop or failed start left an owned process whose exit is unconfirmed. */
|
|
4556
|
+
readonly pendingOwnedProcess?: boolean;
|
|
4557
|
+
/** The native form running now. */
|
|
4558
|
+
readonly form?: StartForm;
|
|
4559
|
+
/** Session persistence settled enough for a recovery prepare. */
|
|
4560
|
+
readonly recoveryReady?: boolean;
|
|
4561
|
+
/** Record live resume evidence before a requested stop or a recovery inspect/prepare/commit; throws when unavailable. */
|
|
4562
|
+
captureResume?(): Promise<Record<string, unknown>>;
|
|
4563
|
+
/** The native session that context readings, supervision receipts and memory provenance bind to. */
|
|
4564
|
+
readonly nativeSession?: string;
|
|
4565
|
+
/** Leave the roster without stopping a person's terminal; resetMode is the hub's next mode choice. */
|
|
4566
|
+
detachForRoster?(resetMode: PermissionMode): void | Promise<void>;
|
|
4567
|
+
/** Back on the roster after detachForRoster. */
|
|
4568
|
+
resumeFromRoster?(): Promise<void>;
|
|
4569
|
+
/** Fixed builtins only: a person's frontend attaches to the native session through this hub proxy. */
|
|
4570
|
+
readonly frontendProxy?: FrontendProxyPort;
|
|
4571
|
+
/** Fixed builtins only: a fixed operator notice; never native ids or provider text. */
|
|
4572
|
+
readonly statusNotice?: { code: "unidentified-cancellation"; message: string };
|
|
4573
|
+
/** Fixed builtins only, and only with session.nativeRevert: roll the native conversation back to before this native turn. */
|
|
4574
|
+
revertTurn?(nativeTurn: string): Promise<void>;
|
|
4575
|
+
```
|
|
4576
|
+
|
|
4577
|
+
```ts
|
|
4578
|
+
export interface FrontendProxyPort {
|
|
4579
|
+
/** Loopback URL the person's frontend attaches to. */
|
|
4580
|
+
readonly url: string;
|
|
4581
|
+
/** A frontend link owns the thread and its upstream socket is open. */
|
|
4582
|
+
readonly attached: boolean;
|
|
4583
|
+
/** The daemon's choice before any frontend attaches. Throws unless fresh: no link, offline, no restoration debt. */
|
|
4584
|
+
setStartupPermissionMode(mode: PermissionMode): void;
|
|
4585
|
+
/** Clear to ask while detached. Never refused; false when an overridden thread has no captured native baseline. */
|
|
4586
|
+
clearPermissionMode(): boolean;
|
|
4587
|
+
}
|
|
4588
|
+
```
|
|
4589
|
+
|
|
4590
|
+
Who sets what (parity by construction; each value is today's member, renamed at most):
|
|
4591
|
+
|
|
4592
|
+
| Port | Set by | Today's source |
|
|
4593
|
+
|---|---|---|
|
|
4594
|
+
| `deliveryOnly = true` | `AcpPeer`, `LocalPeer` | the `isLocalPeer \|\| isAcpPeer` pair, `daemon.ts:2123`, `:2257` |
|
|
4595
|
+
| `hubOwned = true` | `AcpPeer`, `PiPeer`, `LocalPeer`, `ReviewerPrintPeer` | the four-way predicate, `daemon.ts:3555` |
|
|
4596
|
+
| `pendingOwnedProcess` | `AcpPeer` | `acp.ts:327` |
|
|
4597
|
+
| `form` | `PiPeer` (its `mode`, `pi.ts:468`), `WsPeer` (`"tui"`), `AcpPeer` (`"headless"`) | `daemon.ts:3553` (WsPeer means tui), `:4727` (Pi mode) |
|
|
4598
|
+
| `recoveryReady` | `PiPeer` | `pi.ts:143` |
|
|
4599
|
+
| `captureResume` | `PiPeer` | `pi.ts:172` |
|
|
4600
|
+
| `nativeSession` | `CodexPeer` (`threadId \|\| undefined`), `AcpPeer` and `PiPeer` (`recoveryMetadata().sessionId` when it is a string) | `daemon.ts:1690-1691` |
|
|
4601
|
+
| `detachForRoster` | `WsPeer` (ignores the mode, `channel-peer.ts:91`), `CodexPeer` (`codex-appserver.ts:473`), `PiPeer` (`pi.ts:471`) | `daemon.ts:4758-4760` |
|
|
4602
|
+
| `resumeFromRoster` | `PiPeer` (`pi.ts:496`) | `daemon.ts:4773` |
|
|
4603
|
+
| `frontendProxy` | `CodexPeer`: `url` = `proxyUrl` (`:137`), `attached` = `proxyAttached` (`:488`), the two methods as at `:494`, `:500` | `daemon.ts:3701`, `:5198-5280` |
|
|
4604
|
+
| `statusNotice` | `CodexPeer` (`codex-appserver.ts:99`), unchanged | `daemon.ts:2791` |
|
|
4605
|
+
| `revertTurn` | `CodexPeer.revert` (`codex-appserver.ts:610`) | `daemon.ts:2446`, `:2459` |
|
|
4606
|
+
|
|
4607
|
+
Per-port absent and undefined semantics:
|
|
4608
|
+
|
|
4609
|
+
- `deliveryOnly`:
|
|
4610
|
+
- Absent or false: retry allowed (today's WsPeer, Codex and Pi path).
|
|
4611
|
+
- Undefined: retry refused, and the hint is withheld.
|
|
4612
|
+
- `hubOwned`:
|
|
4613
|
+
- Absent: `peer_stop` refuses with today's "unknown or non-owned peer" text.
|
|
4614
|
+
- Undefined: refused the same way.
|
|
4615
|
+
- `form`:
|
|
4616
|
+
- Absent: each site keeps its default (`"headless"` at the stop notice, `undefined` at the retained-terminal notice).
|
|
4617
|
+
- Undefined: `peer_stop` and the unmanaged-TUI permission refusal refuse. Display-only uses pass `undefined`.
|
|
4618
|
+
- `recoveryReady`:
|
|
4619
|
+
- Absent: settled.
|
|
4620
|
+
- False or undefined: not settled.
|
|
4621
|
+
- `nativeSession`:
|
|
4622
|
+
- Absent or undefined: unbound. Context checkpoints already refuse without a session (`daemon.ts:1704`), and the context view reads unknown (`context-window.ts:58`).
|
|
4623
|
+
- `detachForRoster`:
|
|
4624
|
+
- Absent, or `form === "headless"`: the roster-removal stop path.
|
|
4625
|
+
- `frontendProxy`:
|
|
4626
|
+
- Absent: no proxy, today's non-Codex path.
|
|
4627
|
+
- Undefined: permission changes, startup reconcile and the `already` start reply refuse with "frontend proxy state unknown".
|
|
4628
|
+
|
|
4629
|
+
Why `nativeSession` is an adapter getter and not `definition.session.currentId`: `currentId` returns the local worker's `sessionId` (`builtin-metadata.ts:52-56`, `:152`), but today `contextSession(local)` is `undefined`. Routing through `currentId` would add a session to local memory provenance (`daemon.ts:1832`). Ponytail: two session readers. Converge them when that difference is settled by test.
|
|
4630
|
+
|
|
4631
|
+
### Trust gate (A4)
|
|
4632
|
+
|
|
4633
|
+
- Export `isFixedBuiltinDefinition(definition): boolean` from `src/adapters/registry.ts`, the boolean twin of `assertNativeCapabilityScope`'s object-identity check (`registry.ts:26-27`, `:224-225`).
|
|
4634
|
+
- The daemon reads `frontendProxy`, `statusNotice` and `revertTurn` only when `isFixedBuiltinDefinition(definitionOf(peer))` holds for the resolved definition (instances included). Otherwise it treats them as absent.
|
|
4635
|
+
- So an external or configured adapter that declares them changes nothing: it cannot claim a native proof port.
|
|
4636
|
+
- The Claude launch proof (below) admits an owner only under the same gate.
|
|
4637
|
+
- `session.nativeRevert` joins the `assertNativeCapabilityScope` restricted list.
|
|
4638
|
+
- External definitions already cannot set it (`external-modules.ts:146` whitelist), and configured ACP does not set it (`configured-acp.ts:118`). The scope test makes that explicit.
|
|
4639
|
+
- At the ACP factory admission (`daemon.ts:3791`), the daemon also refuses an owner unless `deliveryOnly === true`, `hubOwned === true` and `form === "headless"`.
|
|
4640
|
+
- A module that returns an `AcpPeer` subclass therefore cannot widen delivery authority by overriding a restrictive port.
|
|
4641
|
+
- Unchanged ceiling: a pin authorizes code with daemon privileges and is not a sandbox (spec §A4).
|
|
4642
|
+
- `deliveryOnly`, `hubOwned`, `pendingOwnedProcess`, `form`, `recoveryReady`, `captureResume`, `nativeSession`, `detachForRoster` and `resumeFromRoster` are generic. They are read from any adapter, configured and external `AcpPeer`s included, as their `isAcpPeer` branches are today.
|
|
4643
|
+
- The public `/adapter` entry exports no new value. The port types reach module authors through the existing `export type *` (`src/adapters/public.ts:5-7`).
|
|
4644
|
+
|
|
4645
|
+
### Claude launch-generation and unattended-hook proof
|
|
4646
|
+
|
|
4647
|
+
- The proof logic stays in the host; the typed port is host-side. Extract the existing reads unchanged into `src/hub/claude-launch-proof.ts`, bound once in `startDaemon`:
|
|
4648
|
+
- `claudePermissionLaunch` (`daemon.ts:5180-5186`);
|
|
4649
|
+
- the `claudePermissionGenerations` map (`:2504`);
|
|
4650
|
+
- the hello launch check that closes 4430 (`:5323-5330`);
|
|
4651
|
+
- the facts expected-launch read (`:5571-5591`).
|
|
4652
|
+
- The adapter side contributes only `sessionGeneration` (slice 1; `WsPeer` sets it from the daemon's own claim, `channel-peer.ts:18`, `:43`).
|
|
4653
|
+
|
|
4654
|
+
```ts
|
|
4655
|
+
export interface ClaudeLaunchProof {
|
|
4656
|
+
/** This instance's claude-launch record when permissionHook is true; undefined when absent, foreign or unreadable. */
|
|
4657
|
+
permissionLaunch(peer: string): { launchId: string; unattended: boolean; hookPurpose?: string } | undefined;
|
|
4658
|
+
/** #551: "refuse" closes the hello with 4430; no record or no launchId admits, as today. */
|
|
4659
|
+
helloLaunch(peer: string, launchId: unknown): "admit" | "refuse";
|
|
4660
|
+
/** Facts: the launch a native hook must name (Orca terminal row, then launch record); undefined admits legacy hooks, as today. */
|
|
4661
|
+
expectedLaunch(peer: string): string | undefined;
|
|
4662
|
+
/** The daemon-owned channel object of a fixed-builtin claude-channel peer. Holds the one remaining isWsPeer guard. */
|
|
4663
|
+
owner(peer: string): WsPeer | undefined;
|
|
4664
|
+
/** Record the generation a permission-bearing facts reply was issued for. */
|
|
4665
|
+
bindGeneration(peer: string, generation: string): void;
|
|
4666
|
+
/** True only while the bound generation equals owner(peer).sessionGeneration. */
|
|
4667
|
+
verified(peer: string): boolean;
|
|
4668
|
+
}
|
|
4669
|
+
```
|
|
4670
|
+
|
|
4671
|
+
- Keyed by `transport === "claude-channel"`, as phase 1 chose (daemon.md Claude state files rule), plus the fixed-builtin gate.
|
|
4672
|
+
- `owner()` stays class-checked because the proof binds the launch record to the daemon's own socket claim. An adapter-reported generation must never satisfy it.
|
|
4673
|
+
- Native-hook turn driving (`nativeStarted` and `nativeStopped`, `channel-peer.ts:113`, `:119`) is reached through `owner()`, never through a generic port.
|
|
4674
|
+
- A second owner class (#677's headless Claude implementer, 525-4) is admitted only by an explicit edit to `owner()` with its own test, never by a generic port.
|
|
4675
|
+
- Unchanged:
|
|
4676
|
+
- `--unattended` refusal;
|
|
4677
|
+
- `hookPurpose` permission-only hooks that never drive busy state;
|
|
4678
|
+
- `readClaudeTranscriptCompletion` bounds;
|
|
4679
|
+
- `facts-hook.ts` decisions;
|
|
4680
|
+
- the `claude-launch.json` / `claude-native-*.json` split (daemon.md).
|
|
4681
|
+
|
|
4682
|
+
### Call-site inventory
|
|
4683
|
+
|
|
4684
|
+
Counts are call occurrences in `src/hub/daemon.ts` (`grep -o`). `ripwire . --callers=<predicate>` lists the same functions and lines.
|
|
4685
|
+
|
|
4686
|
+
| Predicate | Count | Ported | Intentional |
|
|
4687
|
+
|---|---|---|---|
|
|
4688
|
+
| `isCodexPeer` | 12 | 12 | 0 |
|
|
4689
|
+
| `isPiPeer` | 20 | 11 | 9 |
|
|
4690
|
+
| `isWsPeer` | 25 | 12 | 13 |
|
|
4691
|
+
| `isAcpPeer` | 12 | 8 | 4 |
|
|
4692
|
+
| `isLocalPeer` | 6 | 3 | 3 |
|
|
4693
|
+
| `isJcodePeer` | 1 | 0 | 1 |
|
|
4694
|
+
| `isReviewerPrintPeer` | 2 | 1 | 1 |
|
|
4695
|
+
|
|
4696
|
+
The brief cited 19 `isPiPeer` and 24 `isWsPeer`. The extra calls are second calls on the same line: `:5036` for Pi, and the `claiming` and `pullOnly` reads at `:2791` for Ws. `isReviewerPrintPeer` is included because `peer_stop` combines it with the others.
|
|
4697
|
+
|
|
4698
|
+
`isCodexPeer` (all ported):
|
|
4699
|
+
|
|
4700
|
+
| Line | Function | Assignment | Slice |
|
|
4701
|
+
|---|---|---|---|
|
|
4702
|
+
| 1690 | `contextSession` | `nativeSession` | 2c |
|
|
4703
|
+
| 2446 | `taskOpBody` turn_revert | `revertTurn` (gated), error text unchanged when absent | 2d |
|
|
4704
|
+
| 2791 | `status` | `statusNotice` (gated) | 2d |
|
|
4705
|
+
| 3701 | `startPeerBody` already | `frontendProxy.url` | 2d |
|
|
4706
|
+
| 4721 | `codexResumeCommand` | `owner?.nativeSession ?? lastCodexThreads.get(peer)`. Only Codex's notice uses the command (`native-daemon-policy.ts:6-12`), so other kinds' output is unchanged. | 2c |
|
|
4707
|
+
| 4759 | `changeRosterSetting` | `detachForRoster(resetMode)` | 2b |
|
|
4708
|
+
| 5198 | `changePermission` offline ask | `frontendProxy.clearPermissionMode()` | 2d |
|
|
4709
|
+
| 5206, 5215 | `changePermission` | `frontendProxy.attached` / present | 2d |
|
|
4710
|
+
| 5247 | `confirmPermissionDefault` | `frontendProxy` present and offline | 2d |
|
|
4711
|
+
| 5278 | `reconcileNativeStart` | `definitionOf(peer).permission.trackUnattended` (Codex only, `builtin-metadata.ts:110`), as `:5246` already does; no port | 2d |
|
|
4712
|
+
| 5280 | `reconcileNativeStart` | `frontendProxy` not attached → `setStartupPermissionMode` | 2d |
|
|
4713
|
+
|
|
4714
|
+
`isPiPeer`:
|
|
4715
|
+
|
|
4716
|
+
| Line | Function | Assignment | Slice |
|
|
4717
|
+
|---|---|---|---|
|
|
4718
|
+
| 1691 | `contextSession` | `nativeSession` | 2c |
|
|
4719
|
+
| 2549 | `recoveryReady` | `recoveryReady` port; update seed `roster-detached-pi-recovery` (`scripts/seeds.json:325-330`) in the same PR | 2c |
|
|
4720
|
+
| 3555, 3556 | `stopPeer` | `hubOwned`; `form === "tui"` refuses with `${displayName} belongs to a native TUI terminal; end it in its terminal` ("Pi", `builtin-metadata.ts:133`) | 2b |
|
|
4721
|
+
| 3565 | `stopPeer` | `captureResume` when idle | 2b |
|
|
4722
|
+
| 4727 | `retainedTerminalNotice` | `owner?.form`. Only Claude, Codex and Pi define the notice, and Claude and Codex ignore the form (`native-daemon-policy.ts:6-11`). | 2b |
|
|
4723
|
+
| 4760, 4773 | `changeRosterSetting` | `detachForRoster` / `resumeFromRoster` when `form !== "headless"` | 2b |
|
|
4724
|
+
| 5036 (x2) | `recoveryView` | `recoveryReady`, `form`; text uses `displayName` | 2c |
|
|
4725
|
+
| 5070 | `recoveryOp` | `captureResume` on idle peers | 2c |
|
|
4726
|
+
| 2791 | `status` | **Intentional.** `requestedModel` and relay backends come from the daemon-owned pi-rpc `modelRelays` (spec §A2b ceiling: pi-rpc owns the verified relay). | n/a |
|
|
4727
|
+
| 3038, 3068 | `settlePiContinuity`, `recordSessions` | **Intentional.** The persisted continuity bookmark is Pi-shaped (`{ pi }`, `native-recovery-continuity.ts:6-12`). Generalizing it changes recovery shape and needs a floor. | n/a |
|
|
4728
|
+
| 3519 | `hubStart` headless | **Intentional.** A generic `form === "tui"` would also catch an attached Claude or Kimi `WsPeer`, which today reaches `startPeer`. Existing ponytail at `:3516`. | n/a |
|
|
4729
|
+
| 3657, 3665, 3704 | `startPeerBody` | **Intentional.** Pi replacement and handover (`--replace-session` is a person-only flag, `stopPiForReplacement`). | n/a |
|
|
4730
|
+
| 3886, 4172 | pi-rpc port wiring, `admitPiRequest` | **Intentional.** Bridge `/budget` admission for the verified owner (adapters.md Pi). | n/a |
|
|
4731
|
+
|
|
4732
|
+
`isWsPeer`:
|
|
4733
|
+
|
|
4734
|
+
| Line | Function | Assignment | Slice |
|
|
4735
|
+
|---|---|---|---|
|
|
4736
|
+
| 1418 | `offerPullOnly` | `peer?.pullOnly === true` (existing port, `peers.ts:26`; only `WsPeer` sets it) | 2b |
|
|
4737
|
+
| 2727 | `permissionStatus` | `pullOnly` | 2b |
|
|
4738
|
+
| 3553 | `stopPeer` stop notice | `owner?.form ?? (owner ? "headless" : startMode(peer))` | 2b |
|
|
4739
|
+
| 4758 | `changeRosterSetting` | `detachForRoster`; the Kimi admission clear stays in that branch under `attach.admission` | 2b |
|
|
4740
|
+
| 5191 | `changePermission` | `owner?.form === "tui"`, or no owner and a tui start mode | 2b |
|
|
4741
|
+
| 2716, 5213, 5597, 5605, 5628, 5637, 5643 | `permissionDisplay`, `changePermission`, facts | `ClaudeLaunchProof.owner` / `verified` / `bindGeneration` | 2e |
|
|
4742
|
+
| 802, 5335, 5401, 5411, 5423 | socket close, hello, `delivery_receipt`, `inbox`, `delivery_complete` | **Intentional.** Host channel transport: the daemon constructs this `WsPeer` (`:5334`) and owns socket authentication (daemon.md line 3). | n/a |
|
|
4743
|
+
| 925 | bus `onImportantQueued` | **Intentional.** Pull-only nudge on the host channel (#398). | n/a |
|
|
4744
|
+
| 1688, 1704, 2791 (x2) | `contextSession`, `requestCheckpoint`, `status` | **Intentional.** `claiming` and `lastInboxAt` are the daemon's claim window and channel telemetry. | n/a |
|
|
4745
|
+
| 3642 | `startPeerBody` | **Intentional.** Inside `attach.admission`, Kimi's native TUI receipt, fixed builtin (spec §A2b ceiling). | n/a |
|
|
4746
|
+
| 5499 | `native_facts` | **Intentional.** The agy managed-hook proof binds to the daemon's socket generation (adapters.md native facts). | n/a |
|
|
4747
|
+
| 5724 | `watch` | **Intentional.** `attach.admission`; seed `kimi-watch-live-owner` (`scripts/seeds.json:3157-3162`) stays. | n/a |
|
|
4748
|
+
|
|
4749
|
+
`isAcpPeer`:
|
|
4750
|
+
|
|
4751
|
+
| Line | Function | Assignment | Slice |
|
|
4752
|
+
|---|---|---|---|
|
|
4753
|
+
| 1691 | `contextSession` | `nativeSession` | 2c |
|
|
4754
|
+
| 2123, 2257 | `ownHeldHint`, `hub_delivery_retry` | `deliveryOnly` | 2b |
|
|
4755
|
+
| 2536 | `importantWaitingStatus` | `definitionOf(id)?.transport === "acp"` with no `steer` while busy. The Kimi TUI `WsPeer` under an acp id is never busy (adapters.md). | 2b |
|
|
4756
|
+
| 3555, 3557 | `stopPeer` | `hubOwned`, `pendingOwnedProcess` | 2b |
|
|
4757
|
+
| 3624 | `startPeerBody` | `pendingOwnedProcess` (still under `permission.genericAcp`) | 2b |
|
|
4758
|
+
| 5279 | `reconcileNativeStart` | `owner.permissionModeState === "unmanaged"` (slice-1 port; Codex never returns unmanaged, `codex-appserver.ts:491`). `getPermissionMode()` stays on the typed `AcpPeer \| CodexPeer` parameter: `AcpPeer.permissionModeState` reads `"unknown"` where `getPermissionMode()` does not (`acp.ts:325-330`). | 2d |
|
|
4759
|
+
| 3634, 5333, 5724 | Kimi admission, hello `kimiPull`, `watch` | **Intentional.** `attach.admission`. | n/a |
|
|
4760
|
+
| 3791 | ACP factory admission | **Intentional.** The A4 trust boundary: the factory must return the public `AcpPeer`. It gains the port checks above. | n/a |
|
|
4761
|
+
|
|
4762
|
+
`isLocalPeer`:
|
|
4763
|
+
|
|
4764
|
+
| Line | Function | Assignment | Slice |
|
|
4765
|
+
|---|---|---|---|
|
|
4766
|
+
| 2123, 2257 | `ownHeldHint`, `hub_delivery_retry` | `deliveryOnly` | 2b |
|
|
4767
|
+
| 3555 | `stopPeer` | `hubOwned` | 2b |
|
|
4768
|
+
| 2791 | `status` `servedBy` | **Intentional.** Local relay telemetry, hub-native. | n/a |
|
|
4769
|
+
| 4205, 4259 | `holdPeer`, `validateRelease` | **Intentional.** `validateLocalChoice` over the local route and model. | n/a |
|
|
4770
|
+
|
|
4771
|
+
`isJcodePeer`:
|
|
4772
|
+
|
|
4773
|
+
| Line | Function | Assignment | Slice |
|
|
4774
|
+
|---|---|---|---|
|
|
4775
|
+
| 5858 | `native_tui_launch` | **Intentional.** The owned-acp TUI tuple; only a person's launcher gets it (daemon.md). | n/a |
|
|
4776
|
+
|
|
4777
|
+
`isReviewerPrintPeer`:
|
|
4778
|
+
|
|
4779
|
+
| Line | Function | Assignment | Slice |
|
|
4780
|
+
|---|---|---|---|
|
|
4781
|
+
| 3555 | `stopPeer` | `hubOwned` | 2b |
|
|
4782
|
+
| 1324 | review authority | **Intentional.** `reviewAuthority` compares `reviewToken`; never widen it (adapters.md Reviewers). | n/a |
|
|
4783
|
+
|
|
4784
|
+
After all slices, `daemon.ts` keeps 31 intentional calls (Pi 9, Ws 13, Acp 4, Local 3, Jcode 1, ReviewerPrint 1), plus one `isWsPeer` inside `claude-launch-proof.ts`.
|
|
4785
|
+
|
|
4786
|
+
### Slices
|
|
4787
|
+
|
|
4788
|
+
Every slice:
|
|
4789
|
+
|
|
4790
|
+
- `scripts/check.sh --changed` ends with `check: OK (changed)`.
|
|
4791
|
+
- No safety default is lowered. A4 holds: no new public export, no external module import path, no field leaves the restricted list.
|
|
4792
|
+
- Moved lines that `scripts/seeds.json` searches update their seed in the same PR (tests.md).
|
|
4793
|
+
- An implementation that has to differ from this text amends it first.
|
|
4794
|
+
|
|
4795
|
+
Common acceptance criteria:
|
|
4796
|
+
|
|
4797
|
+
- **No-branch test:** a plain `BasePeer` subclass with no native class, exposing the port, gets the behaviour in a real daemon. This is the pattern of `test/peer-capabilities.test.ts:61-181`.
|
|
4798
|
+
- **Parity tests:** the existing native tests for those sites stay green unchanged, and byte-identical error and status text is asserted where text moved to `displayName`.
|
|
4799
|
+
- **Ratchet:** the table loses exactly the slice's rows.
|
|
4800
|
+
|
|
4801
|
+
1. **2a: Ratchet and trust gate.** Area note: `tests.md`. Size S.
|
|
4802
|
+
- Paths:
|
|
4803
|
+
- new `test/daemon-native-branches.test.ts` and `test/daemon-native-branches.json` (rows `{predicate, function, count, reason}`; TypeScript AST over `src/hub/daemon.ts` and `src/hub/claude-launch-proof.ts`, reusing the source walk in `test/helpers/registry-inventory.ts`; never imports daemon code);
|
|
4804
|
+
- `src/adapters/registry.ts` (`isFixedBuiltinDefinition`; `session.nativeRevert` restricted);
|
|
4805
|
+
- `test/adapter-capability-scope.test.ts`;
|
|
4806
|
+
- `src/adapters/conformance/kit.ts` and `enrollment.ts`, with a ports leg.
|
|
4807
|
+
- ACs:
|
|
4808
|
+
- The table equals today's counts (the inventory above).
|
|
4809
|
+
- An inline fixture with one extra predicate call fails, and so does a row with an empty reason.
|
|
4810
|
+
- A configured definition that declares `session.nativeRevert` is refused.
|
|
4811
|
+
- Kit leg: a non-fixed-builtin definition whose product has `frontendProxy`, `statusNotice` or `revertTurn` fails the kit. (Today no product has these members, so the leg passes at 2a and starts biting from 2d.)
|
|
4812
|
+
- The ACP ports leg (`deliveryOnly`, `hubOwned`, `form: "headless"`) is not part of 2a: no ACP product exposes those members until 2b adds them, so 2a cannot assert them. 2a only reserves the leg in `UNSUPPORTED_LEGS` with the reason "ports added in 2b".
|
|
4813
|
+
- Builtins that do not apply name their reason in `UNSUPPORTED_LEGS`.
|
|
4814
|
+
2. **2b: Delivery-only, owned stop, form and roster detach.** Area note: `adapters.md` (Spawned processes; Turns). Size M.
|
|
4815
|
+
- Paths: `src/hub/peers.ts`, `src/adapters/acp.ts`, `src/adapters/local-worker.ts`, `src/adapters/pi.ts`, `src/adapters/reviewer-print.ts`, `src/adapters/channel-peer.ts`, `src/adapters/codex-appserver.ts` (`detachForRoster` signature only), `src/hub/daemon.ts`, tests.
|
|
4816
|
+
- Ports: `deliveryOnly`, `hubOwned`, `pendingOwnedProcess`, `form`, `captureResume` (stop use), `detachForRoster`, `resumeFromRoster`, plus the generic `pullOnly` reads.
|
|
4817
|
+
- ACs:
|
|
4818
|
+
- A plain peer with `deliveryOnly: true` is refused `hub_delivery_retry` and gets no retry hint.
|
|
4819
|
+
- With `deliveryOnly` undefined it is refused too.
|
|
4820
|
+
- A plain `hubOwned` peer is admitted by `peer_stop`; a `form: "tui"` one is refused with the `displayName` text.
|
|
4821
|
+
- Following the `detachForRoster` rule above (stop only when the method is absent or `form === "headless"`): a plain peer with `detachForRoster` and `form: "tui"` or `form` absent is detached, not stopped, on roster removal. The same peer with `form: "headless"`, or without the method, takes the roster-removal stop path.
|
|
4822
|
+
- Codex parity: `CodexPeer`, which has `detachForRoster` and no `form`, is still detached on roster removal and keeps its frontend proxy, as at `daemon.ts:4759` today. `stopPeer` still does not admit it as `hubOwned`.
|
|
4823
|
+
- ACP factory admission refuses an `AcpPeer` subclass that overrides `deliveryOnly` to false.
|
|
4824
|
+
- The conformance kit's ACP ports leg moves from `UNSUPPORTED_LEGS` to active: every ACP-transport definition's `start.create` product reports `deliveryOnly`, `hubOwned` and `form: "headless"`, and 2b's own ports land in the same PR as that leg.
|
|
4825
|
+
- `test/kimi-tui.test.ts`, `test/roster-pi.test.ts`, the `peer_stop` and configured ACP tests stay unchanged.
|
|
4826
|
+
3. **2c: Session binding and recovery settlement.** Area note: `daemon.md` (recovery), with the budget.md context rules unchanged. Size M.
|
|
4827
|
+
- Paths: `src/hub/peers.ts`, `src/adapters/codex-appserver.ts`, `src/adapters/acp.ts`, `src/adapters/pi.ts`, `src/hub/daemon.ts` (`contextSession`, `codexResumeCommand`, `recoveryReady`, `recoveryView`, `recoveryOp`), `scripts/seeds.json` (`roster-detached-pi-recovery`), tests.
|
|
4828
|
+
- ACs:
|
|
4829
|
+
- For every builtin, `contextSession` equals today's value: the local worker and reviewer print stay `undefined`, and an ACP empty string stays `""`.
|
|
4830
|
+
- A plain peer with `recoveryReady: false` blocks `recoveryReady()` and is named in `recoveryView`.
|
|
4831
|
+
- Undefined blocks too.
|
|
4832
|
+
- Memory provenance `session` (`daemon.ts:1832`) is unchanged for local.
|
|
4833
|
+
- Seed runner: green, then the named assertion fails, after the reanchor.
|
|
4834
|
+
4. **2d: Codex frontend proxy.** Area note: `adapters.md` (Permissions, modes and approvals). Size M.
|
|
4835
|
+
- Paths: `src/hub/peers.ts`, `src/adapters/codex-appserver.ts`, `src/hub/daemon.ts` (`changePermission`, `confirmPermissionDefault`, `reconcileNativeStart`, `startPeerBody`, `status`, `taskOpBody` turn_revert), `test/peer-capabilities.test.ts`, the Codex permission and revert tests.
|
|
4836
|
+
- The restoration debt stays inside `CodexPeer` (`nativeApprovalPolicies`, `overriddenApprovals`, `settlePermissionRestore`). The port changes no frame handling.
|
|
4837
|
+
- ACs:
|
|
4838
|
+
- A plain fixed-builtin-gated test peer (built in a test catalog that marks it builtin by identity) with `frontendProxy` gets the proxy refusals and startup reconcile.
|
|
4839
|
+
- The same peer under a configured definition is treated as absent.
|
|
4840
|
+
- `frontendProxy` undefined refuses permission changes and `already`.
|
|
4841
|
+
- The `unidentified-cancellation` notice shows only from a fixed builtin.
|
|
4842
|
+
- The Codex sticky-override, restoring-turn, detached-clear and attached-refusal tests stay unchanged.
|
|
4843
|
+
- `isCodexPeer` rows leave the table.
|
|
4844
|
+
5. **2e: Claude launch proof extraction.** Area note: `daemon.md` (Claude launch and hook rules). Size M.
|
|
4845
|
+
- Paths: new `src/hub/claude-launch-proof.ts`, `src/hub/daemon.ts` (hello, `permissionDisplay`, `changePermission`, facts), tests.
|
|
4846
|
+
- No change to `src/cli/launch.ts`, `src/cli/facts-hook.ts`, `src/adapters/claude-channel.ts` or the plugin bundle.
|
|
4847
|
+
- ACs:
|
|
4848
|
+
- The existing #242, #551, #517 and #624 daemon tests pass unchanged.
|
|
4849
|
+
- `daemon.ts` has no `isWsPeer` row for these functions.
|
|
4850
|
+
- A non-`WsPeer` adapter under a claude-channel id never verifies a permission generation, even when it reports `sessionGeneration`.
|
|
4851
|
+
- A foreign `instanceId` record and a 4430 mismatch behave as today.
|
|
4852
|
+
|
|
4853
|
+
Order: 2a first (it measures everything else). 2b and 2c are independent. 2d after 2b (roster detach). 2e is independent. Run one slice at a time per repo (resource policy).
|
|
4854
|
+
|
|
4855
|
+
### Interactions
|
|
4856
|
+
|
|
4857
|
+
- **A4.** Covered by the trust gate. An external module cannot reach `frontendProxy`, `statusNotice`, `revertTurn` or `ClaudeLaunchProof.owner`. Its restrictive ports are checked at factory admission, and its CI sees the new kit leg through `/conformance`.
|
|
4858
|
+
- **Recovery floors.** None added: ports are in memory, the restart snapshot still reads `recoveryMetadata()` (`daemon.ts:3073`, `restart.ts:15-27`), and `lastCodexThreads` stays daemon memory (`daemon.ts:2731`). The floor rule above applies to any slice that has to change this.
|
|
4859
|
+
- **#525 / PR #677** (open, head `8525ad00`):
|
|
4860
|
+
- **525-1** adds a `headlessVariant`. It must join `fixedBuiltinDefinitions` (`registry.ts:26-27` adds only `reviewerVariant`), or the gate treats the variant as non-builtin and ignores its ports. That fails closed, but it breaks 525-4.
|
|
4861
|
+
- **525-4**'s headless Claude implementer needs the facts permission proof for a child launch id. It extends `ClaudeLaunchProof.owner` explicitly with its own test (above), and never reads a generic port.
|
|
4862
|
+
- **525-5** adds `executionCwd` to `claude-launch.<id>.json` and an attach admission. `claude-launch-proof.ts` is the one reader: whichever of 2e and 525-5 lands second rebases onto the other. 2e changes no record fields.
|
|
4863
|
+
- **525-6** adds read-only thread sandbox confirmation in `codex-appserver.ts`. The confirmation is in-protocol evidence, so it stays in `CodexPeer`. If the daemon must refuse on it, add a readonly state member to `FrontendProxyPort` (`threadSandbox?: "read-only" | "workspace-write" | "unknown"`; unknown refuses), never a daemon `isCodexPeer`.
|
|
4864
|
+
- The `role_stages` floor belongs to #525. This amendment adds no floor.
|
|
4865
|
+
|
|
4866
|
+
### Unverified
|
|
4867
|
+
|
|
4868
|
+
- Total parity of `codexResumeCommand` and the retained-terminal notice after `form` and `nativeSession` widen to ACP and `WsPeer` owners. It rests on only Claude, Codex and Pi defining the notices (`builtin-metadata.ts:99`, `:113`, `:142`); 2b and 2c must test it.
|
|
4869
|
+
- That the Kimi TUI `WsPeer` is never `busy` (the `importantWaitingStatus` move). This is stated in adapters.md, not checked in source here.
|
|
4870
|
+
- Whether `exactOptionalPropertyTypes` is relevant: `tsconfig.json` does not set it. Presence is tested with `in`, as in slice 1.
|
|
4871
|
+
- PR #677 line references are from its diff at `8525ad00`, which is not merged.
|
|
4872
|
+
|
|
4873
|
+
### S6 person release authorization boundary (#481)
|
|
4874
|
+
|
|
4875
|
+
- `ahub sdlc authorize-release <version>` is console-only, never live-test delegated. The daemon binds consent to its candidate checkout's exact HEAD, committed package version, origin fetch/push identity and fresh committed-doc verdict. Caller-supplied expected HEAD must match. Head and origin are rechecked after the bounded package-owned documentation checker. Only ids, hashes, version, timestamp and counts enter the receipt/audit event; no project script or release operation runs.
|
|
4876
|
+
- Authoritative receipts live only in the current daemon generation's bounded memory ledger (at most 64 candidates). An additive metadata event audits consent but cannot recreate it. Restart revokes all approvals. The release report shows matching/stale consent without certifying native acceptance, changing the tag workflow or publishing.
|
|
4877
|
+
- Optional `ahub init --release-guard` installs a managed executable Git pre-push hook plus instructions. A regular existing hook is renamed, preserving its inode/mode, and chained with exact argv/stdin after successful guard readback. Linked/hardlinked hooks, occupied backups and custom hooksPath refuse before project writes. Re-init retains the guard; the wrapper pins Bun/package paths and needs re-init if they move.
|
|
4878
|
+
- Actual pushed `v*` refs must have a valid version and captured source-object/commit matching consent. Missing/mismatched commit/version/remote, unreachable daemon, deletion, unknown evidence or restart refuses the version-tag push. Branch-only pushes work offline. `--no-verify` remains the person's explicit bypass and is forbidden to agents; this is cooperative Git enforcement, not a sandbox.
|
|
4879
|
+
- Literal CLI preflights support a single named-remote tag refspec, GitHub release creation (existing remote tag readback, or explicit full SHA for a new tag), and current-checkout npm publish. Ambiguous/alternate-repository/bulk/workspace/archive selectors refuse. npm requires a clean tracked worktree and rechecks HEAD and committed documentation; artifact bytes, npm config and lifecycle scripts are outside this receipt. gh/npm instructions and preflight are the S6 boundary; adapter refusal belongs to #312/#313. No release effect is executed and no native/live pass is inferred. AC7 is covered by host-ledger units and actual hook/CLI/daemon fixture tests.
|
|
4880
|
+
|
|
4881
|
+
- S6 npm cleanliness uses filter-free cached-index/raw-blob comparison before and after docs readback, never `git status` or content conversions. Unknown modes, gitlinks, transformed bytes, unsafe paths or the 4,096-entry / 4 MiB-file / 32 MiB-total / 250 ms bounds refuse. Untracked artifact content remains outside this consent check.
|
|
4882
|
+
|
|
4883
|
+
## Amendment: one version policy for native agent CLIs (#691)
|
|
4884
|
+
|
|
4885
|
+
Native CLIs update often: Codex 0.146.0 → 0.163.0, and Kimi 2.2.0 → 2.3.0 by self-update on 2026-10-22. Separate exact pins made each update a refusal that only a hub release could lift, and some reasons hid the version.
|
|
4886
|
+
|
|
4887
|
+
- **One module.** `src/adapters/compat.ts` holds every gated kind's `verified` set, an optional compatible `line` (contract gates only) and its gate class. `assessVersion` gives verified, in-line, trusted or refused. The gates are the Codex and Kimi reviewer `prepare`, the Jcode preflight, probe and ACP identity, the agy launcher, the Qwen ACP identity and the doctor's Qwen probe. All of them use it.
|
|
4888
|
+
- **Person trust on this machine.** `ahub agents trust <id> <version>` writes `<hub home>/agent-compat.json` (schema 1, at most 64 entries, atomic; an invalid file trusts nothing). It is console-only by the identity classifier.
|
|
4889
|
+
- An isolation gate keeps every runtime proof after trust. Trust covers only the source-level claims a runtime probe cannot see.
|
|
4890
|
+
- Nothing is trusted automatically.
|
|
4891
|
+
- **Named reasons.** A version refusal starts with `VERSION_REFUSAL` and names only versions and the trust command. The daemon passes it through `sanitize()` for every reviewer backend, while other Codex prepare failures stay generic. The Jcode refusal is named before the owned daemon starts.
|
|
4892
|
+
- **Seeing drift.**
|
|
4893
|
+
- `ahub agents` (allowed for agents) and `ahub doctor` show each verdict.
|
|
4894
|
+
- A weekly workflow compares the npm-published versions of the npm-distributed kinds with `verified` and keeps one `agent-compat` issue current.
|
|
4895
|
+
- Jcode and agy have no public registry and are not watched; their launcher verdict is the signal.
|
|
4896
|
+
- **Moving a verified set.** A receipt in `docs/verification/` adds the version to `verified` in the same PR.
|
|
4897
|
+
|
|
4898
|
+
## Amendment: lifecycle identity linked by the hub (#694)
|
|
4899
|
+
|
|
4900
|
+
Lifecycle status read the issue and the PR only from `hub_sdlc_record`, which agents rarely call, so every task showed `issue identity missing` and `PR identity missing` while its title, refs and PR named both. The hub already trusted the same source for plan review.
|
|
4901
|
+
|
|
4902
|
+
- **Issue.** When the receipt has no issue and `planIssueIdentity` resolves exactly one, the hub writes `repo` and `issue` at plan review, at done, and in a groom backfill that runs after every authority check with its own bound (32 per call, separate from the metadata-read budget), so later tasks are reached on later calls.
|
|
4903
|
+
- **PR.** When the receipt has no PR, the hub links it at done. It reads the open PRs once and takes the one whose head equals the done's full `refs.commit`, or, with no commit, whose branch equals `refs.branch`. Zero or several matches link nothing. A full page of 100 (possibly truncated) or any malformed row makes the inventory unknown, so nothing is linked. Agent text is never parsed.
|
|
4904
|
+
- **Staged at done.** The done's identities are staged in memory: the lifecycle and review gates read them, and only the admitted done update (or its `done (checking)` record) persists them, so a refused done stays a pre-effect refusal (#254). The missing-issue `next` step says how to supply one; the blocker text stays exact for gate matching.
|
|
4905
|
+
- **Safety.**
|
|
4906
|
+
- A first link keeps every other receipt field, so red-first and local-gate records survive.
|
|
4907
|
+
- An existing or foreign identity is never relinked.
|
|
4908
|
+
- PII tasks are skipped before any read.
|
|
4909
|
+
- A task that turned private during the read gets no link, and a PR read for one commit or branch is never linked to a task whose head changed meanwhile.
|
|
4910
|
+
- Plan-review and groom links record `lifecycle linked` with the ids only; a done link is part of the done's own record.
|
|
4911
|
+
|
|
4912
|
+
## Amendment: the limit ledger (#683 U1)
|
|
4913
|
+
|
|
4914
|
+
- Additive events in `events.jsonl`, no `PROTOCOL` bump, ignored by a reader that does not know them:
|
|
4915
|
+
- `usage` with `source: "codex_appserver"` (`reasoningTokens` is new on `usage`): written beside the unchanged `tokens` event from `thread/tokenUsage/updated`, as the growth of Codex's `input`, `cachedInput`, `output` and `reasoningOutput` counters since the previous update, under the existing baseline rule (a fresh thread counts from zero, a resumed one from its replayed total). Cached input is part of input and reasoning part of output, so `inputTokens + outputTokens` equals the `totalTokens` growth. Task attribution is the one `tokens` uses.
|
|
4916
|
+
- `budget_pause` and `budget_resume` (`peer`, `reason`, `window`, `resetsAt`, `by: "hub" | "person"`), written at the single pause INSERT and the single resume DELETE in `budget.ts`; an override is `by: "person"`. The pause window is the worst window that crossed the gate.
|
|
4917
|
+
- `quota_reset` (`peer`, `window`, `cause`, `resetsAt`, `previousResetsAt`, `used`, `previousUsed`), derived from two readings of one window id: `resetsAt` moved forward by more than 15 minutes (a provider's `resetsAt` wobbles by seconds inside one window: replaying the 177k real readings of this project's log first gave 85 entries, 31 of them jitter, and 54 with the tolerance, the smallest real move being 2.4 h), or `used` fell by 0.5 or more with a `resetsAt` within that tolerance. One entry per peer, window and `resetsAt`, also across a restart (the daemon seeds the dedupe set from the log). A reading older than the last one never counts.
|
|
4918
|
+
- Ids, windows and numbers only: no checkpoint text, summary, task title or agent-written text. A ledger failure never costs a pause, a resume or a reading.
|
|
4919
|
+
- Ceiling: `ahub reset` clears `budget_pauses` rows and, by its own contract and test, leaves `events.jsonl` byte for byte as it was, so a pause it clears has no `budget_resume`; a timeline must treat a `budget_pause` with no later resume for that peer and a later `budget_pause` or hub start as ended. A new pause that replaces an expired record writes the old pause's `budget_resume` first, so two open pauses never coexist.
|
|
4920
|
+
- Ceiling: Codex 0.163.0's `TokenUsageBreakdown` also has `cacheWriteInputTokens`, not read here (0 in the recorded session replayed in `docs/events.md`). A first reading after a restart is a baseline, so a window that rolled while the hub was down is not recorded as a reset (the pause record's own restore note covers a pause). The roll-up, the `usage` query op, the CLI and the console and dashboard views are slices U2 to U4.
|
|
4921
|
+
|
|
4922
|
+
### Dogfood scorecard (#687)
|
|
4923
|
+
|
|
4924
|
+
`ahub report --dogfood [--since 7d] [--json]` is a read-only projection with the same authority as `ahub report`, including agent callers (the 2026-10-22 correction to task #369's original person-only line). It never invokes a model or creates an issue/task. It reports flow, quality, attributed cost, delivery, hub performance and one evidence row per `docs/dogfood.md` feature. Export metadata only; count PII without text, and skip its known PR readbacks. Preserve unknown rather than zero for missing, malformed, truncated or uninstrumented sources.
|
|
4925
|
+
|
|
4926
|
+
Use `summarizeByTask`, `costMetrics` and bounded SDLC GitHub helpers. Bound files, rows, history cells, GitHub pages and total read time. Aggregate multi-task PR readiness at the final linked approval. Expose original first-run CI only when observed, never infer it from a rerun. Count bot-thread timing and explicitly cross-referenced bug issues as associations, not severity/causality. Retained journal counts are labelled as retained; do not infer first delivery from its latest transition. Log-pattern observations and burst-level snapshot notices are labelled separately from exact telemetry. The weekly receipt records its source commit, window and coverage under `docs/verification/dogfood/`; `docs/dogfood.md` specifies the bounded collector and current unknowns.
|
|
4927
|
+
|
|
4928
|
+
## Amendment: the usage roll-up, the `usage` op and `ahub usage` (#683 U2)
|
|
4929
|
+
|
|
4930
|
+
- `UsageRollup` (`src/hub/usage-rollup.ts`) folds `events.jsonl` into `hub.db` from a stored byte offset: `usage_rollup`, `token_rollup`, `limit_events`, `rollup_state`, `rollup_seen`, `rollup_pii`. A pass is bounded (8 MiB), whole lines only, and commits rows and offset in one transaction. A rebuild from byte 0 equals the incremental result; a log shorter than the offset starts over. Records are counted once by peer, source and id.
|
|
4931
|
+
- The `usage` control message (`op` `query`, `limits` or `total`; `by` one of `peer|task|issue|pr|release|day|model`; `since`, `until`, `peer`) is answered for the console and for a configured planner (not during that planner's private turn); every other agent gets `{ ok: false }`. Every message gets a reply.
|
|
4932
|
+
- Every number is `{ value, source }`, source `native`, `count`, `manual` or `unknown` (the labels of #285). A counter no record carried is `unknown`, never 0. `total` and `totalWithoutCache` are both returned and cache reads are their own column. Claude's total is the sum of its four counters when it reports none; no total is derived where cache is part of input. A lifetime total mixing native and counted peers is `count`.
|
|
4933
|
+
- `issue`, `pr` and `release` are the board's current `sdlc.issue`, `sdlc.pr` and `release` of each task, read per query. A private task shows `#id` and `[pii]`.
|
|
4934
|
+
- Review rules (#702): a lifetime overall figure with an unknown peer has source `partial` (a lower bound) and `unknownPeers` names them; it is never a sum that counts them as 0. `usage limits` returns `{ total, shown, truncated, omitted, limit, maxLimit }` and the CLI says when older events are cut (`--limit`, 1 to 5000, default 200); it never drops rows silently. A pause the events never ended and the hub no longer holds (`ahub reset`) is `endedUnknown`, not `open`.
|
|
4935
|
+
- `ahub usage`, `usage limits` and `usage total` (`--json`, `--since`, `--by`, `--peer`, `--limit`); the command is `allowed` for agent shells and the hub refuses non-planner agents. `docs/verified.json` restamps `src/cli/help.ts`.
|
|
4936
|
+
- Evidence on this project's real log (47 MB, 238k lines): first fold about 170 ms, `ahub usage` about 95 ms, per-task counted tokens equal `ahub report --by task` for all 170 tasks that have any, and Claude's derived total equals the hub's own count for Claude exactly (2,149,669,788). The one place `usage` is deliberately fuller than `report`: the report shows Claude's total as unknown when no `totalTokens` was reported (9 of those 170 tasks), while `usage` derives it from the four counters. The first version read Claude's total as unknown because 5,649 of its 5,656 real records carry no `totalTokens`.
|
|
4937
|
+
- Ceilings: the day is the UTC day of the event time; a pass is capped at 8 MiB and a log far past that needs several 10 s polls to catch up (reported as `done: false`, never as a failure); the dashboard and console views are #683 U3 and U4; `manual` appears in the type but nothing writes it yet (`ahub budget set` readings are quota windows, not token counts).
|
|
4938
|
+
|
|
4939
|
+
## Amendment: the console Usage panel (#683 U3)
|
|
4940
|
+
|
|
4941
|
+
- Tab `7 Usage` in the console (`src/cli/console-usage.ts` for the text, `console-state.ts` for the reducer and render, `console.ts` for the fetch). Key `7` and the digit guards of `1`..`6` apply; Left and Right from `7` behave as from Settings (wrap to `1` and to `5`), so the pinned `1`..`5` cycle (#309) is unchanged.
|
|
4942
|
+
- The refresh loop asks the `usage` op only while the panel is open: `by: "peer"` for the UTC day, for the last seven UTC days and for lifetime, `op: "limits"` with `limit: 1000`, and `by: "peer", task: <id>` for the task that was selected in Tasks when `7` was pressed. A refusal or an error is shown as `usage unavailable: <reason>`, never as empty usage.
|
|
4943
|
+
- Columns: PEER, TODAY, 7D, LIFETIME, PAUSE, QUOTA, LAST RESET. At 80 columns only LAST RESET gives way. The header drops one space per tab so seven tabs fit beside the project name.
|
|
4944
|
+
- A peer's tokens are the provider's total when it reported one, otherwise the hub's own count (labelled `hub`), otherwise `unknown`. A period with no records reads `no records`. Enter shows input, output, cache read and cache write each with its source.
|
|
4945
|
+
- `UsageQuery.task` (the roll-up and the op, a positive task id, refused with an answer otherwise) filters to one task; the result says `taskPii` for a private task, so the console prints `#id [pii]` and never a title.
|
|
4946
|
+
- The `usage` op now folds the log with `catchUp(250)` before it answers (one 8 MiB pass per poll left a hub that had just started on a big log answering from the first pass: found by the live panel on a copy of the real 47 MB log, where TODAY read `no records` for peers that had used tokens). When the budget runs out the reply has `behind: true`; the console says `still catching up` and the CLI warns on stderr.
|
|
4947
|
+
- Ceiling: the limits history is the newest 1000 events; the panel only needs each peer's last three resets. The dashboard panel is slice U4 and calls the same op.
|
|
4948
|
+
|
|
4949
|
+
## Amendment: the dashboard Usage view (#683 U4)
|
|
4950
|
+
|
|
4951
|
+
- The Usage view is part of the Agents view, under Usage budgets (`src/ui/index.html`, `renderUsage`), and reads the same roll-up as `ahub usage` and the console panel: lifetime totals with the peers whose figure is unknown, the last 14 UTC calendar days of stacked tokens per peer, the open budget pauses with the last 20 pause, resume and reset events, and a task table (state, owner, PR, input, output, cache read, cache write, total).
|
|
4952
|
+
- `uiSnapshot` builds `usage` only when the request names `view: "agents"` (the page sends its open view with every poll, like it sends `memory`), so a hidden view costs no roll-up work on the 1 s poll. It folds the log with `catchUp(100)`; when that returns false the snapshot carries `usage.behind: true` and the view says the figures are a lower bound until it catches up, never the folded part as the whole.
|
|
4953
|
+
- `UsageRollup.daily(days, now)` returns the last `days` calendar days ending today, clipped to the first day with a record. A day with no record is a row with `empty: true` and the view shows `no record`, never zero. Every counter keeps its source (`native`, `hub` for a count, `at least` for a partial sum) and `unknown` stays the word.
|
|
4954
|
+
- A private task is the public title `[pii]` on the board (`Tasks.publicView`) or a `pii` roll-up row; the view shows `#id [pii]` and never a title, and the snapshot carries ids and numbers only.
|
|
4955
|
+
- Colours are design tokens only; the view is one column at 600 px and its tables and chart scroll inside themselves. `test/ui-usage.test.ts` runs the page's own `renderUsage` over a snapshot fixture (PII marker absent, unknown not zero, the lower-bound note) and checks the view's token pairs for contrast in both themes.
|
|
4956
|
+
- Ceiling: the view recomputes the daily and task aggregates on each poll while the Agents view is open; a materialised daily table is the upgrade if that ever costs more than a few ms on a large log.
|
|
4957
|
+
|
|
4958
|
+
## Amendment: the console changed-symbols block (#684 phase 2b)
|
|
4959
|
+
|
|
4960
|
+
- `src/cli/console-symbols.ts` (pure data) and `taskFields` in `console-state.ts`: Enter on a task in Tasks adds `symbols` and `symbols note` after the plan field and drops the raw `codeSymbols` field. It reads the public `codeSymbols` that `task_show` already carries (#684 2a) and the task's `plan.symbols`; it asks the hub for nothing new.
|
|
4961
|
+
- The plan names are `plan.symbols`, the list the review brief reads as `planned` (`daemon.ts` passes `task.plan?.symbols` to `withCodeSymbols`), not a second source.
|
|
4962
|
+
- Each observed symbol is `matching` (some plan name matches it) or `unplanned`; each plan name that no observed symbol matches is `untouched`. The rule is `planSymbolMatches`, exported from `src/hub/review-brief.ts` and used by the review brief itself, so the console and the brief cannot drift.
|
|
4963
|
+
- States: no observation or an `unknown` one reads `changed symbols: unknown (no code facts)`; `known` and empty reads `none observed` (with a plan, every planned name is `untouched`); an incomplete observation says `incomplete` and that an untouched symbol may have changed; a private task (`pii` or `pii_path` signal, or a `[pii]` title) shows no block and no raw observation. Only a task_show-shaped detail (numeric id and a state) gets the block, and only the public receipt: a `codeSymbols` that still carries the stored `repositoryKey` was never re-screened by `publicTaskCodeSymbols`, so it reads as unknown and shows no names, whatever feeds the renderer.
|
|
4964
|
+
- Names go through the console's field renderer, so they are quoted; at most 40 are listed and the rest are counted. The note says deleted definitions are not reported.
|
|
4965
|
+
- Not in the original 2b slice: overlap lines (the read-only `symbolOverlaps` projection and console block follow in #379 above) and the related-notes count (phase 4, no hub op returns it).
|
|
4966
|
+
|
|
4967
|
+
## Amendment: reserved backlog pulls offer their exact owner (#718)
|
|
4968
|
+
- A planner pull of an existing backlog task with `reserved` set admits that exact peer before any task write. It cannot silently fall back to another peer when the reserved peer is excluded, unavailable, at planner WIP capacity or otherwise ineligible. Existing planning authority, revision, dependency, release, person-order and SDLC plan gates still apply. Authority, routing/WIP eligibility and plan/flow fences are rechecked after asynchronous screening or plan readbacks and immediately before the admitted write.
|
|
4969
|
+
- `Board.pullAssigned` records `backlog pulled` and the owner assignment in one atomic task transition. Both writes commit or roll back together; board reads and deferred callback payloads both expose the committed assigned row, while history retains both events. Pull increments the planning revision, clears any old `offer`, and the existing assignment path creates a fresh owner offer bound to the new assignment flow revision. A stale prior owner request is withdrawn through the existing delivery path rather than reused as the new offer.
|
|
4970
|
+
- The committed assignment remains fact if subsequent routing notice or task delivery fails. Existing typed undelivered-assignment handling reports that failure; an assignment superseded while awaiting delivery follows the existing supersession handling, without rollback or a second owner offer. This change adds no separate delivery mechanism.
|
|
4971
|
+
- An unreserved backlog pull retains its lazy, ownerless To Do transition and existing planning undo. A reserved pull now assigned/offered to its peer cannot be undone through planning; the existing planner-or-person untouched-return path remains the recovery route when its own conditions hold. Acceptance is still a separate peer action and does not occur as part of pull.
|
|
4972
|
+
- No new model call, role authority, configuration or privacy exception is introduced. Existing screened task/note views, PII routing and refusal boundaries apply. Dogfood starts after release and checks the same-peer `backlog pulled` -> `assigned` -> `accepted` sequence, fresh offer binding, stale/unowned offers and capacity refusals.
|
|
4973
|
+
## Amendment: the dashboard task sheet's Code group (#684 phase 2c)
|
|
4974
|
+
|
|
4975
|
+
- `src/hub/task-code-group.ts` (`codeGroup`) classifies a task's public `codeSymbols` against `plan.symbols` with the review brief's own matcher (`symbolsOverlap`) and is the only place that does for the dashboard (the console classifies with `classifySymbols` in `console-symbols.ts`, over the same matcher); the UI snapshot carries the result as `codeGroup` on each task. The page (`codeGroupSection` in `src/ui/index.html`) draws it and never matches names.
|
|
4976
|
+
- A private task has no `codeGroup`, so the sheet shows no Code group for it and the snapshot carries none of its plan names. A stored receipt (one that still has `repositoryKey`), a missing one or an `unknown` one is `{ state: "unknown" }` and the sheet reads `unknown (no code facts)`. Symbol ids that fail `publicMetadataText` or the id shape (`symbol:<name>@<path>`) are dropped and the group is marked incomplete, so an untouched symbol may have changed. Every stored plan name is read (the stored plan allows 50 names of 300 characters, and the group uses the same bounds); one that is malformed or fails the screen is counted in `planWithheld`, the group is marked incomplete, and the sheet says a symbol marked unplanned may be in the plan, so a withheld name never shows a plan departure that did not happen. The counts on the sheet are the totals, not the listed part.
|
|
4977
|
+
- 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
|
+
- 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
|
+
- Colours are design tokens only, and the group wraps at phone width with 44 px targets.
|