@staix/agent-hub 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +19 -7
- package/docs/agent-notes/README.md +4 -0
- package/docs/agent-notes/adapters.md +18 -4
- package/docs/agent-notes/budget.md +5 -4
- package/docs/agent-notes/bus.md +1 -0
- package/docs/agent-notes/daemon.md +19 -7
- package/docs/agent-notes/tasks.md +15 -7
- package/docs/agent-notes/tests.md +4 -4
- package/docs/dogfood.md +18 -10
- package/docs/events.md +21 -7
- package/docs/operations.md +131 -27
- package/docs/profiles.md +1 -0
- package/docs/quickstart.md +13 -3
- package/docs/sdlc.md +40 -2
- package/docs/security.md +45 -14
- package/docs/specs/2026-09-19-agent-hub-design.md +112 -10
- package/docs/verification/2026-10-22-codex-reviewer-isolation.md +14 -0
- package/docs/verification/2026-10-23-0.14.0.md +69 -0
- package/docs/verification/2026-10-23-0.15.0.md +80 -0
- package/docs/verification/2026-10-23-daemon-responsiveness.md +44 -0
- package/docs/verification/2026-10-23-headless-claude-permissions.md +47 -0
- package/docs/verification/2026-10-23-terminal-claude-execution-root.md +41 -0
- package/docs/verified.json +34 -17
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +1078 -327
- package/src/adapters/acp.ts +43 -7
- package/src/adapters/claude-channel.ts +26 -4
- package/src/adapters/claude-implementer.ts +243 -0
- package/src/adapters/claude-print-child.ts +90 -0
- package/src/adapters/claude-task-sessions.ts +80 -0
- package/src/adapters/codex-review-isolation.ts +18 -7
- package/src/adapters/compat.ts +4 -0
- package/src/adapters/conformance/enrollment.ts +2 -1
- package/src/adapters/conformance/fakes/acp-server.ts +3 -1
- package/src/adapters/definitions/builtin-metadata.ts +15 -3
- package/src/adapters/definitions/native-channel-state.ts +1 -1
- package/src/adapters/definitions/native-cli-entry-claude.ts +13 -8
- package/src/adapters/definitions/native-cli-entry-codex.ts +13 -0
- package/src/adapters/definitions/native-cli-entry-types.ts +2 -0
- package/src/adapters/definitions/native-factories.ts +8 -0
- package/src/adapters/kimi-wire-usage.ts +303 -0
- package/src/adapters/registry-types.ts +7 -3
- package/src/adapters/registry.ts +14 -2
- package/src/adapters/reviewer-print.ts +64 -60
- package/src/cli/console-config.ts +123 -0
- package/src/cli/console-state.ts +28 -1
- package/src/cli/console.ts +25 -1
- package/src/cli/facts-hook.ts +9 -5
- package/src/cli/help.ts +6 -4
- package/src/cli/launch.ts +10 -4
- package/src/cli/main.ts +52 -9
- package/src/cli/output.ts +75 -2
- package/src/cli/reset.ts +1 -1
- package/src/cli/settings-edit.ts +89 -0
- package/src/cli/status-lines.ts +1 -1
- package/src/cli/terminal-recovery.ts +1 -6
- package/src/hub/action-enforce.ts +82 -0
- package/src/hub/action-policy.ts +22 -10
- package/src/hub/board.ts +48 -3
- package/src/hub/budget.ts +5 -0
- package/src/hub/bus.ts +5 -5
- package/src/hub/claude-launch-proof.ts +33 -0
- package/src/hub/config-apply.ts +5 -0
- package/src/hub/config-edit-host.ts +173 -0
- package/src/hub/config-edit.ts +282 -0
- package/src/hub/config-tier.ts +89 -0
- package/src/hub/control-client.ts +24 -3
- package/src/hub/daemon.ts +450 -71
- package/src/hub/deliverable-files.ts +131 -0
- package/src/hub/deliverables.ts +125 -0
- package/src/hub/delivery-journal.ts +18 -2
- package/src/hub/events.ts +3 -1
- package/src/hub/hub-tools.ts +21 -4
- package/src/hub/instances.ts +19 -6
- package/src/hub/kimi-usage.ts +144 -0
- package/src/hub/manager.ts +3 -3
- package/src/hub/peers.ts +7 -0
- package/src/hub/permission-payload.ts +15 -0
- package/src/hub/routing.ts +118 -3
- package/src/hub/sdlc-metrics.ts +260 -0
- package/src/hub/sdlc.ts +5 -3
- package/src/hub/settings.ts +1 -1
- package/src/hub/spec-read.ts +31 -0
- package/src/hub/task-code-group.ts +52 -0
- package/src/hub/tasks.ts +374 -37
- package/src/hub/token-window.ts +33 -0
- package/src/hub/ui.ts +3 -0
- package/src/hub/usage-rollup.ts +74 -6
- package/src/hub/workflow.ts +16 -0
- package/src/ui/index.html +388 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,36 @@ Issue and pull request numbers in the entries for 0.7.7 and earlier refer to the
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.15.0
|
|
8
|
+
|
|
9
|
+
- `ahub backlog list` prints a table, one line per card by rank (RANK, ID, PRI, RESERVED, AFTER, CLASS, AGE, TITLE, with titles cut to the terminal width), and a count by reserved owner; `--full` and piped output keep whole titles, `--json` prints the raw listing (#399).
|
|
10
|
+
- Deliverable records bound to content hashes (#509 AC1, AC2, AC4): `hub_task_done` and `hub_sdlc_record` accept `deliverables: [{path, format, version?}]`. The hub resolves each path with `realPath` and `guardPath`, hashes the file itself (a claimed hash is refused) and records the sha256 and size; it works in a plain folder with no git, PR or snapshot. A reviewer's approval is refused with `deliverable changed since it was delivered` when a file moved, the person may approve the current bytes, and `ahub task show` reports `changedSinceReview` and each file as `unchanged`, `changed`, `missing` or `unknown` (never `unchanged` for a file the hub cannot read). A PII task or a `pii.paths` file shows id, hash and size only. The `ahub deliverables` listing and the dashboard column follow in the next slice. Files are read under the owner's execution root (a declared worktree instance's own verified root); an unproven root refuses, and a read of it reports `unknown`, never `unchanged`.
|
|
11
|
+
- Acceptance criteria with ids in review (#507, AC3): a task's recorded criteria checklist (the #481 S8 `criteria` receipt, no second model) is listed in the review request with each id and result, and a `hub_review` unmet item that starts with a recorded id (`AC2: what is missing`) is recorded on the verdict as `unmetCriteria`, shown in the task history and public view. The ids are read from the board's checklist at the verdict, never from the caller; a PII task lists and records none, and a task with no checklist gets exactly the request it got before.
|
|
12
|
+
- Planner walk and reviewer independence in `assign()` (#525 slice 525-3): `assign()` gains a pure planner walk over the settled planner holders (rotation by task id over every holder, never renumbered by availability, a named planner exact, a recorded planner kept, a PII task none) and returns `Assignment.planner` and `plannerReason`; `ahub route explain` prints the same walk. A peer that accepted or finished a task never reviews it (I-2, including a named reviewer and the declared reviewer pool): when no other candidate is left the person reviews. Nothing records the planner yet, so task behaviour is unchanged until 525-2.
|
|
13
|
+
- Headless Claude implementer instances (#525-4) run one owned child per turn in a declared worktree, persist a separate session per task and use current-child-bound person approvals in ask or ask-when-needed mode; unscoped chat without one active task stays ephemeral. Native permission transport was verified model-free on Claude Code 2.1.309; real account/model interaction remains a live leg.
|
|
14
|
+
|
|
15
|
+
- Terminal Claude implementer instances (#525-5) start in their declared managed worktree and own write tasks only after the daemon verifies the current launch record and channel cwd; facts and terminal recovery retain the verified execution root.
|
|
16
|
+
|
|
17
|
+
- Fix #719: keep periodic delivery reads independent of terminal payload history, time-slice usage folding and retry transient native launch connections within a bounded budget.
|
|
18
|
+
- Always-ask actions are enforced for Claude and ACP peers (#636 slice 2): the Claude PreToolUse hook returns `ask` for a classed call (publish, external-write, destroy, install) in every permission mode, `never-ask` included; a hub-owned ACP peer's `session/request_permission` for a classed call is never auto-approved, not even under `--unattended`, carries `Action approval (<classes>): <command>` built from the task's public view and the PII screen, and offers the per-session class budget as its own choice. `ahub status` says per peer whether the call is seen before it runs: a Kimi in `yolo` or `auto` mode, a Claude session without the hook and an `--unattended` Claude launch are listed as not enforceable, never silently claimed. Codex, Pi and the local worker follow in slice 3 (#313).
|
|
19
|
+
- Fix #736: a review finding carried from a task to its follow-up on the same PR or branch (an approve-as-step follow-up, a superseded task) is no longer proposed as a recurring finding; the same finding on separate work still writes one planning card.
|
|
20
|
+
- Fix #735: until slice 525-5, a person-started Claude channel instance owns no task of any write class, not only implement, test and bulk_edit: the refused set is derived from the class's owner role (`ownerRoleOfClass`), so `summarize`, `triage` and any later class are refused too, named or automatic, and `ahub route explain` names the refusal. The spec records that a configured ACP agent may hold `planner` but can neither be pinned as planner nor chosen as the automatic planner (it has no host projection).
|
|
21
|
+
- Kimi's consumption is counted from its session wire log (#285 phase 2): `budget.kimi_tokens_5h` finally has a counter. The reader (`src/adapters/kimi-wire-usage.ts`, wire protocol 1.5 as written by Kimi Code 2.3.0) reads each session's `agents/*/wire.jsonl` incrementally, sub-agents included, and writes a `usage` event (`source: kimi_wire`) and a `tokens` event per model request. The `tokens` window counts input, cache writes and output over 5 h; cache reads are recorded and not counted. Anything the reader cannot trust is unknown, with one notice, never a short count. Kimi's ACP `usage_update` no longer feeds a token total. Not yet verified live against Kimi's own `/usage` (#285 AC11).
|
|
22
|
+
- Structural configuration from the console and the dashboard (#689): instances (`kind`, `roles`, `start_mode`, `review_mode`, `readOnly`, `worktree`, `permission_mode`), `review`, `snapshots.keep`, `approvals.timeout_s`, `context.gate` and `sdlc.toml` `mode` and `required_checks` are edited with `ahub settings edit`, the console Settings key `c` and a dashboard Structure group in a settings session. The hub validates with its own loaders and shows the loader's reason at the field, shows a diff, checks the file revision twice, keeps the newest five backups (`ahub settings backups`), writes atomically, records a `config_edit` event with hashes only and lists a changed instance or `sdlc.toml` as restart pending (console `R` prefills `restart`). Executable and secret-locating keys (`kimi_cmd`, `omniroute`, `terminal`, an instance's `jcode_home` and `model`, and the rest of `NEVER_EDITABLE`) stay console only and need their name typed; the dashboard shows them as text with the command, and the hub refuses a forged request for one. A fresh `codex-review` reviewer missing `start_mode` shows the loader's reason and writes nothing.
|
|
23
|
+
|
|
24
|
+
## 0.14.0
|
|
25
|
+
|
|
26
|
+
- Fix #724: `ahub board` at a terminal width prints one line per task: TITLE cut with `...`, an AFTER column for dependencies, a compact STAGE (meter, stage and a next-step code such as `intent: issue?`) and a legend line. `--full` and piped output keep the wrapped layout with the whole SDLC advice; `--json` is unchanged.
|
|
27
|
+
- The dashboard task sheet gets a "Code" group (#684 phase 2c): the task's changed symbols as buttons marked `matching` or `unplanned` against its plan, the plan names nothing changed under as `untouched`, and `unknown (no code facts)` without facts; a private task shows none. The hub classifies them (`codeGroup` in the UI snapshot) and a symbol opens the Memory tab with its canonical `symbol:<name>@<path>` node as the graph start.
|
|
28
|
+
- SDLC metrics and control bands (#481 S7): `ahub sdlc metrics [--since]` and the planner tool `hub_sdlc_metrics` report time from acceptance to plan, first review and merge, first-pass approval and rework rounds, the first-pass completion check, same-head CI reruns, stale-head re-reviews (a verdict bound to a different head than the review requested) and stalls from board history, with `unknown` for anything the board cannot derive. An optional `[bands.<metric>]` table in `sdlc.toml` sets a rolling baseline (mean and standard deviation, a per-metric deviation floor, a minimum step and a drift rule); a breach episode is a hub notice (`log`, remembered across restarts), one read-only triage backlog task (`diagnose`) or one backlog card naming the metric and its evidence (`propose`), once per episode and never while the metric has an open band card. The evaluator runs every five minutes when the board changed, and not while the hub is stopping, recovering or has a storage error. No model runs in detection and no GitHub issue is created.
|
|
29
|
+
- Fix #723: the hub links a task's SDLC issue identity when the task is proposed or accepted (advise and enforce), from its title and refs with no GitHub read, so `issue identity missing` no longer waits for a groom that times out on a large board. A title that qualifies the sibling repository `refs.repo` names (`agent-mem#13`, `owner/repo#N`) now links to it; with no `refs.repo` the propose result suggests setting it. The groom backfill runs first and bounded (32 links, 1 s) before the slow metadata reads.
|
|
30
|
+
- The dashboard gets a Usage view in its Agents view (#683 U4): lifetime totals, the last 14 UTC days of stacked tokens per peer (a day with no record is a gap, never zero), the open pauses and recent limit events, and a task table with a private task shown as `#id [pii]`. It reads the roll-up of `ahub usage` only while the Agents view is open, says the figures are a lower bound while the roll-up is still behind a large log, and keeps `unknown` as the word.
|
|
31
|
+
- Fix #722: a fresh reviewer whose per-review start fails now names a closed cause class (`version`, `binary`, `mcp_inventory`, `plugins_hooks_apps`, `sandbox_proof`, `credential_root`, `timeout`, `unknown`) in the unavailable reason, in hub.log and as the `reviewer` line of `ahub status`, instead of one generic sentence. The class never carries a path, native CLI text or a token; a credential root is named by its path class only. Model-free reproduction of the Codex 0.163.0 reviewer proof under an isolated HOME and CODEX_HOME passed on this machine with empty and synthetic ambient configs, so the failing leg is now readable from the log instead of guessed.
|
|
32
|
+
- Roles capability per kind and form (#525-1): a Claude channel instance may declare `implementer` or `verifier` and then requires its own `worktree` (a planner or reviewer instance still reads the project checkout); a headless Claude instance stays reviewer-only and a headless implementer is refused until #525-4; `planner` is refused on kinds that cannot plan (pi, local, agy) in instance declarations and dropped from `roles` with the ceiling notice.
|
|
33
|
+
- Fix #720: `ahub codex --as <id>` for a headless reviewer instance (`start_mode: "headless"`, `review_mode: "fresh"`) sends the hub `start`, prints the result and returns, as the Claude entry does, instead of failing at terminal recovery after the start succeeded. TUI-only flags are refused for it; the default `ahub codex` TUI path is unchanged.
|
|
34
|
+
- Fix #718: a planner pull of a reserved backlog task now commits the pull and the owner assignment together and offers the task to its exact reserved peer, so no ownerless task with a stale offer is left; an offline, off-roster, paused, full or excluded reserved peer refuses the pull with its reason and no fallback, and an unreserved pull stays lazy.
|
|
35
|
+
- Releases: 0.14.0 includes features, so the minor is bumped.
|
|
36
|
+
|
|
7
37
|
## 0.13.0
|
|
8
38
|
|
|
9
39
|
- Add the #636 action-policy foundation: a pure classifier, machine-local always-ask configuration, session-bound approval budgets and explicit per-peer enforcement status. Adapter pre-tool gates remain pending; status reports that limit instead of claiming protection.
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Code, Codex, Kimi Code, Pi and a hub-owned local worker (plus Qwen Code, Jcode a
|
|
|
5
5
|
Antigravity if you want them) share a message bus, a task board and a memory, each
|
|
6
6
|
connected through its own native interface.
|
|
7
7
|
|
|
8
|
-
Status: 0.
|
|
8
|
+
Status: 0.15.0, control protocol 16. Bun + TypeScript, one loopback daemon per
|
|
9
9
|
project. Site: [agent-hub.staix.net](https://agent-hub.staix.net).
|
|
10
10
|
|
|
11
11
|
## Quick start
|
|
@@ -36,7 +36,14 @@ ahub ui # the web dashboard
|
|
|
36
36
|
restarts.
|
|
37
37
|
- **A shared task board.** Work is proposed, accepted, done and reviewed through hub
|
|
38
38
|
tools. Roles split planning, implementation and review, `routing.toml` picks
|
|
39
|
-
owners by task class, and owners of overlapping work are told what changed.
|
|
39
|
+
owners by task class, and owners of overlapping work are told what changed. A
|
|
40
|
+
peer that accepted or finished a task never reviews it, a review request lists
|
|
41
|
+
the task's acceptance criteria by id, and a done report can name deliverable
|
|
42
|
+
files that the hub hashes itself, so an approval binds the bytes reviewed. Extra
|
|
43
|
+
Claude Code instances can implement in their own worktree, in a terminal or
|
|
44
|
+
headless with one session per task. `ahub sdlc metrics` reads lead times,
|
|
45
|
+
rework and stalls from board history, and
|
|
46
|
+
optional control bands turn a breach into a backlog card.
|
|
40
47
|
- **Shared memory.** Decisions, contracts, fails and findings are kept through the
|
|
41
48
|
first-party [`@staix/agent-mem`](https://github.com/STAIxBWLB/agent-mem) library
|
|
42
49
|
(short- and long-term tiers, a typed graph, git sync between machines). Agents get
|
|
@@ -45,12 +52,17 @@ ahub ui # the web dashboard
|
|
|
45
52
|
through an authenticated relay. Tasks marked as PII stay on the on-prem worker.
|
|
46
53
|
- **Guard rails.** The local worker and Pi run in a sandbox, and their writes and
|
|
47
54
|
shell commands wait for your approval unless you choose a looser permission mode.
|
|
48
|
-
|
|
55
|
+
Publish, external-write, destroy and install calls ask a person in every mode for
|
|
56
|
+
Claude Code and hub-owned ACP peers such as Kimi, wherever the hub sees the call
|
|
57
|
+
before it runs; `ahub status` names each peer where it cannot. Budgets pause an
|
|
58
|
+
agent before it runs out of quota, and recovery never replays an
|
|
49
59
|
effect whose outcome is unknown.
|
|
50
60
|
- **Operations.** A terminal console and a local dashboard, one hub per repository or
|
|
51
|
-
worktree with a machine-wide view, live settings reload,
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
worktree with a machine-wide view, live settings reload, structural config
|
|
62
|
+
(instances, review, lifecycle) edited with a validated diff and a backup from the
|
|
63
|
+
console or a dashboard settings session, token usage and quota
|
|
64
|
+
history (`ahub usage` and the dashboard's Usage view), and `ahub upgrade`, which
|
|
65
|
+
carries running sessions across versions.
|
|
54
66
|
|
|
55
67
|
## Upgrading
|
|
56
68
|
|
|
@@ -78,7 +90,7 @@ uses the home must run a version that understands the new layout.
|
|
|
78
90
|
[#258](https://github.com/STAIxBWLB/agent-hub/issues/258)).
|
|
79
91
|
|
|
80
92
|
Live checks that still need a real account or a person are listed in the
|
|
81
|
-
[release verification report](docs/verification/2026-10-23-0.
|
|
93
|
+
[release verification report](docs/verification/2026-10-23-0.15.0.md) and the
|
|
82
94
|
[smoke checklist](docs/smoke.md).
|
|
83
95
|
|
|
84
96
|
## Documentation
|
|
@@ -35,3 +35,7 @@ Background moved out of the area notes for 0.13.0:
|
|
|
35
35
|
- Jcode MCP entry (#685, adapters.md): jcode gives an MCP entry's own env precedence over the process env (probed on 0.99.0), so a peer id in the shared `.jcode/mcp.json` entry would make every extra instance act as `jcode`.
|
|
36
36
|
- Bulk queue discard (#713, bus.md): a queued row with no journal record of its own carries the journal's global revision, which every discard moves, so a revision read once at the start goes stale after the first discard.
|
|
37
37
|
- Usage roll-up (#683 U2, budget.md): a pass cap smaller than one log line once hung the fold.
|
|
38
|
+
|
|
39
|
+
Background moved out of the area notes for 0.15.0:
|
|
40
|
+
|
|
41
|
+
- I-2 as the last reviewer check (#744, tasks.md): placed earlier, it refused an offline former implementer as "implemented this task" instead of "offline", which turned a task that used to approve directly into a blocked person review.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Native adapters and spawned agents
|
|
2
2
|
|
|
3
|
-
Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` and `activity` events), peer turn state, approvals, and the agent processes the hub spawns and stops. Read before editing `src/adapters/`, `src/pi/`, `src/hub/peers.ts`, `src/hub/permission-mode.ts`, `src/hub/tool-refusal.ts`, `src/hub/child-process.ts`, `src/hub/lifecycle.ts` (shutdown deadline), `src/hub/start-mode.ts`, `src/hub/terminal-open.ts`, `src/memory/capture.ts` (session end), `src/hub/native-hook-launch.ts`, `src/hub/peer-catalog.ts`, the launcher-managed MCP entries (`src/cli/agy-mcp.ts`, `src/cli/kimi-mcp.ts`, `src/cli/jcode-mcp.ts`), `src/cli/configured-acp-entry.ts`, the version policy (`src/adapters/compat.ts`, `src/cli/agents.ts`) or the process-tree stop in `scripts/benchmarks/teardown.ts`, or before adding a native peer.
|
|
3
|
+
Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` and `activity` events), peer turn state, approvals, and the agent processes the hub spawns and stops. Read before editing `src/adapters/`, `src/pi/`, `src/hub/peers.ts`, `src/hub/permission-mode.ts`, `src/hub/tool-refusal.ts`, `src/hub/action-enforce.ts`, `src/hub/permission-payload.ts`, `src/hub/child-process.ts`, `src/hub/lifecycle.ts` (shutdown deadline), `src/hub/start-mode.ts`, `src/hub/terminal-open.ts`, `src/memory/capture.ts` (session end), `src/hub/native-hook-launch.ts`, `src/hub/peer-catalog.ts`, the launcher-managed MCP entries (`src/cli/agy-mcp.ts`, `src/cli/kimi-mcp.ts`, `src/cli/jcode-mcp.ts`), `src/cli/configured-acp-entry.ts`, the version policy (`src/adapters/compat.ts`, `src/cli/agents.ts`) or the process-tree stop in `scripts/benchmarks/teardown.ts`, or before adding a native peer.
|
|
4
4
|
|
|
5
5
|
- For conceptual architecture, definition fields, generic ACP config and the new-adapter review checklist, see [Adding an AI Tool](../adding-an-ai-tool.md). For definition interfaces, see [registry-types.ts](../../src/adapters/registry-types.ts).
|
|
6
6
|
|
|
@@ -8,14 +8,14 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
8
8
|
|
|
9
9
|
- Keep per-kind native policy in `src/adapters/definitions/` and consume it through the registry. Private CLI implementations accept only fixed host-bound ports; keep identity, audit, lifecycle admission and effect ordering in the host. Except for the verified person-owned A4 startup-module contract, do not add config-selected callbacks or runtime imports from registry leaves into eagerly initialized hub/CLI modules; use type-only ports for existing host planners.
|
|
10
10
|
- Adapter registry (#451): kind-specific policy lives in that peer's `AdapterDefinition` under `src/adapters/definitions/` (runtime definitions `NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN` in `builtin-metadata.ts` plus `qwen.ts`; the daemon constructs every native driver through `start.create` and `DAEMON_PORTS`; `BUILTIN_DEFINITIONS_WITHOUT_QWEN` is the frozen A1 export, read by no runtime consumer). Outside `src/adapters/`, read a semantic field of `definitionOf(id)`: never a raw peer-id comparison, a resolved kind (`kindOf`, `definitionOf(...).kind`) or a constant alias compared with a native name, a vendor-keyed object, a peer-name array, or a closed-list import from `peer-catalog.ts`, `start-mode.ts` or `permission-mode.ts`; the guard resolves aliases and import/re-export bindings. `test/registry-guard.baseline.json` is `[]` and stays empty: a new site moves into a definition, never into the baseline. The only exceptions are the cross-release literals `KNOWN_PEERS`, `OPTIONAL_PEERS` and `START_MODE_PEERS`, which `test/registry-guard.test.ts` holds equal to the derived lists. `LaunchContext.nativeCommand` carries only validated config argv (or its redacted preview), never model input; a definition's `permission.grants` text is what `permissionGrant` shows, ahead of the built-in wording. Keep native mode-id maps in `definitions/*-native.ts`, free of registry factories (`test/adapter-import-order.test.ts`).
|
|
11
|
-
- Registry load runs `assertDaemonEngine` and `assertNativeCapabilityScope` before publication. A definition that is not a fixed builtin (object identity in `fixedBuiltinDefinitions`, never a kind name) may not declare `start.acpCommand`, `attach.admission`, `budget.resetSoftTokensOnResume`, `session.crashIdentity`, a `pi-rpc` daemon port, `routing`, `projection`, `reviewerVariant`, `reviewer`, `hooks.factsHook`, `cli.entry`, `cli.preview` or `cli.usage`. Only `definitionOf(id, instances)` picks a kind's `reviewerVariant`, for a declared extra with `start_mode: "headless"`, roles exactly `["reviewer"]`, and either a variant whose `reviewer.declaration` is `"automatic"` (Claude, agy) or `review_mode: "fresh"` (Codex, Kimi); without that opt-in a reviewer-only Codex or Kimi instance keeps its native adapter. Metadata-only reviewer variants never establish native peer support; `isSupportedPeer` shares the registry runtime/validated-instance predicate with the catalog and daemon. A new field that depends on fixed-builtin assumptions or host-global native state joins that list (`test/adapter-capability-scope.test.ts`). `trust.onPrem` admits PII work (rule: `tasks.md`).
|
|
11
|
+
- Registry load runs `assertDaemonEngine` and `assertNativeCapabilityScope` before publication. A definition that is not a fixed builtin (object identity in `fixedBuiltinDefinitions`, never a kind name) may not declare `start.acpCommand`, `attach.admission`, `budget.resetSoftTokensOnResume`, `session.crashIdentity`, a `pi-rpc` or `claude-implementer` daemon port, `identity.currentChild`, `routing`, `projection`, `reviewerVariant`, `headlessVariant`, `reviewer`, `hooks.factsHook`, `cli.entry`, `cli.preview` or `cli.usage`. Only `definitionOf(id, instances)` picks a kind's `reviewerVariant`, for a declared extra with `start_mode: "headless"`, roles exactly `["reviewer"]`, and either a variant whose `reviewer.declaration` is `"automatic"` (Claude, agy) or `review_mode: "fresh"` (Codex, Kimi); without that opt-in a reviewer-only Codex or Kimi instance keeps its native adapter. It picks a kind's `headlessVariant` (Claude, #525-4) for a declared headless extra whose roles include `implementer` and hold only `implementer` and `verifier`. Metadata-only reviewer variants never establish native peer support; `isSupportedPeer` shares the registry runtime/validated-instance predicate with the catalog and daemon. A new field that depends on fixed-builtin assumptions or host-global native state joins that list (`test/adapter-capability-scope.test.ts`). `trust.onPrem` admits PII work (rule: `tasks.md`).
|
|
12
12
|
- Adding a native peer: declare its `AdapterDefinition` under `src/adapters/definitions/` (or its own directory, as agy) and list it in `ADAPTERS` (`src/adapters/registry.ts`). `KNOWN_PEERS`, `OPTIONAL_PEERS`, `SUPPORTED_PEERS`, `START_MODE_PEERS` (a kind with both a TUI and a headless form), `ROLE_ORDER`, `HELP_ORDER` and `PLANNER_PEERS` are derived from it by `deriveAdapterLists`: set `optional`, `projection` (`roleRank`, `startRank`, `helpRank`, `plannerEligible`; a configured or external definition cannot) and, for an `ahub <kind>` launcher, `cli.entry`, `cli.usage`, `cli.preview` and `cli.startWords`, never a list edit (`builtinCliEntries(definitions)` projects the entries). The literal cross-release copies `KNOWN_PEERS` and `OPTIONAL_PEERS` in `src/hub/peer-catalog.ts` and `START_MODE_PEERS` in `src/hub/start-mode.ts` stay hand-kept for older coordinators: `test/registry-guard.test.ts` fails when one differs from the derived list. Adding a peer is the one change that updates their probe counts and `VENDORS` in `test/helpers/registry-inventory.ts` (`REGISTRY_COMPATIBILITY_PROBES`), the probe literal in `test/adapter-registry-a2.test.ts`, the `ADAPTERS` order in `test/adapter-registry.test.ts` and the literal in `test/adapter-catalog-a2.test.ts`; say so in the PR as an expected test change. `isSupportedPeer` answers from `ADAPTERS`, and for a declared instance from its kind's runtime definition unless the instance resolves to an unavailable reviewer variant; `isKnownPeer` and `defaultPeerRoles` answer from `definitionOf`; `STARTABLE_PEERS` reuses `SUPPORTED_PEERS`.
|
|
13
13
|
- When an adapter lands, add its project config directories to `AGENT_CONFIG_SEGMENTS` (`src/hub/permission-mode.ts`); scoped automatic writes must ask there, while explicit approval and `never-ask` keep their existing behavior.
|
|
14
14
|
- Load external modules only at daemon startup from trusted untracked local declarations: verify the self-contained bundle and content pin before importing captured bytes. Keep CLI/reload paths metadata-only, modules restart-bound and never settings-editable, builtin capabilities restricted, and external-kind instances refused in A4 (follow-up #287). Export conformance registrations and fake assets without starting them on import.
|
|
15
15
|
- Bind scanned external runtime SDK imports to this daemon's own `/adapter` entry; never resolve them against project packages. Refuse `/conformance`, `bun:ffi`, `bun:test` and CommonJS loading at runtime. Isolate module refusals from hub startup and retain unknown module roster rows as inert when metadata is unavailable.
|
|
16
16
|
- Require the immutable captured project catalog at instance, roster, execution and permission resolver boundaries; choose a builtin catalog explicitly only for builtin-only callers; accept `adapters` only as wholly machine-local trusted data and require restart for changes. Refuse missing/mismatched pins, non-echoed load sessions, leader/attach argv and unknown owned-group evidence; require an explicit standalone `--no-leader` before argv termination for Grok (kind, executable or pinned native name); signal only the original verified group. Keep unmapped policy unmanaged, absent budgets exempt and read-only instances non-raisable through native acknowledgement plus host/MCP mutation gates. Claim native support only with its isolated T0 receipt.
|
|
17
17
|
- Consume quota/context through the resolved definition's declared readers; absence stays unknown. Use explicit `attach.externalHello` and `instances` policy for admission, never `identity.mcp`; keep runtime owner/root and reviewer isolation proof in the host. Never let configured `roles` include `conductor`. Keep native stderr, prompt-failure, permission-mode and transport error text out of configured-kind logs and turn-failure reasons. A configured CLI start (`src/cli/configured-acp-entry.ts`) accepts only `--mode headless`, `--as`, `--session-id` and one preview flag, and its preview never shows argv or session values.
|
|
18
|
-
- Validate coherence and conformance enrollment for reviewer variants too. Construct driven conformance peers through `start.create`; name dedicated coverage and concrete reasons for unsupported legs. Do not restore removed placeholder launch, lifecycle, init or permission metadata.
|
|
18
|
+
- Validate coherence and conformance enrollment for reviewer and headless variants too. Construct driven conformance peers through `start.create`; name dedicated coverage and concrete reasons for unsupported legs. Do not restore removed placeholder launch, lifecycle, init or permission metadata.
|
|
19
19
|
|
|
20
20
|
## Permissions, modes and approvals
|
|
21
21
|
|
|
@@ -95,6 +95,20 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
95
95
|
- Start fresh Kimi reviewers without Seatbelt, with private `KIMI_CODE_HOME`, managed-provider-only configuration, clean temporary cwd, explicit empty skills directory and exactly `mcp__agent-hub__hub_review` plus `mcp__agent-hub__hub_task_show` in its agent tools. Use directory symlinks for the person's native `credentials/` and `oauth/`, and link `device_id` to the native identity; do not copy token files, select static-API-key mode or set `KIMI_DISABLE_OAUTH_LOCK`. Let native OAuth share refresh/atomic rename/`proper-lockfile` coordination and the existing Kimi account budget. Revoke verdict authority before shutdown; allow bounded TERM grace while the OAuth lock is held, then use verified owned-group teardown. Report a forced-kill/re-login requirement without attempting host auth repair.
|
|
96
96
|
- Bind every running print review to its immutable envelope head and review revision. Recheck `Tasks.reviewBindingCurrent` for task-MCP operations, legacy print reads and the verdict authority after awaits; an old child never gains a replacement request. A committed verdict acknowledgement does not authorize subsequent reads or verdicts.
|
|
97
97
|
- `ReviewerPrintPeer` (`src/adapters/reviewer-print.ts`, #223, #519; `ClaudePrintPeer` is its Claude wrapper) runs one fresh child per review and clears `reviewing` at the leader's exit and first in `kill()`. A stop or watchdog settles `needs_review` at once, before the kill; `completed`, `idle` and `offline` come only after the whole process group is confirmed gone, and a failed cleanup keeps the child handle and stays `busy`. The daemon's `reviewAuthority` compares `reviewToken`: never widen it.
|
|
98
|
+
- Every print child (reviewer and Claude implementer) is spawned, bounded and stopped only through `OwnedPrintChild` (`src/adapters/claude-print-child.ts`): its `done` settles only after the whole group is confirmed gone, and a failed stop rejects with `PrintChildCleanupError` while the owning peer keeps the handle. Never add a second spawn path to an adapter.
|
|
98
99
|
- A reviewer backend is a fixed builtin `ReviewerBackend` in a definition's `reviewer`, never config. Its `prepare` must return a complete isolation receipt (matching backend id, binary, version, profile, evidence and `readOnly` true, plus `strictMcp` and `ephemeral` true, or for a `nativePolicy` backend `enforcement: "native-policy"`, `freshSession` and `hubToolsOnly` true) before every child. A `taskMcp` backend refuses without the host's `taskMcp` port and gets only that task's MCP config, never the hub-wide one. `admit(env)` and `launch.current()` are rechecked right before spawn, and a `watchCurrent` launch is polled while the review runs: a change aborts it as `review policy changed`; a missing or failed proof refuses, never a weaker launch or a continuing native session. The daemon puts the reviewer on the bus only after `start()` proved isolation; a failed start or a `start_failed` report marks it in `reviewerUnavailable`, which routing reads as `failing`. An unavailable reviewer variant has no reviewer backend or launch code; `startPeerBody` refuses it before preflight, independently of any native TUI launcher.
|
|
99
100
|
- Report reviewer failures (`onUnavailable`: `start_failed`, `quota`, `no_verdict`) only after the child's group is confirmed gone and only while its generation is current: a person's stop owns its hold and never reroutes. `quota` comes only from the backend's structured `failure(event)`, never from vendor text. An NDJSON frame over 256 KiB or more than 4 MiB of output aborts the review as `needs_review`.
|
|
100
|
-
- Codex reviewer isolation (`src/adapters/codex-review-isolation.ts`): accept only a `codex-cli` version that compat `codex-review` admits (verified or the person's trust, #691), and still run every proof below after trust; pass `-c notify=[]` (legacy notify off) on every launch; prove model-free that Apps, plugins and hooks are disabled (`features list`), ambient MCP servers are replaced by disabled stubs, and the hub server alone is enabled with exactly `hub_review` and `hub_task_show`; run the native sandbox fixture probe before every launch; hash the binary once and fence binary and config files by metadata. Credential denies come only from the bounded `HOST_CREDENTIAL_PATHS` and `HOST_CREDENTIAL_ENV` lists, denied at the lexical and canonical path; refuse glob syntax or a root inside the checkout. A root that is dangling or unreadable refuses too; the reason names only its path class (`~/.docker`, `$DOCKER_CONFIG`), never a path or contents. Never pass the control token in a reviewer child's argv, env or MCP config: keep the hub state dir in its `privatePaths`.
|
|
101
|
+
- Codex reviewer isolation (`src/adapters/codex-review-isolation.ts`): accept only a `codex-cli` version that compat `codex-review` admits (verified or the person's trust, #691), and still run every proof below after trust; pass `-c notify=[]` (legacy notify off) on every launch; prove model-free that Apps, plugins and hooks are disabled (`features list`), ambient MCP servers are replaced by disabled stubs, and the hub server alone is enabled with exactly `hub_review` and `hub_task_show`; run the native sandbox fixture probe before every launch; hash the binary once and fence binary and config files by metadata. Credential denies come only from the bounded `HOST_CREDENTIAL_PATHS` and `HOST_CREDENTIAL_ENV` lists, denied at the lexical and canonical path; refuse glob syntax or a root inside the checkout. A root that is dangling or unreadable refuses too; the reason names only its path class (`~/.docker`, `$DOCKER_CONFIG`), never a path or contents. Never pass the control token in a reviewer child's argv, env or MCP config: keep the hub state dir in its `privatePaths`. A refused `prepare` carries a `causeClass` from the closed `REVIEWER_CAUSE_CLASSES` (`version`, `binary`, `mcp_inventory`, `plugins_hooks_apps`, `sandbox_proof`, `credential_root`, `timeout`, `unknown`; `reviewer-print.ts`, #722): `prepare` tracks the proof leg it is on, and `timeout` means a metadata command ran out of its 2 s or the 12 s budget, never an abort. The host builds the reason only through `reviewerUnavailableReason(cause, detail)`: a fixed sentence per class, plus a credential-root path class (`~/.docker: a dangling link`) as the only detail. The one exception is a failed `start()` whose error is a version refusal (`VERSION_REFUSAL`) or comes from a `nativePolicy` backend: the daemon shows that sanitized message, capped at 1000 characters (#691). `ReviewerPrintPeer` itself classes a missing or invalid receipt and an admission change before spawn as `unknown`, a `launch.current()` change before spawn as `binary`, and any other start error as `unknown`. The daemon writes that reason to the `reviewerUnavailable` map (read by routing and shown as the `reviewer` line of `ahub status`), and a `hub.log` line names the class. Never add native CLI text, a path, a version output or a token to the class, the reason or the log.
|
|
102
|
+
|
|
103
|
+
## Headless Claude implementer (#525-4)
|
|
104
|
+
|
|
105
|
+
- `ClaudeImplementerPeer` (`src/adapters/claude-implementer.ts`) runs one owned print child per turn in its verified worktree and none while idle. Keep the native argv of `claudeImplementerArgs`: `--permission-mode default`, `--strict-mcp-config` and `--permission-prompt-tool mcp__agent-hub__hub_permission_request`; never `acceptEdits`, `dontAsk`, `bypassPermissions` or `--bare`. `never-ask` and `--unattended` are refused at every layer (factory, `setPermissionMode`, daemon start, `validateInstances`); a mode change only changes what the host facts gate allows. A start needs a version compat `claude-implementer` admits, and each turn rechecks the binary fingerprint from `proveClaudeImplementer`.
|
|
106
|
+
- Per-task native sessions live only in `ClaudeTaskSessions` (`src/adapters/claude-task-sessions.ts`, file `claudeStateFile("claude-task-sessions", id)`, ids only). A reserved UUID becomes `--resume` only after `markStarted` saw a stream-json frame naming that session; forget it only when `transcriptPersisted` answers false (unknown keeps it); an unreadable or invalid state file throws, never resets. Chat without a task ref goes to the instance's single `in_progress` task (`chatTask`), else to a `--no-session-persistence` one-off child.
|
|
107
|
+
- A private envelope refuses the delivery before spawn; an item `admit` refuses is dropped without vetoing the others. Settle `failed_safe` until the child's stdin was released (`inputReleased`), `needs_review` after; a failed group cleanup keeps the handle and stays `busy`.
|
|
108
|
+
- The child's tools-mode server (`AGENTHUB_PERMISSION_TOOL=1`) lists only `IMPLEMENTER_HUB_TOOL_NAMES`. `hub_permission_request` returns allow only for the person's allow-once; a malformed request, no hub, expiry or a changed child is deny. The original arguments stay in the child's request; the person sees only `permissionPayload` (`src/hub/permission-payload.ts`): the whole value withheld on any screen match, cut with `[cut, N chars]` inside 600 characters. Daemon admission of that child: `daemon.md`.
|
|
109
|
+
|
|
110
|
+
## Always-ask (#636)
|
|
111
|
+
|
|
112
|
+
- A classed call is decided in `src/hub/action-enforce.ts` and enforced where the hub sees it before it runs: the Claude PreToolUse hook (`actionClasses` on the `facts` reply, `ask` beats `allow` in every mode) and `AcpPeer.answerPermission` (`AcpOptions.actionGate`, consulted before `autoApprove`). It only ever adds an approval: unknown, unparsable or no reported payload is not classed and follows the mode. A throwing gate asks, a classed `PermissionRequest` (`classed: true`) is never answered by `--unattended`, and `allow_always` is withheld for it.
|
|
113
|
+
- The class-budget choice is the hub's own option (`ACTION_BUDGET_OPTION`), answered to the agent as its own `allow_once`; only that choice calls `ActionBudgets.approve`, and a refusal answers cancelled. Never let an ordinary allow seed the ledger. Claude never consults the ledger. `ActionBudgets.approve` consumes a slot when another approval landed since its challenge was issued (requests are concurrent; a reset would let N+1 calls through), and the daemon revokes a peer's entry on its `offline` transition and before creating a new ACP owner, so a resumed session id never revives an allowance.
|
|
114
|
+
- Do not claim enforcement a native mode bypasses: Kimi's `yolo` and `auto` send no request for a call they allow, so `ahub status` lists that peer as not enforceable until it is back in `ask`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Budget, pause and handoff
|
|
2
2
|
|
|
3
|
-
Scope: quota and native context readings, checkpoints, pause, handoff and resume, and the status line tee. Read before editing `src/hub/budget.ts`, `src/hub/context-window.ts`, `src/cli/statusline-tee.ts`, `src/adapters/definitions/native-quota.ts`, `src/adapters/definitions/native-context.ts`, `src/hub/bus.ts`, `src/hub/delivery-journal.ts`, `src/hub/restart.ts` (pause persistence), the per-account fanout in `src/hub/instance-budget.ts
|
|
3
|
+
Scope: quota and native context readings, checkpoints, pause, handoff and resume, and the status line tee. Read before editing `src/hub/budget.ts`, `src/hub/context-window.ts`, `src/cli/statusline-tee.ts`, `src/adapters/definitions/native-quota.ts`, `src/adapters/definitions/native-context.ts`, `src/hub/bus.ts`, `src/hub/delivery-journal.ts`, `src/hub/restart.ts` (pause persistence), the per-account fanout in `src/hub/instance-budget.ts`, the usage roll-up in `src/hub/usage-rollup.ts` or Kimi's count (`src/adapters/kimi-wire-usage.ts`, `src/hub/kimi-usage.ts`, `src/hub/token-window.ts`).
|
|
4
4
|
|
|
5
5
|
- Budget: checkpoint first, pause second (a paused peer receives nothing). A handoff that fails is left unmarked so the next tick or hub run retries it; never record it as done.
|
|
6
6
|
- A handoff needs somebody to hand over to: `canHandOff` is false right after a restart, when no peer is attached yet, and the handoff waits for a later tick instead of stripping tasks of their owner.
|
|
@@ -19,12 +19,13 @@ Scope: quota and native context readings, checkpoints, pause, handoff and resume
|
|
|
19
19
|
- Quota handoff context reads `QUOTA_HANDOFF_PLATFORMS` (`src/adapters/registry.ts`, each definition's `hooks.recallPlatform`: claude, codex, kimi, qwen, jcode). It is independent of `hooks.recallInjected`: a handoff platform never joins the injected-platform exclusion in recall (`test/adapter-handoff-platforms.test.ts`).
|
|
20
20
|
- `Budget.report` resolves the reporter and its account through `hooks.definitionOf` (the daemon's startup-bound project catalog; builtin-only when absent) and skips `user`, `hub` and a registered kind, configured and external module kinds included, with no `budget` block or with `budget.quotaExempt` (only local); an unregistered id keeps quota accounting. `budget.instrumented` (execution budget) never exempts quota.
|
|
21
21
|
|
|
22
|
-
- Key quota readings, pause records, overrides, checkpoints and handoffs by the canonical account (`hooks.account`, idempotent); telemetry keeps the reporter id and native context occupancy stays per instance. `instanceBudgetHooks` fans pause, resume, checkpoint and handoff out to every member, and `canHandOff` excludes the paused account's siblings. Startup normalization merges old alias pause rows without dropping a row or marking an unfinished handoff done. A manual pause stays per member. Reviewer variants keep their base kind's `budget.account`.
|
|
22
|
+
- Key quota readings, pause records, overrides, checkpoints and handoffs by the canonical account (`hooks.account`, idempotent); telemetry keeps the reporter id and native context occupancy stays per instance. `instanceBudgetHooks` fans pause, resume, checkpoint and handoff out to every member, and `canHandOff` excludes the paused account's siblings. Startup normalization merges old alias pause rows without dropping a row or marking an unfinished handoff done. A manual pause stays per member. Reviewer and headless implementer variants keep their base kind's `budget.account`.
|
|
23
23
|
- Release-cut quota admission (`releaseQuotaSnapshot` in `src/hub/daemon.ts`, #481 S11): an account has a quota source only when one of its members' definitions has `budget.reader`, or `budget.resetSoftTokensOnResume` with `kimi_tokens_5h > 0`. Without a source a registered account is reported `no quota source` and never defers; with one, an unknown, stale or below-gate reading defers. Manual and budget-record pauses defer for every attached peer.
|
|
24
24
|
|
|
25
25
|
- Keep `readClaudeTranscriptUsage` at its 32 MiB accounting bound. For native turn completion, follow the admission rule in `daemon.md`.
|
|
26
26
|
|
|
27
27
|
- Limit ledger (#683 U1): write `budget_pause`, `budget_resume` and `quota_reset` only through `Budget.writeLedger` (`hooks.ledger`), at the one pause INSERT in `evaluate` (a new pause that replaces an expired open record first writes that record's `budget_resume`, `by: "hub"`) and the one DELETE in `resume` (`by: "person"` only from `override`); never from a call site that reads `budget_pauses`. `quota_reset` is derived in `deriveResets` from the last reading of each window (`lastSeen`, never `readings`, which a resume clears): a forward `resetsAt` move of more than `RESET_JITTER_MS` (15 min), or a `used` drop of `RESET_DROP` (0.5) or more within one window, is a reset, and anything smaller is the same window. It is deduplicated by `peer|window|resetsAt` in `resetsRecorded`, which the daemon seeds from `events.jsonl` at start so a restart adds none. A reading older than the baseline never counts. Entries carry ids, windows and numbers only: never a reason built from agent text, a checkpoint, a summary or a task title (the reason is the coordinator's own text). A throwing hook never costs the pause or the reading. Codex's `usage` event comes from `onTokenBreakdown` beside `onTokens`; keep `tokens` unchanged.
|
|
28
28
|
|
|
29
|
-
- Usage roll-up (#683 U2): `src/hub/usage-rollup.ts` is the only reader of `events.jsonl` for `ahub usage
|
|
30
|
-
|
|
29
|
+
- Usage roll-up (#683 U2): `src/hub/usage-rollup.ts` is the only reader of `events.jsonl` for `ahub usage`, the console Usage panel and the dashboard Usage view (rules: `daemon.md`). A pass reads from the stored offset, keeps whole lines only and commits its rows with the new offset in one transaction; its 20 ms slice (`poll(maxMs)`, #719) stops only between whole records and commits the offset of the last one folded; a line longer than the cap is read whole and one past `MAX_LINE_BYTES` is skipped, so the fold always advances. Never store a title, reason or summary there; `limit_events` drops `reason`. issue/pr/release are joined from the board at query time through `TaskLinks`, never stored. A counter with no record behind it is `unknown`, not 0. Claude's total is derived as the sum of its four counters (`claudeReportedTokens`); a source whose input contains the cached part gets none. Every `usage` reply, errors included, answers the rid, and the op is for the console and a configured planner only, never during that planner's private turn. A rebuild from byte 0 must equal the incremental result (`test/usage-rollup.test.ts`). Never present unknown as 0 in a sum: an overall figure with an unknown peer is `partial` and names the peers (`total().unknownPeers`). Never drop rows silently: `limits()` reports `truncated`, `omitted` and the bound. An event-open pause the hub no longer holds is `endedUnknown` (`ahub reset` keeps the log), so `limits(peer, { live })` needs the daemon's held pauses. A poll's fold transaction is IMMEDIATE and rechecks the stored offset, so two roll-ups on one `hub.db` never fold a range twice.
|
|
30
|
+
- Daily usage (#683 U4): `UsageRollup.daily(days, now)` returns the last `days` UTC calendar days (at most 366) ending today, clipped to the first day with a record. A day inside the window with no record is a row with `empty: true`, never zeros. Per peer and day, take the native total when reported, else the counted tokens (`count`), else `unknown`; a day's total is `partial` when some peers are unknown and `unknown` when all are, never a sum that treats them as 0.
|
|
31
|
+
- Kimi's consumption (#285 phase 2): `src/adapters/kimi-wire-usage.ts` is the only reader of Kimi's wire log and `src/hub/kimi-usage.ts` the only owner of the Kimi count; `daemon.ts` supplies ports (events, budget, clock, session id) and binds the session at a turn start (no read) and calls `collectKimiUsage` at a turn end, a `usage_update` and an offline transition. Rows of an earlier read pass are recorded before a later pass can fail; restart seeding counts only rows recorded after the last `budget_resume` of a soft-token peer (`kimiSeedRows`). Tests never reach the person's `~/.kimi-code`: `test/hub-home.ts` sets `KIMI_CODE_HOME` to a temp dir. Never feed `budget.report(kimi, tokens)` from an ACP total or an estimate, and never change `kimiCountedTokens` without the AC5 test: cache reads are recorded, not counted. An unreadable log, an unknown row shape or an untested `protocol_version` is unknown (reading dropped via `Budget.forget`, one notice), never a partial count. A usage row's text never leaves the reader: four integers, a model label and the row time. A resume clears the window (`kimiUsage.reset()`).
|
package/docs/agent-notes/bus.md
CHANGED
|
@@ -26,6 +26,7 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
|
|
|
26
26
|
- Release moves each deferred notice through `publishNow` and checkpoints it before delivery, events or nudges run. A failed checkpoint restores only publication state (the affected queue arrays in place, deferred, seen and withdrawn ids, pending outcomes) and keeps the hold. Never reload a snapshot or touch prefaces, pauses or the recovery mode from it. A successful release clears the deferral mode (`deferHub`) after `flushDeferredHub()` and before dropping `recoveryHeld`, so a later bare hold or `fenceRecovery` queues hub notices; a failed flush keeps both the hold and the deferral mode (#541).
|
|
27
27
|
- Do not offer recipient retry to delivery-only local/ACP transports or pass ordinary messages through their hold; their hints direct the person to resolve. Keep current-adapter refusal authoritative and the ephemeral MCP exposure marker advisory; pull-only Kimi TUI retains retry.
|
|
28
28
|
- Route peer retry only through `retryDelivery`: scope explicit-retry history by peer and immutable original ids across all grouped parents, commit monotone consumed-proof inheritance for every grouped non-context original with the checkpoint while keeping resolution audit rows unchanged; exclude hub `presence` originals from every retry proof, warning, inheritance and unknown-marking path, preserve unknown imported lineage fences without disabling fresh work, and admit busy-peer consent only to requeue its own `needs_review` row. Decorate rendered clones after condensation/pull/steer, never originals; reserve the shared journal body cap for the warning and an explicit Unicode-safe cut marker. Keep held hints bounded, body-free and advisory; metadata failure never changes a saved tool result or refusal class (#434).
|
|
29
|
+
- Periodic and status paths (roster, supervision, held-delivery views) read only pending journal state: `pendingPeers()`, `queueList(peer, true)` (`unresolved`) and `revision()`, and `snapshot(false)` when only queues are needed. Never call `list()` or build a journal `snapshot()` there for a revision; they sort the whole payload history (`test/delivery-journal.test.ts`, #719). Explicit queue and history commands and recovery snapshots keep the full history.
|
|
29
30
|
- `restore` takes queued work from the journal, never from the committed snapshot once the journal has state (an empty journal, revision 0, is seeded from the commit): from the commit only an empty queue key is carried over, for an offline peer whose key no delivery ever persisted (#234). Never resurrect a non-empty queue from it.
|
|
30
31
|
- The journal's owner check (`rejectLiveOwner` in `delivery-journal.ts`) judges the manifest pid with `processLiveness` and its `pidSignature`, never a bare probe (rule: `daemon.md`).
|
|
31
32
|
- A hub-native peer (Pi, the local worker) does not get to call its own message `important`: `capPriority` caps it unless the delivery it answers held an `important` envelope addressed to it. Check the delivery, not `replyParent`, which ties on hop and takes the later item. Do not bypass it by setting `priority` in the adapter.
|
|
@@ -8,18 +8,24 @@
|
|
|
8
8
|
|
|
9
9
|
- Console attention navigation selects a delivery only by an exact visible row id. A missing id or row prints read-only guidance; never select by peer or fall back to the first Queue row.
|
|
10
10
|
|
|
11
|
-
Scope: the operator console, control WS and its protocol, state files, the dashboard listener and settings, the Claude channel's reconnect, `ahub setup`, git snapshots, upgrade and recovery. Read before editing `src/adapters/definitions/native-recovery*.ts`, `src/adapters/definitions/native-policy-aux.ts`, `src/adapters/definitions/native-config-*.ts`, `src/adapters/definitions/native-cli-*.ts`, `src/cli/console.ts`, `src/cli/console-state.ts`, `src/cli/attention.ts`, `src/ui/needs.ts`, `src/hub/daemon.ts`, `src/hub/ui.ts`, `src/hub/settings.ts`, `src/hub/control-client.ts`, `src/adapters/claude-channel.ts`, `src/hub/restart.ts`, `src/hub/recovery-store.ts`, `src/hub/snapshots.ts`, `src/hub/manager.ts`, `src/hub/lifecycle.ts`, `src/hub/crash.ts`, `src/hub/start-mode.ts`, `src/hub/terminal-open.ts`, `src/hub/recovery-error.ts`, `src/hub/db-timeout.ts`, `src/hub/config-reload.ts`, `src/hub/config-apply.ts`, `src/hub/recovery-continuity.ts`, `src/cli/recovery-presentation-options.ts`, `src/cli/setup.ts`, `src/cli/upgrade*.ts`, `src/cli/terminal-recovery.ts`, `src/cli/recovery-package.ts`, `src/cli/facts-hook.ts`, `src/hub/usage.ts`, `src/hub/memory-profile.ts`, `src/hub/native-hook-launch.ts`, `src/cli/output.ts`, `src/ui/index.html` or the instance modules (`src/hub/instances.ts`, `src/hub/instance-config.ts`, `src/hub/instance-execution.ts`), or before adding a native peer.
|
|
11
|
+
Scope: the operator console, control WS and its protocol, state files, the dashboard listener and settings, the Claude channel's reconnect, `ahub setup`, git snapshots, upgrade and recovery. Read before editing `src/adapters/definitions/native-recovery*.ts`, `src/adapters/definitions/native-policy-aux.ts`, `src/adapters/definitions/native-config-*.ts`, `src/adapters/definitions/native-cli-*.ts`, `src/cli/console.ts`, `src/cli/console-state.ts`, `src/cli/attention.ts`, `src/ui/needs.ts`, `src/hub/daemon.ts`, `src/hub/ui.ts`, `src/hub/settings.ts`, `src/hub/control-client.ts`, `src/adapters/claude-channel.ts`, `src/hub/restart.ts`, `src/hub/recovery-store.ts`, `src/hub/snapshots.ts`, `src/hub/manager.ts`, `src/hub/lifecycle.ts`, `src/hub/crash.ts`, `src/hub/start-mode.ts`, `src/hub/terminal-open.ts`, `src/hub/recovery-error.ts`, `src/hub/db-timeout.ts`, `src/hub/config-reload.ts`, `src/hub/config-apply.ts`, `src/hub/config-tier.ts`, `src/hub/config-edit.ts`, `src/hub/config-edit-host.ts`, `src/hub/claude-launch-proof.ts`, `src/hub/recovery-continuity.ts`, `src/cli/recovery-presentation-options.ts`, `src/cli/setup.ts`, `src/cli/upgrade*.ts`, `src/cli/terminal-recovery.ts`, `src/cli/recovery-package.ts`, `src/cli/facts-hook.ts`, `src/hub/usage.ts`, `src/hub/memory-profile.ts`, `src/hub/native-hook-launch.ts`, `src/cli/output.ts`, `src/ui/index.html`, `src/hub/task-code-group.ts` (dashboard Code group) or the instance modules (`src/hub/instances.ts`, `src/hub/instance-config.ts`, `src/hub/instance-execution.ts`), or before adding a native peer.
|
|
12
12
|
|
|
13
13
|
- The plugin bundle is installed apart from the daemon. Breaking a control WS message contract bumps `PROTOCOL` in `control-client.ts`; a new request answered as unknown or an optional field preserves it, as does additive optional public task metadata in `ui_snapshot` that older clients ignore (such as `deps` in #246).
|
|
14
|
-
- `peer_stop` is console-only and stops only a hub-owned headless ACP, Pi or
|
|
14
|
+
- `peer_stop` is console-only and stops only a hub-owned headless ACP, Pi, local, print-reviewer or Claude implementer runtime through its adapter. Refuse native-terminal peers and recovery-held mutations; serialize with starts and permission changes, and refuse a recovery `prepare` while a stop runs. An admitted stop holds delivery through `BusOptions.deliveryHeld` before any teardown await (never a console pause that resume can undo, or a global recovery hold that blocks unrelated peers) and withdraws the peer's pending approvals before the adapter stops. A requested stop holds an in-flight delivery as `needs_review` with an ids-only stop reason, suppresses restart/escalation, and never resolves that delivery automatically.
|
|
15
15
|
- Console review guidance (#711): `r` on a task in review opens `reviewChoice` (`src/cli/console-review.ts`, modelled on `modeChoice`) from the row or the open task detail, never a bare prefill; `a` and `c` prefill `review N approved|changes_requested ` with a guide line, a `changes_requested` line with no note is refused in `reduceConsole` before any effect, and the result of a review command is shown as a notice in panels mode (`reviewResult`). The console only prefills and runs `ahub review` as the person: never grant it extra authority, and keep a private task's title out of the choice line (`[pii]`). The CLI cause line (`agentShellCause`, `src/cli/identity.ts`) is printed for the `tools` identity and a person-only hub refusal only, never for the console identity, and never replaces the refusal.
|
|
16
16
|
- Console changed-symbols block (#684 2b): `symbolFields` (`src/cli/console-symbols.ts`) adds `symbols` after `plan` for a task_show-shaped detail only, from the public `codeSymbols`; never from a raw `Task`, never for a private task, never from a stored receipt (one with `repositoryKey` reads as unknown: only the screened public receipt is shown), never as a second copy of the match rule (`planSymbolMatches` in `review-brief.ts`). Unknown is `unknown (no code facts)`, never an empty list.
|
|
17
17
|
- Console Queue `D` (#713) only prefills `queue discard --peer <selected row's peer> --reason `; Enter refuses a missing peer or reason and arms a `y` confirm that names the count, then runs the person's `ahub queue discard ... --yes`. The `queue` op stays console-only; `discard` without `confirmed` only counts, and a confirmed one needs a reason, is refused during recovery or shutdown and discards row by row through `resolveDelivery` (rule: `bus.md`).
|
|
18
18
|
- Console Usage panel (#683 U3): tab `7`, text from `src/cli/console-usage.ts` (pure strings, no terminal or state import), rendered in `console-state.ts`, fetched in `console.ts` `refresh()` only while panel 7 is open. It reads the `usage` op and nothing else: no event-log read, no board field beyond the task id selected in Tasks. A refusal shows `usage unavailable`, never empty usage; an unknown counter is `unknown`, never 0; a private task is `#id [pii]` from the op's `taskPii`, never a title. The `usage` op folds with `catchUp(250)` and answers `behind: true` when it is not done: never answer from one pass of a big log. Keep Left/Right from 7 wrapping like Settings so the `1`..`5` cycle (#309) stays pinned, and keep PAUSE and 7D out of the width drop list.
|
|
19
|
+
- Dashboard snapshot `view` (#683 U4): the daemon's `ui_snapshot` and the manager's forward pass a request's `view` only through `dashboardView` (`DASHBOARD_VIEWS` in `src/hub/ui.ts`); keep that list equal to the page's `DASHBOARD_VIEWS` in `src/ui/index.html`. Any other value is dropped, never forwarded.
|
|
20
|
+
- Dashboard Usage view (#683 U4): `uiSnapshot` builds `usage` only when the request's view is `agents`, never on a hidden view's poll. It folds with `catchUp(100)` and sets `behind: true` when that is not done, so a partial figure is never shown as the whole. A failed read answers `{ unavailable: true }`, never `undefined`, which the page reads as a hidden view and keeps the old figures. The snapshot carries task ids, a `pii` flag and numbers only, never a title; the page shows a private task as `#id [pii]`. Daily figures come only from `UsageRollup.daily` (rule: `budget.md`).
|
|
21
|
+
- Dashboard Code group (#684 2c): `codeGroup` (`src/hub/task-code-group.ts`) is the only dashboard classifier of changed symbols against `plan.symbols`, called in `uiSnapshot` with the public `codeSymbols`, `tasks.publicMetadataText` and `tasks.isPii`; the page (`codeGroupSection`) draws it and never matches names. A private task gets no `codeGroup`. A stored receipt (one with `repositoryKey`), a missing or an unknown one is `state: "unknown"`. A malformed or screened-out plan name is counted in `planWithheld` and marks the group incomplete, never dropped. Lists stop at `CODE_GROUP_MAX` (40) with the rest in `more`, and `counts` are totals. The memory view reads a `node` of up to 512 characters only when it starts with `symbol:` (200 otherwise).
|
|
22
|
+
- SDLC band timer (#481 S7): the daemon's band interval (`bandsEveryMs`, default 5 min) is the only caller of `tasks.proposeFromBands`. It skips while stopping or in recovery, when `tasks.bandsState()` is not ready or `bandsHeld` holds, and when `board.statusRevision()` with the band signature is unchanged since the last pass that ran; it records that key after the write. A failure is one sanitized `hub.log` line. Writer rules: `tasks.md`.
|
|
23
|
+
- `ahub board` (#724): `renderBoard` prints one line per task only with a terminal width and without `--full`; `--full`, `ahub task show` and output without a width keep the full table and SDLC advice. `STAGE_CODES` (`src/cli/output.ts`) keys on blocker-text prefixes from `src/hub/sdlc.ts`: rewording a blocker there updates its code in the same PR. TITLE is cut, never wrapped; STAGE is never dropped (it falls back to the meter and the code); a PII row keeps its `(ahub task show N)` hint while 8 title columns remain.
|
|
19
24
|
- Settings (#269): the dashboard's `setting`, `setting_undo`, `setting_preview` and `setting_reload` actions and the console-role `settings_get|set|undo|preview|reload` requests go through `settingsSet`, `settingsUndo`, `settingsPreview` (`settingsUndoPreview` for `undo: true`) and `reloadConfig` (`src/hub/daemon.ts`) and the registry in `src/hub/settings.ts`. A new editable setting is a registry entry, never a new write path; a key that chooses what the hub runs or where secrets or task text go joins `NEVER_EDITABLE` instead. Writes go only to `.agenthub/config.local.json` and `.agenthub/routing.local.toml` through `store()`: the loader's own validation first, temp file and rename, never a file git tracks. Authority comes from the dashboard session (`DashboardSession`, `src/hub/ui.ts`) or the console role, never from the request body; `ui_action` from the manager is an ordinary session. Never return a setting's value in an event or notice except through `recordSetting`.
|
|
25
|
+
- Structural config edits (#689): `src/hub/config-tier.ts` is the one tier rule (editor, dashboard refusal and `structuralLeaf` in `authorizeConfigLeaf`), `src/hub/config-edit.ts` plans and writes (loaders first, revision checked twice, bounded backups, atomic), `src/hub/config-edit-host.ts` owns authority, the typed tier 3 confirmation, the `config_edit` event and the scoped publish. The daemon only supplies ports (`createConfigEditor` in `daemon.ts`); never validate, write or classify in `daemon.ts`. A new editable structural key is an entry in `config-tier.ts` and nothing else; a key that chooses what the hub runs or where secrets go is tier 3 and is never offered to the dashboard (the hub refuses it, the page also hides it). The audit event and every notice carry key paths and value hashes, never a value. `structural` reaches `authorizeConfigLeaf` only through `structuralScopes` for the one scoped reload. `sdlc.toml` is read once at start, so it joins `pendingRestart` through `allPendingRestart()`, never through `reloadConfig`. A new ahub settings subcommand that writes is classified `console` by default; keep it that way.
|
|
20
26
|
- Live reload (#439): `reloadConfig` is the only publication path. Settings writes and undo, `ahub settings reload`, the dashboard reload and the debounced watcher of the four `CONFIG_SOURCE_FILES` (`src/hub/config-reload.ts`) all reach it; `prepareConfiguration` validates the complete candidate before anything is published, and recovery or shutdown holds it. A write or undo passes its own key as `scope` and publishes only that path, and `acknowledgeConfiguration` records only the accepted paths, so other edits stay pending. Every changed key, inside the registry or not, is decided by `authorizeConfigLeaf` (`src/hub/config-apply.ts`): a refused key keeps its accepted value and is reported by name while the others apply, `roster.*` applies only through its own scoped roster write, and a change of the settled planner or conductor holders (`roleHolders` over `resolveSettledRoles`) is guarded even when a role list shrinks. Outside a named write, only the console's `settings_reload` (authority `terminal`) adopts guarded file edits, by sending back the `confirmation` token bound to the candidate `fingerprint` and `configRevision`; changed bytes or accepted state refuse it, and a watcher, dashboard or settings session never confirms. An undo that names `key` and `undoDigest` writes only while `pendingUndoIdentity` still matches both. Keep config-source snapshots plain and cloneable; classify dynamic ids with validated running/source identity scopes, never suffix discovery or a builtin fallback. Compare restart-bound declarations with the running config on every reload, never a pending desired value accepted by an earlier reload; derived defaults stay bound to that running declaration. A `restart` key (`configApply`) keeps its running value and is listed in `pendingRestart`; reload notices and events carry key names and reasons, never values. The console settings editor (the Settings panel, or the Peers panel's `w` WIP shortcut, which opens only an existing `tasks.wip_limits.<peer>` row for a rostered peer listed in status `taskWip`) stays bound to the panel that opened it (`SettingsEdit.panel`) and arms a write only from a preview reply that matches that panel and its open edit's token, key and value; pasted text never confirms. Console Tasks backlog actions (`p`, `a`, `e`, `R`, `c`) only prefill person CLI commands (`backlog pull-ready|plan|return`, `task cancel`) with the row's immutable id and its planning or flow revision; `validateLockedRow` rechecks the row's state and revision at Enter and again at `y`, and no console path writes the board directly (#532).
|
|
21
27
|
- Retained configuration (#544): a key the reload refused, or a file edit outside a scoped write that still differs from the accepted value, is listed in `retainedConfig` (key and sanitized reason only, never a value) in status, `status.json`, the settings view and the console snapshot until a reload accepts or reverts it (`retainedConfigAfterReload`). A config leaf that selects a peer's native launch belongs in that definition's `cli.launchConfigKeys`: the `start` and `peer_start` replies then carry `retainedLaunchConfig` (the running values), the launcher starts with them and prints `retainedLaunchWarning` before its start request; when a retained value cannot launch (Kimi TUI `kimi_cmd` with no TUI form), the error names the running hub as its source and never echoes the words (#654).
|
|
22
|
-
- `action_policy` (#636) is machine-local only: `stripUntrusted` drops it from any file but `config.local.json`, and `authorizeConfigLeaf` refuses an `action_policy` leaf whose `sourceFile` is not an untracked `config.local.json` before any other check. It applies live (`actionBudgets.policyChanged`);
|
|
28
|
+
- `action_policy` (#636) is machine-local only: `stripUntrusted` drops it from any file but `config.local.json`, and `authorizeConfigLeaf` refuses an `action_policy` leaf whose `sourceFile` is not an untracked `config.local.json` before any other check. It applies live (`actionBudgets.policyChanged`); Claude's PreToolUse hook and hub-owned ACP peers enforce it (#312); Codex, Pi and the local worker do not yet, so keep `actionEnforcement` (`src/hub/daemon.ts`) honest: a peer is enforceable only where the hub sees the call before it runs, in the mode it runs in now, and says why otherwise.
|
|
23
29
|
- `pet` is console-local presentation (#483): validate it only through `validatePetConfig`, which both `loadConfig` and the reload schema call, and keep it applied live with no authority (`authorizeConfigLeaf`).
|
|
24
30
|
- Start modes (#269): every start the hub makes itself goes through `hubStart` (`src/hub/daemon.ts`): the dashboard's `start_peer`, the console-role `peer_start`, the conductor's `start` hook and `pi.auto_start`. It takes the mode from `startMode(peer)` and nothing else; never add a caller-chosen mode, flag or command to it, and never fall back to headless when a terminal cannot be opened (refuse with the manual command). The words a terminal runs are assembled there from `process.execPath`, the CLI entry, the project root and `startWords(peer)`. The only continuity exception is a validated Pi bookmark on `pi.auto_start`: append its recorded session file (or its session id when no file was recorded), backend and model; never pass both session selectors or accept caller-written replacements. Recovery and `startPeer` callers that pass a recorded mode keep it. A launcher with no `--mode` takes the daemon's accepted start mode from `status.json` `startModes` (`readControl`) before the file's value. A TUI start of an `owned-acp` peer (Jcode) from the console's `peer_start` opens its terminal even while the ACP owner is online: that terminal is a frontend on the owner. The conductor and the dashboard answer `already` for an attached one (#559). The real terminal providers are wired only in `src/hub/lifecycle.ts` (`DaemonOptions.terminal`): a daemon built without them, as in every test, opens nothing; a test injects a recording provider and never the real one.
|
|
25
31
|
- A headless start that `hubStart` made for a peer with `capabilities.watch` (Kimi, Qwen) also opens `ahub watch <peer>` through `openWatchPane` (#286 AC10): the same provider, fixed words from `process.execPath`, `CLI_ENTRY`, the project root and the peer id, one per peer per `PEER_START_RETRY_MS`. It runs only after the start succeeded and never fails it: no provider or a failed open reports `watch.command` in the result and a notice, with the provider's own text in the log only. Never open a pane for an `already` result or a declared instance.
|
|
@@ -35,7 +41,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
35
41
|
- Adding a native peer requires testing the command emitted by the actual recovery driver, not only its inspection or launch helpers. Never let a generic non-Codex branch treat a new peer as Claude.
|
|
36
42
|
- Cross-version recovery must use the authenticated source protocol for prepare/commit/abort and the target protocol after startup. Prove the transition against a real prior release before claiming compatibility.
|
|
37
43
|
- Preserve `task_cancel` target-feature preflight before source holds, capture or commit. Reuse stored target features only after verifying the same retained root and digest; legacy receipts still capable of commit must obtain feature proof. Do not apply pre-commit compatibility checks to sources already past commit. Every prepare, including the lease renewal before a terminal close and after a resume, sends the operation's stored `targetFeatures` (#478).
|
|
38
|
-
- A recovery target-feature floor (`task_cancel`, `peer_instances`, `release_trains` while a configured train is active or frozen, `reviewer_pool` while a fresh reviewer or `review.pool` is configured, `configured_acp` while the project declares `adapters`, `external_adapter_modules` while external module definitions are loaded) lives in `incompatibleRecoveryTarget`, which every existing check site calls, and its name in `DAEMON_FEATURES`; never check a floor at one site only.
|
|
44
|
+
- A recovery target-feature floor (`task_cancel`, `peer_instances`, `role_stages` while a declared Claude-kind instance holds `implementer`, `release_trains` while a configured train is active or frozen, `reviewer_pool` while a fresh reviewer or `review.pool` is configured, `configured_acp` while the project declares `adapters`, `external_adapter_modules` while external module definitions are loaded) lives in `incompatibleRecoveryTarget`, which every existing check site calls, and its name in `DAEMON_FEATURES`; never check a floor at one site only.
|
|
39
45
|
- Defer hub notices only for an authenticated recovery `prepare` or `commit` whose target features include `recovery_deferred_hub`, or for a restored daemon. Otherwise `setRecoveryHold(true)` moves the deferred notices into queues before commit records expected integrity, and a bare `fenceRecovery` defers nothing. Abort and lease expiry release the bus hold before clearing the operation; if that release fails, keep the operation and let expiry retry on its 1 s timer (#478).
|
|
40
46
|
- Resume a prepared `commitSent` source that is unavailable or database-busy inside the existing bounded shutdown poll; never re-send commit, re-prepare or close terminals again on that path. Require confirmed `stopped` before target startup, refuse a replacement instance, and retain the prepared receipt on timeout.
|
|
41
47
|
- Keep restoration receipts explicitly tagged as pending, waiting with origin/binding, done with binding, or failed. Project waiting origins as `waiting:pending` and `waiting:failed`; preserve the chosen binding and origin across interruption instead of inferring completion from an object.
|
|
@@ -54,14 +60,18 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
54
60
|
- `upgrade()` (`src/cli/main.ts`, #272) never coordinates a target newer than its own `VERSION`: it hands over to that release's coordinator (`bun x --package @staix/agent-hub@<to> ahub upgrade --to <to>` with the same flags), and `AGENTHUB_UPGRADE_HANDOVER=1` keeps the process it started from handing over again. `--to` passes `exactVersion` before it reaches a package spec or the registry. `latestRelease` supplies it only for a terminal run or a dry run: `--yes`, and an apply through a pipe, still name the release.
|
|
55
61
|
- Recovery judges the registry claim as it is now (`inspectRecovery` re-reads `registeredProjects()`), never the plan's frozen copy; a registry that cannot be read is not inspected, never stopped (#226).
|
|
56
62
|
- `inspectRecovery` catches only `RecoveryDeferredHubError` from `readRestartSnapshot`: keep the stopped state and return the static `snapshotError` blocker without a `snapshot` binding. Never echo rejected content, repair the snapshot or offer resume from it (#478).
|
|
57
|
-
- The managed Claude hook (`src/cli/facts-hook.ts`) may
|
|
63
|
+
- The managed Claude hook (`src/cli/facts-hook.ts`) may add `permissionDecision: "allow"` on `PreToolUse` (for `ask-when-needed` only what `projectFileTool` accepts) and, since #636, `permissionDecision: "ask"` for a call the hub classed (`actionClasses` on its `facts` reply, in every permission mode, and an `ask` replaces any `allow`); it never adds `deny`, and a value that is present but unreadable asks. A hub that is down, slow or refuses decides nothing, so Claude's own policy stays in force. The daemon puts `permission` in a `facts` reply only for the hook of this instance's recorded `ahub claude` launch (the same `launchId`, not `--unattended`) or of a headless Claude implementer's current child (rule below). For a declared instance with a `worktree` it answers `ok: false` without a current verified owner root (`ownerWriteRoot`), never a canonical-tree fallback, and that root is the reply's `projectRoot`. A permission-only hook (`hookPurpose`) never drives busy state (#242). With `AGENTHUB_HOOK_EVENT` set, the hook serves only the peer definition's `hooks.factsHook` port: input over 1 MiB or refused by its `decode` decides nothing, it sends `native_facts`, and it outputs only the port's `encode` (agy: an ephemeral message on SessionStart/PreInvocation, never a permission decision). The daemon refuses plain `facts` from a peer whose definition has `factsHook`.
|
|
58
64
|
- `ahub claude` (`src/cli/launch.ts`, #270) publishes `claude-launch.json` (`claude-launch.<id>.json` for an `--as` instance) for every launch against a running hub, atomically and without a lock: hooks, permission status, session observation and recovery read its `launchId`, so never make it wait for native-proof state. The native child's pid and signature go only into the private `claude-native-<binding hash>.json`, keyed by the exact launch record, written once under `claude-launch.lock` and removed at that child's own exit; never put them in `claude-launch.json`, where a late callback of an earlier launch could replace a newer record. Crash cleanup of a previous launch's settings file (`cleanupStaleClaudeSettings`) needs the launcher gone by `processLiveness` and that native proof, and for a `direct-child` proof the native pid absent from an available `processTable`: a direct child may exec in place, so its signature certifies nothing. A script or interpreter gets no proof and keeps its settings file.
|
|
65
|
+
- Read a `claude-launch*.json` record in the daemon only through `readClaudeLaunchProof` (`src/hub/claude-launch-proof.ts`: no-follow, 16 KiB cap, this instance only), never by parsing the file. An `--as` launch records the `executionCwd` its entry got from `resolveExecutionCwd`; the entry never chooses or creates a worktree.
|
|
66
|
+
- Terminal Claude implementer roots (#525-5): bind `executionRoots`/`executionOwners` only at a hello where the resolved worktree is not the project root, the private launch record names this hello's `launchId` and that root, and the channel's reported `cwd` resolves to it; bind it to that socket and session generation. `currentExecutionOwner` rechecks the record and the worktree in each event-loop turn (reuse only within synchronous code), and `revokeTerminalExecution` drops the proof on a mismatch, detach or new claim. Missing or unknown evidence keeps the channel attached but never grants isolation; the channel omits `cwd` when it cannot resolve its own.
|
|
67
|
+
- A headless Claude implementer's tools socket (#525-4) is admitted only with the launch id of that peer's current child (`ownedTools`) and a verified worktree root, and bound as `toolsOwner`: every request and every reply rechecks `currentImplementerTools` and fails closed. Its `task` ops are limited to `IMPLEMENTER_HUB_TOOL_NAMES` and run inside `childToolAuthority`, so `Tasks.requestCheck` refuses before an effect once the child changed. Its `inbox` refuses, before `bus.pull`, a batch holding a private item or one attributed to another task. Its `permission_request` is `classed: true`, offers allow-once and deny only, and denies on expiry, abort or a changed child. Its facts get `permission` only for the current child's launch id, session id and this instance.
|
|
59
68
|
|
|
60
69
|
- Keep SQLite recovery contention in the existing blocked receipt as retryable `database_busy`, retaining bounded throw-site frames and the ordinary `ok: false` rid reply. Use a neutral control error; the receipt's next actions name resume or the partway disposition. Clear a superseded failure on abort, disposal, fresh-session choice or a refused resume of a disposition. After a commit request, treat busy inspection as unconfirmed and keep polling only inside the existing shutdown bound.
|
|
61
70
|
- Recovery inspection uses its existing 30 s control cap for the first status read and consumes a structured busy reply without a second status probe; ordinary lifecycle probes keep their default. Treat `busy_timeout` as a ceiling for eligible waits, not a guarantee that every write waits: a WAL snapshot conflict may fail immediately. Account for synchronous 3 s hub-database waits and serial timer callbacks when choosing a timeout; several waits may consume the 30 s control margin. Never multiply the busy wait through retries or infer stopped from a shorter probe's timeout.
|
|
62
71
|
- Every `new Database(` under `src/` is followed at once by its own `PRAGMA busy_timeout` from a named `*_BUSY_TIMEOUT_MS` constant (`HUB_DB_BUSY_TIMEOUT_MS` in `src/hub/db-timeout.ts` for `hub.db`), and nothing sets `busy_timeout` again later: `test/db-busy-timeout.test.ts` reads the source and fails on any other opening idiom, so a new one extends that guard first (#303).
|
|
63
72
|
- The manager forwards a dashboard `start_peer` with the terminal provider cap (`MAX_COMMAND_MS`) plus 5 s and reports a missed deadline as `unconfirmed`, never as a failure; it recognises the deadline by the exact text `ControlClient.request` resolves with, so change the two together. The hub's terminal reopen guard and the dashboard's retry hold share `PEER_START_RETRY_MS` (`src/hub/start-mode.ts`): never give either its own number (#298).
|
|
64
73
|
- `openManager` keeps only the manager that answered `/open`: when the answering pid is not the process this open spawned, it stops its own spawn with SIGTERM, so a losing concurrent open never leaves an unowned manager (#584).
|
|
74
|
+
- Only a native launch connect retries (`retryLaunchConnect` in `control-client.ts`, #719): only `AHUB_CONNECT_TIMEOUT` and close 1006, within 15 s, with one `hub busy, retrying` line, rereading the descriptor and keeping the instance pin on each attempt. An unreachable listener and an authentication, wire or identity refusal stay terminal; every other connect keeps its single 3 s attempt.
|
|
65
75
|
|
|
66
76
|
- Use the registry's `KNOWN_PEERS` (`src/adapters/registry.ts`) for the native catalog (the `src/hub/peer-catalog.ts` literal is a cross-release probe only) and `rosterAllows` for participation. Change live membership only at the authorized roster settings write or undo, applying exactly that key. A roster key that `staleInstanceKey` matches (an undeclared `<kind>-<name>` instance id) loads inert and grants nothing; its only admitted write is deletion from `.agenthub/config.local.json` (`ahub roster remove <id>`, applied live), refused while it lives only in, or differs in, `.agenthub/config.json`; adding or setting one is refused (an unknown key on reload). Keep external ids and projects without roster settings compatible with the existing admission and lazy status rules. Fence starts and attachment before detaching or stopping a removed runtime; retain its queue, uncertain deliveries and independent pauses. Refuse removal of the conductor holder and require explicit confirmation and `--now` for an active turn.
|
|
67
77
|
|
|
@@ -80,11 +90,13 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
80
90
|
- Bind adapter and instance declarations once at startup through the daemon-local `adapterCatalog` (`createProjectAdapterCatalog(loadedConfig.adapters, externalDefinitions)`, the definitions `loadExternalAdapterDefinitions` imported at startup; a reload passes the same `externalDefinitions` to `loadConfig` and imports nothing), its `definitionOf` closure and the `configuredIds`/`extraIds` catalogs; never publish them to the global registry or take them from a request. `adapters` and `instances` are restart-class, and `adapters` is `NEVER_EDITABLE` and in `ALL_MACHINE_LOCAL`. Settings rows come only from `projectSettings(declarations, adapterCatalog)`; a `locked` row (a read-only instance's permission) is refused by `checkSettingValue` and `settingRefusal` for every authority, and the console and dashboard never arm an edit or undo for it.
|
|
81
91
|
- Configured ACP starts (`startPeerBody`): before effects refuse native launch overrides (model, route, backend, unattended, provider profile, session file, fresh) and an invalid session selector; answer `already` only for the same requested native session and never pass `qwen_cmd`; after a failed start keep the owner with `pendingOwnedProcess` on the bus, let `peer_stop` admit that offline owner and refuse starts until it is confirmed gone, reporting only a withheld-detail reason. In `taskOp`, refuse a read-only instance's op outside `READ_ONLY_HUB_TOOLS` before effects. Publish `configuredAdapters`, `startablePeers` and `readOnlyPeers` in status as bounded ids only. Keep controlled recovery refused (`session.recoveryRefusal`); crash resume passes only a bounded, control-free recorded `sessionId`, never a recorded model.
|
|
82
92
|
- `validateInstances` admits `review_mode: "fresh"` only on a headless instance whose roles are exactly `["reviewer"]` and whose kind declares a fresh reviewer; a fresh reviewer refuses `worktree` and `permission_mode` (variant selection: `adapters.md`). `review.pool` and `review.rerotate` are validated by `reviewerPoolConfig` at startup and in the config schema: 1 to 16 distinct kinds from `REVIEW_POOL_KINDS`.
|
|
83
|
-
-
|
|
93
|
+
- Channel-kind instances (#525 R1, `validateInstances`): a terminal instance declares roles within its kind's `instances.roles` (Claude: planner, implementer, verifier, reviewer). An `implementer` or `verifier` needs its own `worktree`; a planner or reviewer refuses one. A headless channel-kind instance is either a reviewer with roles exactly `["reviewer"]` or (#525-4) a writer whose roles include `implementer` and hold only `implementer` and `verifier`: it resolves to the kind's `headlessVariant`, needs its own `worktree` and accepts `permission_mode` `ask` or `ask-when-needed` only. `planner` needs `projection.plannerEligible`: `validateInstances` refuses it, `loadConductorPolicy` drops it with the ceiling notice, and `effectiveInstanceRoles` filters it for instance-keyed `roles`. A declared worktree never proves owner isolation (rule: `tasks.md`).
|
|
94
|
+
- Every start resolves `resolveInstanceExecutionCwd` and passes that `executionCwd` as adapter cwd, sandbox, grant and Pi session root; set `executionOwners` only for the adapter actually started, or at a verified terminal Claude hello (rule above). Its git calls share one `GIT_BUDGET_MS` allowance through `gitWithin` (#225): a call that finds it spent refuses without starting git; never give each call its own timeout. Extra-instance native state is per instance, while its MCP binding keeps the canonical hub state dir.
|
|
84
95
|
- Recovery: record an extra Codex launch row only after the start reply supplies `executionCwd`; keep `projectRoot`, `executionCwd` and `terminalRoot` separate and never bind an extra by kind metadata. Abort and archive validate receipts without current declarations; restore, fresh and reattach require them; a screen that only shows a recorded receipt reads it with `restoreReceiptStatus(value, { authority: "historical" })`, never for a decision. `instanceRecoverySourceRefusal` refuses before effects when the source lacks `external_adapter_modules` while `adapter_modules` is declared, and lacks `configured_acp` or `peer_instances` while a configured or declared id is not offline or the source's peer states are unreadable; a source whose such ids all read offline proceeds. Extra-instance turns write `turn_workspaces` in the `turns` transaction and leave the legacy snapshot fields unknown; undo requires the same trusted declaration and validated root.
|
|
85
96
|
|
|
86
97
|
- Name every Claude per-peer state file through `claudeStateFile` (`src/hub/claude-state.ts`) with that peer's id, never a hard-coded builtin name; pick a peer's `terminal-recovery.json` row only through `ownTerminalRow`. `sessionPointers` in `src/cli/reset.ts` must match any new per-peer pointer (#450 I3). Claude state files belong to Claude peers only: select those peers by `transport === "claude-channel"`, never by `identity.mcp === "channel-plugin"` or `turnEnd === "native-hook"` (agy has both), and `claudeStateFile` throws for any other id (#624).
|
|
87
|
-
- A declared Claude channel extra is never hub-started: keep it out of `STARTABLE_PEERS`, instance start modes and `RESERVED_IDS`. A launcher admits `--as <id>` only when the running daemon's status `activeInstances` lists it (`requireActiveInstance`), never from the config on disk.
|
|
98
|
+
- A declared Claude channel extra (one that resolves to the `claude-channel` definition) is never hub-started: keep it out of `STARTABLE_PEERS`, instance start modes and `RESERVED_IDS`. A launcher admits `--as <id>` only when the running daemon's status `activeInstances` lists it (`requireActiveInstance`), never from the config on disk.
|
|
99
|
+
- A declared instance that resolves to its kind's `reviewerVariant` or `headlessVariant` is hub-started (`hubStarted` from `instanceLaunchArguments`): `ahub <kind> --as <id>` refuses launch arguments other than the preview flags, sends one `start` request and returns (Claude #223 and #525-4, Codex #720). It never opens a TUI, writes a launch or terminal-recovery record, or execs; its preview prints `start: "hub"`.
|
|
88
100
|
- Verify a native Claude Stop and its start baseline only through `readClaudeTranscriptCompletion` (4 MiB backwards chunks, at most 64 MiB plus one boundary byte; incomplete or out-of-bound rows are unavailable), never the size-capped `readClaudeTranscriptUsage`; an `unavailable` refusal leaves the peer busy and notifies once per turn start (#517).
|
|
89
101
|
|
|
90
102
|
- Classify HEAD moves with bounded, append-fenced per-worktree reflog evidence. Exclude only incoming checkout/merge/pull/proven-fast-forward reset paths; preserve commit/amend/cherry-pick and sequencer replay changes. Take a rebase/pull --rebase start checkout's incoming paths from the single `merge-base --all` of old and upstream to upstream (no base or several is unknown), so replayed own commits stay attributed; a proven fast-forward reset stays incoming even when its target holds the peer's commit from another worktree (#612). Batch `diff-tree --stdin` only for single-parent pick and commit rows; diff squash, fixup, reword and edit rows one by one. Never exclude paths still dirty against end HEAD (autostash/checkout -m included). Unreadable or ambiguous movement/capture evidence is unknown, never an exclusion. Compute a concurrent change set once and publish no more than one conditional notice per affected task. Keep per-task-pair research events. Cache only raw checkout-branch probes within one synchronous burst, and reapply current privacy screening on every status rendering (#576).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Task board and hub tools
|
|
2
2
|
|
|
3
|
-
Scope: the task flow, assignment, the state machine and the hub's MCP tools. Read before editing `src/hub/tasks.ts`, `src/hub/board.ts`, `src/hub/routing.ts`, `src/hub/task-sweep.ts`, `src/hub/settled-roles.ts`, `src/hub/conductor.ts`, `src/hub/supervision.ts`, `src/hub/tool-refusal.ts`, `src/hub/hub-tools.ts`, `src/hub/review-brief.ts`, `src/hub/reviewer-pool.ts`, `src/hub/reviewer-pool-config.ts`, `src/hub/release-admission.ts`, `src/hub/plan-conformance.ts`
|
|
3
|
+
Scope: the task flow, assignment, the state machine and the hub's MCP tools. Read before editing `src/hub/tasks.ts`, `src/hub/board.ts`, `src/hub/routing.ts`, `src/hub/task-sweep.ts`, `src/hub/settled-roles.ts`, `src/hub/conductor.ts`, `src/hub/supervision.ts`, `src/hub/tool-refusal.ts`, `src/hub/hub-tools.ts`, `src/hub/review-brief.ts`, `src/hub/reviewer-pool.ts`, `src/hub/reviewer-pool-config.ts`, `src/hub/release-admission.ts`, `src/hub/main-checkout.ts`, `src/hub/checks.ts`, `src/hub/task-repository.ts`, `src/hub/review-findings.ts`, `src/hub/plan-conformance.ts`, `src/hub/sdlc-metrics.ts`, `src/hub/no-writes.ts`, `src/hub/workflow.ts`, `src/hub/deliverables.ts` or `src/hub/deliverable-files.ts`.
|
|
4
4
|
|
|
5
5
|
- Scope every task-place comparison through `TaskRepositories` (`src/hub/task-repository.ts`, #476): explicit `refs.repo`, else a common outside-absolute-ref repository or parent, else this project. Preserve relative display spelling within that key and gate overlap, contradiction, replay and held evidence by equal keys. Warm/cache repository identity at task mutation and startup; never repeat Git reads for unchanged overlap inputs. Set `refs.repo` for work in another repository; repo-only accept metadata never grants execution or file access.
|
|
6
6
|
- Tool callers are models: MCP `inputSchema` is not enforced on the way in. Normalize at the boundary (`cleanRefs`) before anything reaches the board, and never throw after a board write: a publish or notify that follows a `review()` or `done()` board write goes through the one `releaseNotice` guard, which turns a failed publish into a console notice, drops a failed notify and never retries (#231, #243). An assignment's delivery once its board write is saved (#276, #297): the move is fact, the failed publish is not retried in place (the journal's own recovery covers a storage error, and a retry may duplicate a publish that actually left), and a person looks for a queued or needs_review delivery with `ahub queue list --peer <owner>` (then `ahub queue show <id>`, or the accepted entry in `ahub task show <id>` if the journal is down) before re-sending with `ahub task assign <id> <owner>`. The distinction is made once, in `assignOwner`: a failure after the write prints the one console notice from `undeliveredNotice` right there, once, and throws `AssignmentUndeliveredError` carrying the saved task; callers (`review()`'s repeated rejection, `escalate` and its callers, the ready offer, both relays, the idle sweep, rescreen) decide control flow by the type and print nothing more, and `propose`, `assignTo` and `decline` additionally let the error reach the caller in its turn, which is a person or peer that can act. Two exceptions get no notice, by construction: a row superseded meanwhile, meaning a later history entry that moves or settles the task (another assignment, an unassign, the owner's accept, done, approval, close), while entries that do neither (the hub's quiet bookkeeping: answers, reviewer changes, idle sweeps, check results) leave the assignment undelivered and the notice stands, so the original error goes to the caller as before only on supersession; and a claim's own accepted write and "claimed by" line stay outside the delivery rule (a claim is its own hand-over; its overlap and routing announcements to earlier owners are deliveries and share the wrapper, so a failure there does read as one). A failure before the write stays a plain `Error` (#254), a failed write leaves the ready offer unconsumed (#231), and the notices before the write (waits, no-owner, the reserved-owner line) go through `releaseNotice` too, so those pre-write notices cannot fail their caller.
|
|
@@ -10,6 +10,7 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
|
|
|
10
10
|
- Capacity declines (#531): `hub_task_decline` takes an optional `capacity: true`, recorded as `capacity: true` on the `declined` history entry. `excluded()` skips such an entry, and `Tasks.decline` passes the decliner as `exclude` to that move's own reroute only, so a later reroute or planner pull may offer the task to it again from its reservation. Never infer a capacity decline from the note text, and never treat a row without the field as one: older rows are refusals. `plannerRefusal` (peer ids and counts only, never task text) names why `pullPlanned` found no owner, and `planBacklog` refuses a reservation for a peer `excluded()` returns. `assign()` stays pure, so `route explain` reads the same `excluded()` and `plannerFull` inputs as assignment.
|
|
11
11
|
- `assign()` never defaults to the task's current owner, or a decline can only come back to the decliner. A reservation (#207) is the same risk: the reserved owner goes first only through `blocked()`, and a decline or an escalation for reason `rejections` or `manual` records the owner it left in the history entry's `from`, which `excluded()` reads, so no later reroute hands the task back through its reservation.
|
|
12
12
|
- A named owner that `assign()` refuses is never a silent no-op (#601): `assignTo` passes `refuseNamed` so `assignOwner` throws with the routing refusal reason before any write (pre-effect for tool callers, a plain `Error` for the console), and a proposal's unmet named owner is reported in the propose result through `namedOwnerNote`, read from the stall the proposal recorded. The task stays where it was; never deliver to an offline peer to make the name stick.
|
|
13
|
+
- Write-class owners (#735): `ownerRoleOfClass` (`src/hub/routing.ts`) is the one derivation of the role that owns a class (`plan` planner, `review` reviewer, everything else implementer); the role ceiling and the owner-admission gate both use it, so never test a fixed list of class names; `explain` and `assign` stay the same function.
|
|
13
14
|
- The state machine allows `in_progress -> approved` only for classes without a reviewer; `review()` checks `in_review` itself.
|
|
14
15
|
- Treat `cancelled` as closed through `isClosedTaskState`, but keep dependency release restricted to approval. Cancellation goes through `Tasks.cancel`, preserves the row and roles, clears its reservation/cohort, sends revision-bound stop notices and writes no outcome memory; never map it to approval or cancel a native turn or worktree. Planner cancellation stays limited to its own historically unaccepted proposed/backlog work; accepted work remains person-only.
|
|
15
16
|
- Capture `TaskOffer.pullOnly` and the exact offer envelope id at publication. Keep unread-offer attention independent of attachment and restart until acceptance, decline, supersession or `offerRead` evidence; never replace that evidence with a generic inbox timestamp or completion state.
|
|
@@ -30,9 +31,10 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
|
|
|
30
31
|
|
|
31
32
|
- Pass off-roster ids as data into `assign()` so its trace and every owner/reviewer choice share the membership filter. Refuse a manual off-roster assignment before a board write, including a reservation. Use `reassignForRoster` through the existing assignment delivery path on removal, including in-review ownership while keeping its review state; keep the old owner or reviewer when no eligible replacement exists and emit the guarded public-title notice. Preserve budget-relay behavior when sharing that implementation.
|
|
32
33
|
|
|
33
|
-
- Criteria checklists (`criteria`, retired alias `phase`) are written only through Tasks.recordLifecycle by that task's reviewer/person, after the PII screen. Refuse a checklist task list before any write; never change dependencies, accepted owners or reservations from a checklist. Keep verified checklist claims separate from externally read CI, and bind them to the current reviewed head. Screen `checklistOf(next)`, never `next.criteria` alone: off stores a phase write under `phase`. Plain off (no retired `workflow`) keeps the #257 receipt bytes, the schema-1 export and the `phase` tool schema; advertise `criteria` only when daemon status carries `lifecycleCriteria`. Export only derived sanitized board/config projections.
|
|
34
|
+
- Criteria checklists (`criteria`, retired alias `phase`) are written only through Tasks.recordLifecycle by that task's reviewer/person, after the PII screen. Refuse a checklist task list before any write; never change dependencies, accepted owners or reservations from a checklist. Keep verified checklist claims separate from externally read CI, and bind them to the current reviewed head. Screen `checklistOf(next)`, never `next.criteria` alone: off stores a phase write under `phase`. Plain off (no retired `workflow`) keeps the #257 receipt bytes, the schema-1 export and the `phase` tool schema; advertise `criteria` only when daemon status carries `lifecycleCriteria`. Export only derived sanitized board/config projections. A verdict's `unmetCriteria` (#507b) comes only from `unmetCriteriaIds` (`src/hub/workflow.ts`) over the board's checklist at the verdict write, never from the caller, and stays empty for a PII task.
|
|
34
35
|
|
|
35
|
-
- A planner patch that names `reserved` at all, the same value included, is refused while `hasPersonReservation` holds
|
|
36
|
+
- A planner patch that names `reserved` at all, the same value included, is refused while `hasPersonReservation` holds (#378).
|
|
37
|
+
- A planner pull of a reserved backlog task (#718) admits only that exact peer, whoever reserved it: refuse when it is excluded, ineligible or at its planner WIP cap, never fall back to another peer. It goes through `assignOwner` with `plannerPull`, whose `guard` rechecks authority, revision and owner right before `Board.pullAssigned` writes `backlog pulled` and the assignment in one transaction; a stale prior offer is withdrawn, never reused, and the pull is not undoable through planning. An unreserved pull stays the ownerless `Board.pullPlanned` move.
|
|
36
38
|
- `done()` gates the SDLC transition once: pass `gated` to `complete()` when nothing was awaited since, and after any awaited gate readback refuse if another done already moved the task, in every mode (#382).
|
|
37
39
|
- A changed `reviewedHead`, or one supplied with a changed repo or PR, needs the reviewer or a person; a head dropped because the owner relinked the PR does not (#382).
|
|
38
40
|
|
|
@@ -46,9 +48,10 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
|
|
|
46
48
|
|
|
47
49
|
- Resolve any id that may be an extra instance or a configured kind through the startup-bound project catalog: `this.d.adapterCatalog` with `this.d.instances?.()` in Tasks, `opts.adapterCatalog` in `assign()` and `relayCandidates`, and the `adapterCatalog` hook in `Conductor`/`conductorStart`; a bare `definitionOf(id)` answers builtins only. Only a kind's routing preference expands to its instances; manual ids, reservations and nominations stay exact. Reviewer independence compares families: take the own family only when no other is eligible, record it through `assignmentNote` (`sameFamilyFallback=true`), explicit nominations included; persist the `instance`/`family` refusal codes as `policy`.
|
|
48
50
|
- Tool publication (`projectToolList`, `projectConductorTools`, `configuredToolContext` in `src/hub/hub-tools.ts`) only narrows the list or adds start targets, using authenticated status metadata (`configuredAdapters`, `startablePeers`, `readOnlyPeers`); in a configured project, malformed or incomplete metadata publishes the read-only set and adds no start target. Publication is never authority: `taskOp` refuses read-only mutations independently, and MCP task and conductor refusals keep their text and set `isError: true`.
|
|
49
|
-
-
|
|
51
|
+
- SDLC metrics and bands (#481 S7): `sdlc-metrics.ts` is pure (history in, numbers out, no clock, no I/O); every metric a task cannot derive is `null`/`unknown`, never 0, and a private task is only counted as withheld. Stale-head re-reviews need both heads on a round (`reviewCost.head` on the done, `reviewFindings.head` on the verdict): never derive them from event order, the board cannot produce an `approved` followed by a `done`. `Tasks.sdlcMetrics` is the one read (planner or person, refused during a private turn) and `Tasks.proposeFromBands` the one writer. It is held like every host writer (`sweepHeld`, `bus.isRecoveryHeld`, `bus.storageError`), keys a breach by its episode (`BandResult.episode`, derived from history so a restart finds the same key), writes nothing while the metric has an open band card, and records a log notice in `band_notices` so it is announced once. Card text is fixed words, numbers and task ids, screened through `signalsFor` and `candidateTextAllowed` like the recurring-finding card; never put task titles, notes or PR text in it, and never open a GitHub issue. Drift honours `min_delta` like every tier. `[bands]` is validated by `bandsConfig` with field-level paths; do not add a tier or a metric without a test that fires it.
|
|
52
|
+
- Admit a declared extra (a person-started `ahub claude --as` instance and a headless Claude implementer included, #742, #747) as owner of any class whose owner role is implementer (`ownerRoleOfClass`) only with the current implementer role and `ownerIsolation[id] === true`, the daemon's proof that the live adapter generation started in `executionRoots[id]` outside the canonical cwd. Never derive it from the declaration's `worktree`, its roles or a reload.
|
|
50
53
|
|
|
51
|
-
- Lifecycle identity (#694): the hub links `sdlc.repo`/`issue` only through `Tasks.linkIssue` (the one issue `planIssueIdentity` resolves) at plan review, done and the groom backfill, and stages a done's PR through `Tasks.identityLink`/`withLink` (the link carries the repository and the commit or branch it was read for, and is dropped when the done's effective repository or head changed during the read; the issue is re-resolved at apply time; `reviewRequestAdmission` compares the board copy through the same `stage` projection) (`openPrFor`: the one open PR whose head is the done's full commit, or with no commit its branch; a full 100-row page or a malformed row is unknown). Persist a done's link only in the admitted done update or `done (checking)`, never before a gate that can still throw `PreEffectToolRefusal` (#254); reapply it after every re-read in `complete()`. Write a first link with `linkedReceipt`, never `sdlcReceipt` (its relink rule would drop red-first and local-gate records); never relink an existing or foreign identity, never parse agent text for a PR, skip PII tasks before any read, and run the groom backfill after its authority checks with its own bound.
|
|
54
|
+
- Lifecycle identity (#694): the hub links `sdlc.repo`/`issue` only through `Tasks.linkIssue` (the one issue `planIssueIdentity` resolves) at propose and accept (`linkOnWrite`, #723: a local parse, never a read, never a refusal, advise and enforce only), plan review, done and the groom backfill (`backfillIssueLinks`: first in `groomingDigest`, 32 links and 1 s per call, before the slow metadata reads), and stages a done's PR through `Tasks.identityLink`/`withLink` (the link carries the repository and the commit or branch it was read for, and is dropped when the done's effective repository or head changed during the read; the issue is re-resolved at apply time; `reviewRequestAdmission` compares the board copy through the same `stage` projection) (`openPrFor`: the one open PR whose head is the done's full commit, or with no commit its branch; a full 100-row page or a malformed row is unknown). Persist a done's link only in the admitted done update or `done (checking)`, never before a gate that can still throw `PreEffectToolRefusal` (#254); reapply it after every re-read in `complete()`. Write a first link with `linkedReceipt`, never `sdlcReceipt` (its relink rule would drop red-first and local-gate records); never relink an existing or foreign identity, never parse agent text for a PR, skip PII tasks before any read, and run the groom backfill after its authority checks with its own bound.
|
|
52
55
|
- Spec acceptance (#481 S1) is written only by `Tasks.acceptSpec` for the person; never accept it from a tool or a receipt. The daemon uses `Tasks.acceptReviewed` to bind spec and plan readbacks to one task snapshot before `Tasks.accept`; retain its synchronous missing-spec and plan-admission refusals. Apply WIP and plan admission before `done` implicitly accepts proposed work and before cohort writes. Never read or store an issue body: identity and edit times only.
|
|
53
56
|
- Test integrity (#481 S4, `src/hub/test-integrity.ts`): in enforce only the person relinks an established `sdlc.repo` or `issue`. A supplied `redFirst` that fails `publicMetadataText` on a public task is refused pre-effect, and `publicView` drops one that fails it later. Integrity is read only for a non-PII task with current PR evidence; otherwise it is unknown with a blocker, never a pass. Enforce adds `test integrity:` blockers to review admission and the merge queue. All gh reads of one lifecycle gate or status share one 20 s deadline; never give each read its own.
|
|
54
57
|
|
|
@@ -56,12 +59,12 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
|
|
|
56
59
|
- Cost reports recheck planner/private-turn authority and current linked privacy before each underlying external read and final projection. Do not emit a stale/relinked observation as a complete zero-cost row. S7 owns band configuration and automatic proposal writes.
|
|
57
60
|
|
|
58
61
|
- `Tasks.review` captures `reviewAuthority` once and rechecks it in `valid()` after every await, right before each write; a headless reviewer's verdict, and its `hub_task_show` (checked before and after the read), holds only while the same child runs for that task at its bound envelope head and review revision (`Tasks.reviewBindingCurrent`, host-only: `in_review`, same reviewer, non-PII, matching `reviewRequestRevision` and review head). Never cache the answer (#223, #533).
|
|
59
|
-
- Keep structured review findings in verdict history, bound to the reviewed head. Validate bounded reports and person-only accepted dispositions before effects; recheck authority, privacy and head identity after awaits. A supplied `head` that differs from the task's resolved head (`findingHead`) is refused pre-effect, and the resolved head is part of the identity rechecked after the screen (#657). Key findings by `id` alone, never by pass (`findingsForHead`): a nit or open reclassification never discharges an Important, only an explicit disposition does. Omitted IDs never settle findings; carry open Important across heads until explicitly dispositioned, while nits stay head-local. Use the shared valid-head resolver for projections. Project text through publicView, and deduplicate recurring public pass/id pairs into backlog proposals through durable `Review finding key:` markers read only from hub-proposed `plan` cards (first history entry by `hub`), without editing instruction files.
|
|
62
|
+
- Keep structured review findings in verdict history, bound to the reviewed head. Validate bounded reports and person-only accepted dispositions before effects; recheck authority, privacy and head identity after awaits. A supplied `head` that differs from the task's resolved head (`findingHead`) is refused pre-effect, and the resolved head is part of the identity rechecked after the screen (#657). Key findings by `id` alone, never by pass (`findingsForHead`): a nit or open reclassification never discharges an Important, only an explicit disposition does. Omitted IDs never settle findings; carry open Important across heads until explicitly dispositioned, while nits stay head-local. Use the shared valid-head resolver for projections. Project text through publicView, and deduplicate recurring public pass/id pairs into backlog proposals through durable `Review finding key:` markers (a pair recurs only across separate work: tasks sharing a PR, a branch in one repository or a supersession count once, `workUnits`, #736) read only from hub-proposed `plan` cards (first history entry by `hub`), without editing instruction files.
|
|
60
63
|
- Plan conformance (#481 S2, `src/hub/plan-conformance.ts`): compare only literal repository-relative `plan.paths` entries (file or directory prefix, no glob) with complete PR file metadata; `completePlanInventory` requires a full head, `pathsComplete` and `typesComplete` (false on any rename or copy). Root, absolute-unverified or unsafe entries, a missing plan, a truncated inventory and a failed read are `unknown`, never covered; never read patches or the filesystem. An untouched planned path is advice; a changed path outside the plan is a departure, and `planConformanceBlockers` blocks enforce approval on `unknown` and on an unaccepted departure. Plan `proof` and `risks` (32 strings of 512 characters) cover no file. `Board` records `planRevision` only while `trackPlanRevisions` (mode not off), separate from backlog `planningRevision`.
|
|
61
64
|
- Departure consent is written only by `Tasks.acceptPlanDeparture` for the person (`ahub sdlc accept-departure <task> <head> <reason...>`; no peer tool) on a currently known departure at the current PR head, never on `unknown`, and never for a PII task. It binds repo, PR, head, plan revision and fingerprint, the inventory, `planScopeIdentity` and this host's `departureGeneration`, so a restart or a change to any of them invalidates it (`validPlanDepartures`). Status and merge-queue reads never write the board: they only note observations and invalidations in memory (`observedPlans`, `invalidatedPlanDepartures`), which `planObservation()` attaches to the next owner or reviewer transition write. Off captures no observation or revision; a plan or refs change still invalidates an existing acceptance.
|
|
62
65
|
- The review coverage gate (#520) pins `refs.commit` and the sdlc receipt and rechecks them in `valid()`; an unavailable brief is never an empty one (enforce refuses, advise notes). Read brief attributes and rules at the merge base (`--attr-source`, `check-attr --source`, `git show <base>:`), never from the head or the checkout.
|
|
63
66
|
- A `routing.reviewOnly` definition is refused as owner inside `assign()`'s eligibility check, for every class, nomination and reservation; keep it there, not at call sites. A definition with `reviewer.unavailable` is refused as reviewer there too (code `policy`); unavailable never means pass.
|
|
64
|
-
- A definition's `routing.allowedRoles` is a fixed role ceiling (#543): `assign()` refuses a peer whose required role (reviewer for the reviewer role or the `review` class, planner for `plan`, otherwise implementer) is outside it, with code `policy`, and `loadConductorPolicy` drops project `roles` entries outside it with a config notice. Never widen
|
|
67
|
+
- A definition's `routing.allowedRoles` is a fixed role ceiling (#543): `assign()` refuses a peer whose required role (reviewer for the reviewer role or the `review` class, planner for `plan`, otherwise implementer) is outside it, with code `policy`, and `loadConductorPolicy` drops project `roles` entries outside it with a config notice. `planner` also needs `projection.plannerEligible` (#525-1): `loadConductorPolicy` drops it from a kind's entry and `effectiveInstanceRoles` from an instance-id entry. Never widen either from project config.
|
|
65
68
|
- Reviewer pool (#519): `selectReviewerPool` is pure and called only inside `assign()`, which builds the candidates (declared reviewer-only `routing.reviewOnly` instances) and their blocked reasons; Tasks passes only `reviewPool` (`pool`, `rerotate`, `failed`, `required`). With no fresh reviewer declared and no configured `review.pool`, routing stays the legacy path. The seed is the linked PR number, else the task id; compute its modulo over configured kinds with declared candidates, including blocked/offline/paused/quota/failing candidates, and do not renumber by availability; append undeclared-kind trace entries (`no declared reviewer`) after the declared walk without assigning them modulo shares or changing a selected reason; the implementer's family is skipped while another can run, and a delta review keeps the last `changes_requested` family unless `rerotate`. `required` (an explicit `review.pool`) exhaustion goes to the person with `reviewBlocked`; only the optional default pool may fall back to an eligible planner, other family first. Record the choice through `reviewMetadata` (`reviewerFamily`, `reviewerReason`, including `same-family fallback`), never in note text alone. PII tasks, `review`-class tasks and a reviewer a person named (`reviewer changed`, reason `manual`) stay outside the pool.
|
|
66
69
|
- `Tasks.retryReviewer` is host-only (the daemon's reviewer `onUnavailable`), never a tool or CLI path. It acts only while the failed peer is still the reviewer of an `in_review`, non-PII, not manually named task, with the same review head, the request's `reviewRequestRevision` and a current child generation; `valid()` rechecks all of it after every await and right before the write, which also passes the flow revision. Each failed peer is tried once per head and round (`reviewerFailure` history). `reviewRequestRevision` counts only role events (`done`, `reviewer changed`, `reviewer unavailable`, ownership), so bookkeeping never fences a request; keep `refs.reviewRevision` and `refs.commit` on the review envelope.
|
|
67
70
|
- Release trains (#481 S11): change trains only through `Tasks.newRelease`, `configureRelease`, `assignRelease` and `cutRelease` (planner or person), and refuse every `hub_release_*` in a PII turn. `assignRelease` writes only `release` and refuses a frozen, shipped or dropped release on either side; private or pinned scope is person-only. One active or frozen train at a time. A cut freezes only through `Board.freezeRelease` after a fresh `releaseWindow` (host lease from `readReleaseHostAdmission`, quota, `current()`, at most 5 s old); unknown evidence defers, and the window is closed in `finally`. A cut never publishes, tags or upgrades, and upgrade and recovery never consult `releaseWindow`. Keep two of `RELEASE_TRAIN_HISTORY_LIMIT` slots for cut and ship; a configuration no-op appends nothing. An S9 plan receipt survives only contiguous planning revisions that changed exactly `["release"]`.
|
|
@@ -73,3 +76,8 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
|
|
|
73
76
|
- At done, add `no writes recorded since acceptance` to review only when the daemon proves no owner touches since the first acceptance after the current ownership boundary and an empty acceptance-to-now diff, with no branch, review head or PR evidence. Preserve that baseline and earlier touches across plan-revising re-acceptance. Require a canonical-root D2 result to pass the same elsewhere-hint and exclusive-current-worktree checks before treating it as negative evidence. Bind extra-instance evidence to its current verified execution owner/root; missing, private, disabled, expired-budget or restart-lost baselines stay unknown. Capture through the guarded `TasksDeps.ownerAccepted` callback for explicit accept/self-claim only; an implicit done acceptance has no pre-work baseline and remains unknown. Do not capture private work for this advisory, block done, or accept evidence flags from models.
|
|
74
77
|
|
|
75
78
|
- `ahub report --dogfood` (#687, `src/hub/dogfood.ts`, `src/hub/dogfood-read.ts`) opens `hub.db` read-only with byte and row caps; an unavailable or incomplete source reports `null`, never 0, and per-PR GitHub reads run only when the task privacy inventory is complete.
|
|
79
|
+
- Qualified titles (#723): `planIssueIdentity` links a sibling `repo#N` only when `refs.repo` names that repository (`siblingTitleRefs`, `spec-read.ts`), and an `owner/repo#N` only when it is `refs.repo`, else this project's repository; a sibling shorthand beside a bare `#N` is two identities and links neither. A title that names another repository with no `refs.repo` does not link, and `refsRepoHint` tells the proposer to set it (advice in the `hub_task_propose` result, never a refusal; none in mode off, for a PII task or once `refs.repo` or an issue is set). There is no retitle path on the board: a title is fixed at propose.
|
|
80
|
+
|
|
81
|
+
- Planner walk and reviewer independence (#525-3): `plannerWalk` and `implementers` in `src/hub/routing.ts` are pure; the planner is chosen only inside `assign()` (`planners`, `namedPlanner`, `keptPlanner` in, `Assignment.planner`/`plannerReason` out), and `Tasks` never picks one outside it. `Tasks.weights()` is the one place `planners` is built (rostered holders of `d.roles`, sorted), so every `assign()` path and `explain` agree. Rotation is `task.id mod n` over every holder and is never renumbered by availability; a named planner is exact and never replaced. I-2 (`implemented this task`) lives in `blocked()` for the reviewer role as its LAST check, so the legacy, named and pool paths share it and it only ever removes an otherwise eligible candidate; when I-2 removed every otherwise eligible reviewer, the person reviews with `reviewBlocked`, never an implementer. 525-3 records nothing: do not read `Assignment.planner` into a board write, notice or authority check until 525-2 does it with its own tests.
|
|
82
|
+
- Owned implementer child (#747): `propose`, `acceptReviewed`, `done` (through `complete()`) and `remember` take `requestCheck(by)` (`TasksDeps.requestAuthority`) at entry and call it after every await, before the next write; a new await on those paths adds a call, and its refusal stays pre-effect. The child's tool surface is the fixed `IMPLEMENTER_HUB_TOOL_NAMES` (`src/hub/hub-tools.ts`); never widen it from project roles.
|
|
83
|
+
- Deliverables (#509a, `src/hub/deliverables.ts`, `src/hub/deliverable-files.ts`): the hub hashes each file itself under the owner's verified execution root (`TasksDeps.executionRoot`, the daemon's `ownerWriteRoot`; an unproven root refuses); never accept a caller's hash, size or unknown field. Vet and hash before any board write and refuse pre-effect; a `done (checking)` carries its records to `complete()`, never a later read. An approval rereads the delivered files: a reviewer's verdict is refused before any write unless every file is `unchanged`, and only the person approves moved files, which are rehashed and named in the note. A file the hub cannot read or vouch for is `unknown`, never `unchanged`. Peer-facing records go through `publicDeliverables`: for a PII task or a `pii.paths` file, id, hash and size only.
|