@staix/agent-hub 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/README.md +23 -9
- package/docs/agent-notes/README.md +8 -0
- package/docs/agent-notes/adapters.md +21 -4
- package/docs/agent-notes/budget.md +6 -4
- package/docs/agent-notes/bus.md +1 -0
- package/docs/agent-notes/daemon.md +20 -9
- package/docs/agent-notes/local-worker.md +2 -2
- package/docs/agent-notes/tasks.md +21 -4
- package/docs/agent-notes/tests.md +6 -3
- package/docs/dogfood.md +19 -5
- package/docs/events.md +21 -7
- package/docs/memory.md +78 -10
- package/docs/operations.md +197 -39
- package/docs/profiles.md +1 -0
- package/docs/quickstart.md +16 -6
- package/docs/sdlc.md +17 -0
- package/docs/security.md +110 -21
- package/docs/smoke.md +7 -3
- package/docs/specs/2026-09-19-agent-hub-design.md +156 -16
- package/docs/verification/2026-10-23-0.15.0.md +80 -0
- package/docs/verification/2026-10-23-daemon-responsiveness.md +44 -0
- package/docs/verification/2026-10-23-headless-claude-permissions.md +47 -0
- package/docs/verification/2026-10-23-terminal-claude-execution-root.md +41 -0
- package/docs/verification/2026-10-24-always-ask-adapters.md +52 -0
- package/docs/verification/2026-10-24-amem-runtime.md +69 -0
- package/docs/verification/2026-10-24-codex-0164-reviewer-isolation.md +55 -0
- package/docs/verification/2026-10-24-delivery-holds-ac8.md +264 -0
- package/docs/verification/2026-10-24-hardcoded-values.md +2108 -0
- package/docs/verification/2026-10-25-0.16.0.md +82 -0
- package/docs/verified.json +40 -19
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +3109 -2046
- package/src/adapters/acp.ts +46 -7
- package/src/adapters/claude-channel.ts +26 -4
- package/src/adapters/claude-implementer.ts +243 -0
- package/src/adapters/claude-print-child.ts +90 -0
- package/src/adapters/claude-task-sessions.ts +80 -0
- package/src/adapters/codex-appserver.ts +69 -1
- package/src/adapters/compat.ts +6 -2
- package/src/adapters/conformance/enrollment.ts +2 -1
- package/src/adapters/conformance/fakes/acp-server.ts +3 -1
- package/src/adapters/definitions/builtin-metadata.ts +17 -5
- package/src/adapters/definitions/native-channel-state.ts +1 -1
- package/src/adapters/definitions/native-cli-entry-claude.ts +13 -8
- package/src/adapters/definitions/native-cli-entry-types.ts +2 -0
- package/src/adapters/definitions/native-factories.ts +8 -0
- package/src/adapters/kimi-wire-usage.ts +303 -0
- package/src/adapters/local-worker.ts +5 -4
- package/src/adapters/registry-types.ts +8 -4
- package/src/adapters/registry.ts +14 -2
- package/src/adapters/reviewer-print.ts +30 -53
- package/src/cli/console-config.ts +123 -0
- package/src/cli/console-state.ts +48 -3
- package/src/cli/console-symbols.ts +3 -2
- package/src/cli/console-usage.ts +5 -2
- package/src/cli/console.ts +30 -2
- package/src/cli/facts-hook.ts +9 -5
- package/src/cli/help.ts +10 -7
- package/src/cli/identity.ts +3 -1
- package/src/cli/launch.ts +10 -4
- package/src/cli/main.ts +91 -19
- package/src/cli/memory.ts +119 -7
- package/src/cli/output.ts +40 -4
- package/src/cli/reset.ts +1 -1
- package/src/cli/settings-edit.ts +89 -0
- package/src/cli/terminal-recovery.ts +1 -6
- package/src/hub/action-class.ts +211 -17
- package/src/hub/action-enforce.ts +90 -0
- package/src/hub/action-policy.ts +40 -17
- package/src/hub/ask.ts +16 -4
- package/src/hub/board.ts +110 -31
- package/src/hub/budget-notes.ts +65 -0
- package/src/hub/budget.ts +44 -7
- package/src/hub/bus.ts +6 -6
- package/src/hub/claude-launch-proof.ts +33 -0
- package/src/hub/code-facts.ts +56 -16
- package/src/hub/conductor.ts +5 -2
- package/src/hub/config-apply.ts +12 -0
- package/src/hub/config-edit-host.ts +173 -0
- package/src/hub/config-edit.ts +282 -0
- package/src/hub/config-tier.ts +89 -0
- package/src/hub/control-client.ts +24 -3
- package/src/hub/daemon.ts +614 -104
- package/src/hub/deliverable-files.ts +131 -0
- package/src/hub/deliverables.ts +125 -0
- package/src/hub/delivery-journal.ts +18 -2
- package/src/hub/envelope.ts +1 -1
- package/src/hub/events.ts +5 -3
- package/src/hub/hub-tools.ts +22 -6
- package/src/hub/instances.ts +7 -4
- package/src/hub/kimi-usage.ts +156 -0
- package/src/hub/lifecycle.ts +10 -5
- package/src/hub/manager.ts +1 -1
- package/src/hub/memory-code-head.ts +44 -0
- package/src/hub/memory-code-overlay.ts +119 -0
- package/src/hub/memory-plan-guard.ts +10 -1
- package/src/hub/memory-stale-anchors.ts +159 -0
- package/src/hub/memory-symbols.ts +74 -0
- package/src/hub/peers.ts +7 -0
- package/src/hub/permission-payload.ts +15 -0
- package/src/hub/planning.ts +10 -4
- package/src/hub/read-tools.ts +21 -3
- package/src/hub/routing.ts +118 -3
- package/src/hub/sdlc.ts +34 -0
- package/src/hub/settings.ts +18 -3
- package/src/hub/settled-roles.ts +15 -3
- package/src/hub/task-sweep.ts +25 -3
- package/src/hub/tasks.ts +551 -75
- package/src/hub/token-window.ts +46 -0
- package/src/hub/usage-rollup.ts +16 -6
- package/src/hub/window-kind.ts +13 -0
- package/src/hub/workflow.ts +16 -0
- package/src/local/tools.ts +5 -5
- package/src/memory/brief.ts +22 -12
- package/src/memory/capture.ts +26 -40
- package/src/memory/client.ts +137 -65
- package/src/memory/recall.ts +13 -22
- package/src/ui/index.html +215 -19
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,48 @@ 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.16.0
|
|
8
|
+
|
|
9
|
+
- The project daemon stops through its normal stop on SIGHUP and SIGQUIT too, not only SIGINT and SIGTERM, so memory sync git (detached since agent-mem 0.7.0) never outlives the hub; SIGQUIT no longer reaches its default core dump (#438).
|
|
10
|
+
- Raise the exact `@staix/agent-mem` pin to 0.7.0: standalone setup, notes and the review queue, decay and promotion candidates, recall validity with person-only history, intent briefs and use telemetry, doctor and the sync cadence. The hub passes its own command names (`ahub memory`, `ahub goal`) into the library's usage and refusal text. A generation home now lists its candidate review queue instead of refusing it. agent-mem 0.7.0 runs memory sync git in its own process group, which the daemon stops on all four exit signals (#438).
|
|
11
|
+
- Add read-time memory code neighbours (#684): opt-in dashboard calls with commit-stamped dashed edges, bounded symbol graph overlays, fails-first related task briefs and a console note count. Code edges are never stored; unknown and partial evidence stay explicit.
|
|
12
|
+
|
|
13
|
+
- Fix #758: the planner "board changed" notice skips the planner whose own write changed the board, tells only a card's planners, replaces an unsettled copy so a planner meets the latest revision once, and is recorded at `fyi` weight (no turn) for state moves, reviews and assignments, at most once per planner per ten minutes. A planning revision or dependency change, and a new card, are still task notices; the hub's own bookkeeping entries tell nobody.
|
|
14
|
+
- Verify the Codex 0.164.0 fresh reviewer (#757): the model-free production `codexReviewerBackend.prepare` passed every isolation leg under temporary HOME and CODEX_HOME (empty and synthetic-ambient configuration, all native canaries) and the six source claims were rechecked at upstream `rust-v0.164.0`, so `COMPAT["codex-review"].verified` lists 0.164.0 and `ahub codex --as codex-review` no longer needs `ahub agents trust` on it. Receipt: `docs/verification/2026-10-24-codex-0164-reviewer-isolation.md`.
|
|
15
|
+
- Memory symbol follow-ups (#684): reuse one exact-tree preparation and unknown name lookups at done, query historical raw signatures alongside normalized nodes, label index-only absence, and keep optional doctor cleanup failures neutral. Transient worktree cleanup remains a person action.
|
|
16
|
+
|
|
17
|
+
- Add the `closing-refs` check (#760): a PR workflow fails a pull request whose `closingIssuesReferences` names an issue that its title and body only mention with `Refs`, a negation or a qualifier ("Does not close #15" is a closing keyword to GitHub), and one whose commit messages carry a closing keyword the PR text does not mean. AGENTS.md gains the rule.
|
|
18
|
+
- Fix #762: the always-ask classifier no longer drops a command at its first shell expansion. `git push origin $B`, `npm publish $D`, `gh issue create --title "$T"` and `rm -rf "$DIR"` are classed from their literal words, the command inside `$(...)` or a backtick is classed too, and where the opaque part can change the class (`git $CMD`, `gh api -X $M`, `gh api graphql -f query=$Q`, an opaque `rm` target) the strictest reachable class is used. A command with no classed literal prefix stays unclassed. `gh repo delete|archive|edit`, `gh secret|variable set|delete` and `gh label create|edit|delete` are `external-write`.
|
|
19
|
+
- Fix #756: the always-ask classifier classes `gh api graphql` by operation. One literal `query` field whose every operation is a query (or shorthand `{...}`) is a read and no longer asked about; a mutation, a subscription, a second query field, a query from a file or stdin and any unparsable document stay `external-write`. `gh run rerun|cancel|delete` and `gh workflow run|enable|disable` are `external-write`. REST `gh api` is unchanged.
|
|
20
|
+
- Backlog planning (#759): dependency-ordered phases sharing an issue are no longer duplicate candidates; overlap findings remain. Effective planners/persons relink only repo/issue identity on unstarted backlog through a planning revision, invalidating old spec acceptance. Omitted/empty tool fields record reason-only revisions; CLI `{}` and tool/help descriptions match.
|
|
21
|
+
|
|
22
|
+
- Add per-task and release planners (#525-2): one or up to eight explicit planner holders, person-only planner reassignment, scoped planner tools and notices, independent plan review with recorded authorship/self-review, and a persisted-authority recovery floor. Legacy null bindings retain their existing authority; singleton settings remain strings.
|
|
23
|
+
|
|
24
|
+
- `ahub deliverables [--task <id>] [--approved] [--check] [--json]` and a dashboard badge (#509b, #317): the person's list of what each task delivered and a reviewer approved (id, hash, size, and a path only where the PII screen allows), `--check` measures the files now and marks one `changed since review`. The dashboard card shows a `Deliverables n` chip and the task sheet a Deliverables group from the board's records, never a file read on the poll; a measurement shows only after a live read. An agent's shell is refused. No new stored data.
|
|
25
|
+
- Canonical task-memory symbol anchors (#684 phase 3): completion notes use current observed symbols with positive code-facts evidence, earlier notes stay connected through their task, and `doctor --stale-anchors` provides a bounded read-only report. Unknown or private evidence never falls back to plan guesses.
|
|
26
|
+
|
|
27
|
+
- Replace claude-mem worker runtime calls with exact-pinned amem 0.6.0 (agent-mem#13 P4): curated recall, briefs and ask use in-process storage; local capture is deterministic and model-free. Privacy and selected-profile fences remain; raw capture is excluded from evidence. Auxiliary model/checkpoint writes become pending candidates. Claude-mem remains an explicit import source only.
|
|
28
|
+
|
|
29
|
+
- Budget window kinds, precedence and notes (#285 phase 4, AC6, AC7, AC8, AC10): `UsageWindow` gains an optional `kind` (`native`, `count`, `manual`). `ahub budget` prints each window's kind, source and age, marks a hub count that a native or manual reading outranks as `info only`, and says why a peer shows no number (Pi and local have no quota and show the hub's own 5 h token count; Kimi without `budget.kimi_tokens_5h`, or with an unreadable wire log, says so). A fresh `ahub budget set` value replaces the provider's and the hub's for its window at the gate and in routing headroom, and a provider refusal clears it. The console peer and Usage details and the dashboard Usage budgets row show kind and source, and `ahub doctor` gains `kimi usage source`. `PROTOCOL` is unchanged.
|
|
30
|
+
- Complete the remaining always-ask adapter paths (#636): received Codex command approval RPCs, local worker and Pi model tools share the classifier and explicit class-budget gate. Codex native no-request calls remain not enforceable in status; local/Pi modes cannot suppress a classed prompt. The local/Pi console shows the complete command even during a private turn; cut or withheld native views are deny-only. Idle headless Claude instances report no current enforcement (#402); weakened local startup policies surface attention.
|
|
31
|
+
|
|
32
|
+
## 0.15.0
|
|
33
|
+
|
|
34
|
+
- `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).
|
|
35
|
+
- 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`.
|
|
36
|
+
- 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.
|
|
37
|
+
- 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.
|
|
38
|
+
- 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.
|
|
39
|
+
|
|
40
|
+
- 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.
|
|
41
|
+
|
|
42
|
+
- 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.
|
|
43
|
+
- 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).
|
|
44
|
+
- 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.
|
|
45
|
+
- 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).
|
|
46
|
+
- 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).
|
|
47
|
+
- 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.
|
|
48
|
+
|
|
7
49
|
## 0.14.0
|
|
8
50
|
|
|
9
51
|
- 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.
|
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.16.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
|
|
@@ -35,22 +35,36 @@ ahub ui # the web dashboard
|
|
|
35
35
|
into digests, priorities decide what interrupts a turn, and delivery survives
|
|
36
36
|
restarts.
|
|
37
37
|
- **A shared task board.** Work is proposed, accepted, done and reviewed through hub
|
|
38
|
-
tools. Roles split planning, implementation and review
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
tools. Roles split planning, implementation and review (up to eight planners,
|
|
39
|
+
each task bound to one), `routing.toml` picks
|
|
40
|
+
owners by task class, and owners of overlapping work are told what changed. A
|
|
41
|
+
peer that accepted or finished a task never reviews it, a review request lists
|
|
42
|
+
the task's acceptance criteria by id, and a done report can name deliverable
|
|
43
|
+
files that the hub hashes itself, so an approval binds the bytes reviewed, and
|
|
44
|
+
`ahub deliverables` lists them. Extra
|
|
45
|
+
Claude Code instances can implement in their own worktree, in a terminal or
|
|
46
|
+
headless with one session per task. `ahub sdlc metrics` reads lead times,
|
|
47
|
+
rework and stalls from board history, and
|
|
41
48
|
optional control bands turn a breach into a backlog card.
|
|
42
49
|
- **Shared memory.** Decisions, contracts, fails and findings are kept through the
|
|
43
|
-
first-party [`@staix/agent-mem`](https://github.com/STAIxBWLB/agent-mem) library
|
|
44
|
-
(short- and long-term tiers, a typed graph,
|
|
50
|
+
first-party [`@staix/agent-mem`](https://github.com/STAIxBWLB/agent-mem) library,
|
|
51
|
+
in process with no claude-mem worker (short- and long-term tiers, a typed graph,
|
|
52
|
+
git sync between machines). Agents get
|
|
45
53
|
it as a recall preface, as briefs on a task, and through bounded search tools.
|
|
46
54
|
- **Local and on-prem models.** Pi and the local worker can run on DGX or local MLX
|
|
47
55
|
through an authenticated relay. Tasks marked as PII stay on the on-prem worker.
|
|
48
56
|
- **Guard rails.** The local worker and Pi run in a sandbox, and their writes and
|
|
49
57
|
shell commands wait for your approval unless you choose a looser permission mode.
|
|
50
|
-
|
|
58
|
+
Publish, external-write, destroy and install calls ask a person in every mode for
|
|
59
|
+
Claude Code, the local worker, Pi, hub-owned ACP peers such as Kimi and the
|
|
60
|
+
approval requests Codex sends, wherever the hub sees the call before it runs;
|
|
61
|
+
`ahub status` names each peer where it cannot. Budgets pause an
|
|
62
|
+
agent before it runs out of quota, and recovery never replays an
|
|
51
63
|
effect whose outcome is unknown.
|
|
52
64
|
- **Operations.** A terminal console and a local dashboard, one hub per repository or
|
|
53
|
-
worktree with a machine-wide view, live settings reload,
|
|
65
|
+
worktree with a machine-wide view, live settings reload, structural config
|
|
66
|
+
(instances, review, lifecycle) edited with a validated diff and a backup from the
|
|
67
|
+
console or a dashboard settings session, token usage and quota
|
|
54
68
|
history (`ahub usage` and the dashboard's Usage view), and `ahub upgrade`, which
|
|
55
69
|
carries running sessions across versions.
|
|
56
70
|
|
|
@@ -80,7 +94,7 @@ uses the home must run a version that understands the new layout.
|
|
|
80
94
|
[#258](https://github.com/STAIxBWLB/agent-hub/issues/258)).
|
|
81
95
|
|
|
82
96
|
Live checks that still need a real account or a person are listed in the
|
|
83
|
-
[release verification report](docs/verification/2026-10-
|
|
97
|
+
[release verification report](docs/verification/2026-10-25-0.16.0.md) and the
|
|
84
98
|
[smoke checklist](docs/smoke.md).
|
|
85
99
|
|
|
86
100
|
## Documentation
|
|
@@ -35,3 +35,11 @@ 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.
|
|
42
|
+
|
|
43
|
+
Background moved out of the area notes for 0.16.0:
|
|
44
|
+
|
|
45
|
+
- Closing-refs negation list (#760, tests.md): agent-mem PR #36 said "Does not close #15", GitHub read `close #15` as a closing keyword, and the design issue closed with T3 open. The list errs toward flagging: a false positive costs a rewrite of the PR text, a false negative closes a design issue, and a body that really means a qualified close is the author's to reword. A keyword typed into a comment or the merge dialog is out of the check's reach. At 0.16.0 `closing-refs` is not a required status check (branch rules require `check (ubuntu-latest)`, `check (macos-latest)` and `seeded guards`).
|
|
@@ -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
|
|
|
@@ -52,7 +52,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
52
52
|
|
|
53
53
|
- Every agent the hub spawns (the Codex app-server, ACP agents such as Kimi, Pi) runs in its own process group and is stopped as one (`stopOwnedProcess(proc, { group: true })`, `trackGroup` after spawn): `codex` is a node launcher, and mid-turn its native app-server does not exit on SIGTERM within the grace period, so a SIGKILL to the launcher alone left the app-server running under init and holding the daemon alive (#113, #115). A new builtin adapter that spawns an agent does the same. A configured ACP kind stops only through `AcpOwnedGroup` (`src/adapters/acp-owned-group.ts`): capture the direct child and its original group after spawn, verify before admission and again before stop, SIGSTOP, re-verify, then SIGKILL only that group; never discover or signal descendants outside it, and with unproven ownership send no signal, close stdin and keep the owner until it is confirmed gone.
|
|
54
54
|
- To stop a process tree, freeze it (SIGSTOP) before reading what it contains, then SIGKILL, and call it done only when the table shows none of it (with no table at all, the stop can only ask the group itself, which cannot see groups below it). A process a read after the freeze shows for the first time is frozen and read again before anything is killed.
|
|
55
|
-
- Work that must survive `ahub kill` (
|
|
55
|
+
- Work that must survive `ahub kill` (amem capture session end) is awaited in `stop()`; fire-and-forget dies with the process.
|
|
56
56
|
- Native owner teardown must handle a lost shutdown acknowledgement using verified process identity. Pending launches must be revocable, and active tools/streaming output must count as watchdog activity. A reloaded `watchdog_ms` reaches a peer only through `configureWatchdog` (`src/hub/peers.ts`, #439) and applies from its next busy turn, never to the running one.
|
|
57
57
|
- `stop(reason?)` (`src/hub/peers.ts`): a reason marks a person's requested stop (`peer_stop`, rule: `daemon.md`). Fence before the first await (the adapter's stopping flag, or `offline`) so `deliver()` and buffered native callbacks do nothing more, hold the in-flight delivery as `needs_review` with that reason, and end `offline`. For ACP without a journal, return ordinary in-flight envelopes once through `onFailed` before a requested stop invalidates the turn; never return the watchdog-cancelled original prompt. A requested stop never completes, escalates or reports the turn it cut (`AcpPeer` bumps `turn`; `PiPeer.fail` skips the failure message and `onTurnFailure` while `requestedStopReason` is set). `PiPeer.stop()` waits for aborted tools only up to `stopGraceMs`: a tool past it never blocks the teardown, and the stop throws `PiToolSettlementFailure` once the owner is down.
|
|
58
58
|
- A terminal the hub opens for a person (`src/hub/terminal-open.ts`, #269) gets `personEnv`, which strips every agent marker; never `peerChildEnv`, which sets one.
|
|
@@ -95,6 +95,23 @@ 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
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
|
+
- Class calls in `src/hub/action-enforce.ts`: the Claude PreToolUse hook returns `actionClasses` (`ask` beats `allow`), `AcpPeer.answerPermission` consults `actionGate` before `autoApprove`, and the daemon's `classedToolPermission` runs before `never-ask`, `ask-when-needed` and `piAlways` in local `permit` and `piPermit`. Keep Pi's gate inside its persisted tool receipt so a replay never spends twice. Classify a local/Pi call with `cwd` and `workdir` set to the host execution root, never the model's fields. Unknown, unparsable or absent payload follows the native mode; a classed request is never answered by `--unattended`. A gate that throws asks deny-only (`command withheld`, `denyOnly`), never allows.
|
|
113
|
+
- Local/Pi approvals supply the host's complete tool title only through the optional `onPremToolTitle` port; match it against the exact bash/git input before showing it with the class line. The person's on-prem console gets that full title even on a PII turn. Keep it out of logs, public status, other peers and dashboard projections. Public/native cut or withheld views set `denyOnly`: drop every grant and budget choice, and reject forged allow/budget answers. Never infer completeness from the presence of a marker string.
|
|
114
|
+
- Codex (`interceptActionApproval` in `codex-appserver.ts`) intercepts only `item/commandExecution/requestApproval` and legacy `execCommandApproval` with a well-formed command; any other request, or no `actionGate` verdict, keeps native handling. Classed command approvals need the current link, thread and observed active turn; without that binding deny, including legacy exec approvals. A pending classed wait is aborted, and answered deny, on stop, watchdog, detach, thread adoption, its turn's completion or interrupt. Keep `approvalIds` tombstones for the socket lifetime and suppress late/duplicate TUI replies. At 1024 ids close both sides rather than evict a fence; reattachment is the upgrade path. Native calls that emit no request remain not enforceable.
|
|
115
|
+
- Action allowances use their own host `actionSession` port; do not widen `contextSession` for Local and thereby change checkpoint, telemetry or supervision behavior. Managed headless Claude enforceability needs a current child generation in busy state, never a truthy method reference.
|
|
116
|
+
- 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 owned adapter, so a resumed session id never revives an allowance.
|
|
117
|
+
- 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/window-kind.ts`, `src/hub/budget-notes.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,13 +19,15 @@ 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
|
+
- Window kinds (#285 AC6, AC8): classify a window only through `windowKind` (`src/hub/window-kind.ts`: `native`, `count`, `manual`); a reporter that knows its kind passes it (`ahub budget set` is `manual`, the Kimi count `count`). Readings are kept per window id and kind, never overwriting each other. The gate and `headroom` act only on `gating()`: a fresh `manual` reading replaces the others for its window, and the hub `count` never gates while a fresh `native` or `manual` reading exists. A hard refusal drops the `manual` reading of each window it reports. `status()` lists the gating reading first and marks the information-only fresh ones `superseded`; never gate or route from `status()` windows.
|
|
26
|
+
- No-quota notes (#285 AC6, AC7): a peer whose definition sets `budget.noQuotaReason` (Pi, local) gets a hub 5 h token count (`hubTokenWindows`, replayed at start through `TokenWindow.seed`) as information only, never a window, a meter or a gate. Build the notes in `ahub budget`, the console and the dashboard only through `budgetNotes` (`src/hub/budget-notes.ts`) from the definition and the counters, and Kimi's source state only from `KimiUsage.status()`: a rule and a count, never a path, a session id or log text.
|
|
25
27
|
- Keep `readClaudeTranscriptUsage` at its 32 MiB accounting bound. For native turn completion, follow the admission rule in `daemon.md`.
|
|
26
28
|
|
|
27
29
|
- 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
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; 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.
|
|
31
|
+
- 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
32
|
- 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
|
-
|
|
33
|
+
- 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(peer, "tokens", "count")`, so only the count's own reading goes; 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,10 +8,10 @@
|
|
|
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`, `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.
|
|
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`, the memory code modules (`src/hub/memory-symbols.ts`, `src/hub/memory-code-overlay.ts`, `src/hub/memory-code-head.ts`, `src/hub/memory-stale-anchors.ts`, `src/cli/memory.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`).
|
|
@@ -22,9 +22,11 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
22
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
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.
|
|
24
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.
|
|
25
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).
|
|
27
|
+
- Planner setting (#755): `planner` is one id or a list of one to eight distinct eligible ids, validated only by the `planner` row of `projectSettings` (eligible declared instances join its `values`) and `checkSettingValue`, in `loadConductorPolicy` and `validateRawConfig` alike. Every `resolveSettledRoles` call (`settlePlanner`, `roleHolders`) passes the same `plannerEligible` predicate built from that row, so reload guards compare holders the way the running map settles them.
|
|
26
28
|
- 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).
|
|
27
|
-
- `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`);
|
|
29
|
+
- `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, hub-owned ACP peers in `ask`, the local worker and Pi (`classedToolPermission`, #750) enforce it, and a headless Claude implementer only while a current child generation is busy. Codex's approval RPCs are gated but Codex stays not enforceable, because its native policy can run a command without a request. 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 every refusal names its reason (rules: `adapters.md`, Always-ask).
|
|
28
30
|
- `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`).
|
|
29
31
|
- 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.
|
|
30
32
|
- 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.
|
|
@@ -40,7 +42,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
40
42
|
- 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.
|
|
41
43
|
- 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.
|
|
42
44
|
- 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).
|
|
43
|
-
- 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.
|
|
45
|
+
- A recovery target-feature floor (`task_cancel`, `peer_instances`, `role_stages` while a declared Claude-kind instance holds `implementer`, `task_planners` while a list/instance planner is configured or `Board.hasPlannerState()` finds a task/release binding or plan-review authorship marker, `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
46
|
- 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).
|
|
45
47
|
- 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.
|
|
46
48
|
- 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.
|
|
@@ -59,14 +61,18 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
59
61
|
- `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.
|
|
60
62
|
- 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).
|
|
61
63
|
- `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).
|
|
62
|
-
- The managed Claude hook (`src/cli/facts-hook.ts`) may
|
|
64
|
+
- 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`.
|
|
63
65
|
- `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.
|
|
66
|
+
- 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.
|
|
67
|
+
- 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.
|
|
68
|
+
- 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.
|
|
64
69
|
|
|
65
70
|
- 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.
|
|
66
71
|
- 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.
|
|
67
72
|
- 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).
|
|
68
73
|
- 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).
|
|
69
74
|
- `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).
|
|
75
|
+
- 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.
|
|
70
76
|
|
|
71
77
|
- 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.
|
|
72
78
|
|
|
@@ -82,16 +88,20 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
82
88
|
|
|
83
89
|
- Keep full memory home, generation ID, selector revision and malformed-row totals in authenticated person status/CLI receipts, never disk status.json or agent CLI output; low-privilege receipts use a generation prefix and omit home, selector revision and malformed-row totals. Emit typed unbound disabled/unavailable receipts with no profile when no memory handle opened; profile-check may treat an authenticated same-home generation-capable unbound daemon as memory-quiesced, never a stale bound handle. Scope profile-check readiness to its named registry inventory; old, unknown or unreadable receipts refuse readiness, and other custom registries need separate checks.
|
|
84
90
|
|
|
91
|
+
- The memory client (#752) is the in-process amem `MemoryClient` built once in `startDaemon` from the alias-resolved `memoryProject`, the bound store and candidates, `screenMemory` and the capture binding; its `check` rechecks that binding and the project before each call. `DaemonOptions.memoryClient` is a host-only seam, never read from configuration or a tool; `memory.worker_url` is ignored legacy configuration, never a destination. Local capture takes `memoryProject`, never the project chain. Shutdown aborts `syncStop`, awaits `syncRun`, then closes `memory`, the candidates and the store, in that order.
|
|
92
|
+
- Symbol anchors (#769, #770): resolve `memorySymbols` only in the `prepareReviewBrief` pass that fills `reviewSymbols`, under the same key and eviction, through `resolveMemorySymbolNodes` (a positive, unambiguous `exists` at the brief's head, at most `MEMORY_SYMBOL_LIMIT` candidates within `MEMORY_SYMBOL_START_BUDGET_MS`); a filled key is reused by the completion pass, a PII task is never resolved, and one that turns PII or changes owner meanwhile resolves unknown. `memorySymbolNodes` answers only for a non-PII task whose `codeSymbols.head` is its `refs.commit`. Raw and normalized symbol forms are query compatibility only (`taskMemoryQueryNodes`, `planGuardNodes`): never rewrite a stored note or id. `ahub doctor --stale-anchors` (`memoryStaleAnchorsCommand`, `src/cli/memory.ts`) is read-only: it never creates a missing profile, machine or shard, never edits, deletes or promotes a note, and reports unavailable, private or ambiguous evidence as unknown, never stale.
|
|
93
|
+
- Read-time code neighbours (#771): derive them only through `memoryCode` and publish only through `currentMemoryCode` (`src/hub/daemon.ts`), which rechecks the memory binding, the overlay head against its `readMemoryCodeHead` proof and every node's canonical id, and screens each attached note again; never store derived `calls` edges or neighbours in amem. Neighbour queries run through `codeFacts.neighbours` with `priority: "optional"`, so review and completion queries win. Only `symbol:` starts query: the dashboard only through `uiMemorySnapshot` for the `memory` view with `codeNeighbours: true` and a node of at most 512 bytes (`ui_snapshot` answers a failure with `memory view unavailable`), and `hub_memory_graph` only after `memory_read`, rechecking the caller's connection generation, private turn and PII context after the await. Console task detail reads `taskMemoryResult(task, true)`, which never starts a query; its `relatedMemory` is a count or `unknown`. A PII task or turn never queries. Keep the cache ceilings (32 entries, 30 s known, 5 s unknown, 5 s between derive starts), and keep direct path/title/symbol notes in `taskMemoryResult` independent of code availability: an optional code failure never drops them.
|
|
94
|
+
|
|
85
95
|
- 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.
|
|
86
96
|
- 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.
|
|
87
97
|
- `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`.
|
|
88
|
-
- 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 a reviewer with roles exactly `["reviewer"]
|
|
89
|
-
- 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. 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.
|
|
98
|
+
- 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`).
|
|
99
|
+
- 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.
|
|
90
100
|
- 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.
|
|
91
101
|
|
|
92
102
|
- 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).
|
|
93
|
-
- 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.
|
|
94
|
-
- A declared instance that resolves to its kind's `reviewerVariant` 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, Codex #720). It never opens a TUI, writes a launch or terminal-recovery record, or execs; its preview prints `start: "hub"`.
|
|
103
|
+
- 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.
|
|
104
|
+
- 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"`.
|
|
95
105
|
- 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).
|
|
96
106
|
|
|
97
107
|
- 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).
|
|
@@ -99,3 +109,4 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
99
109
|
- Share one monotonic 250 ms snapshot Git budget until the next event-loop I/O turn. Repository/HEAD probes, capture and end diffs consume the same remaining deadline; exhaustion or unreadable diff evidence records unknown trees and no attribution/undo, with one diagnostic per burst. Never substitute a stale baseline or apply a late result (#576). `DaemonOptions.snapshotClock` is host-only (tests hold it, rule: `tests.md`): never read it from configuration or a tool, production leaves it unset (`performance.now`), and the share stays 250 ms with `snapshotBurstMs` still clamped to it. Every deadline comparison goes through `budgetNow(budget)`, never `performance.now()` against `budget.deadline`.
|
|
100
110
|
|
|
101
111
|
- Channel tool-result hints (#493, #221): `withPendingHint` (`src/adapters/claude-channel.ts`) appends fresh `delivery_holds` counts, and drops the "call hub_inbox" advice whenever the same reply carries a held-delivery hint, because `bus.pull` refuses under that hold and the advice would send the session into a refused read. A `hub_inbox` result that already says how many messages remain unread adds only the important count, never a second queued count. Do not add a Claude client-version gate on steer without a probe of the older client and an optional hello field that carries the version (background: README).
|
|
112
|
+
- Memory sync (#438): stop an in-flight `syncMemory` only through `syncStop` (`src/hub/daemon.ts`), and await `syncRun` before the memory store closes, as `stopOnce` does. agent-mem runs the sync's git in its own process group, so a process that ends without `daemon.stop()` leaves that git and its transport child holding the memory repository. A signal the hub exits on goes through lifecycle stop (`STOP_SIGNALS` in `src/hub/lifecycle.ts`: SIGINT, SIGTERM, SIGHUP, SIGQUIT), never the default action; SIGQUIT in particular must not reach its default core dump (Bun honours it: agent-mem #435 measured a 5.9 GB core file and a delayed exit). The forced exit after the shutdown deadline stays `process.exit`.
|
|
@@ -15,8 +15,8 @@ Scope: the hub-native local worker, its tools, path guard, denylist and seatbelt
|
|
|
15
15
|
- After a tool with side effects ran, a failed turn is reported, never redelivered.
|
|
16
16
|
- `ctx.permit` answers a `ToolApproval` (`true`, `false`, `"expired"`, `"aborted"`, #253): only `true` permits an effect, so never test it for truthiness. A tool returns `approvalError`'s text for the rest: a denial, the expiry (which tells the model not to retry) or the withdrawal. A turn that two unanswered approvals ended (`ApprovalWaitStop`; counting rule: `adapters.md`) completes its missing tool results, settles its delivery `completed` with the reason and is never handed back for replay, also when no tool had a side effect.
|
|
17
17
|
- `Facts` (`src/hub/facts.ts`, #428) names a large file whose ctime is under `RACY_MS` by its git blob id: in `load()` and `observe()` only through `racyBlob()` (per-file `RACY_HASH_MAX_BYTES`, per-store `RACY_BURST_BYTES` per event-loop turn, memo cleared at `boundary()`), past which it keeps the stat-only stamp and makes no claim; `atHead()` keeps its own `HASHED_BYTES` bound, `tree()` reads large files by stat only, and `largeBlob()` caches only settled stamps.
|
|
18
|
-
- Hub tool calls (`hub_send`, `TASK_TOOL_NAMES`, `CONDUCTOR_TOOL_NAMES`) are never passed to `capture.observe`; `Tasks` alone decides what hub tool text reaches memory (#230).
|
|
19
|
-
- A tool that asks for approval passes its name to `ctx.permit`, and `write`/`edit` also the `guardPath` result, never the model's path string: also pass the original path separately through every permit wrapper; `ask-when-needed` grants only when both the original project-relative spelling and the real path avoid protected names through `grantablePath` (`src/hub/permission-mode.ts`). `bash` and mutating `git` pass no path. After the awaited approval `write`/`edit` run `guardPath` again and refuse a target that changed (#242).
|
|
18
|
+
- Hub tool calls (`hub_send`, `TASK_TOOL_NAMES`, `CONDUCTOR_TOOL_NAMES`) are never passed to `capture.observe`; `Tasks` alone decides what hub tool text reaches memory (#230). Every live `capture.observe` passes a host `sourceId` (`<session>.<turn>.<step>.<n>`, #752); a new host caller supplies one too. Capture stores `paths` only for `read`/`write`/`edit` and only project-relative ones: command argv is never path metadata.
|
|
19
|
+
- A tool that asks for approval passes its name to `ctx.permit`, and `write`/`edit` also the `guardPath` result, never the model's path string: also pass the original path separately through every permit wrapper, and every tool its parsed arguments as `input` (the always-ask classifier reads it, #750; rule: `adapters.md`); `ask-when-needed` grants only when both the original project-relative spelling and the real path avoid protected names through `grantablePath` (`src/hub/permission-mode.ts`). `bash` and mutating `git` pass no path. After the awaited approval `write`/`edit` run `guardPath` again and refuse a target that changed (#242).
|
|
20
20
|
|
|
21
21
|
- Honor the live `protect_instruction_files` policy in scoped grants for the three instruction names; keep reads, explicit approval and never-ask unchanged. Only the settings registry writes this machine-local policy (#323).
|
|
22
22
|
|