@staix/agent-hub 0.27.0 → 0.28.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 +23 -0
- package/README.md +11 -3
- package/docs/agent-notes/adapters.md +4 -2
- package/docs/agent-notes/bus.md +4 -4
- package/docs/agent-notes/daemon.md +7 -3
- package/docs/agent-notes/tasks.md +5 -0
- package/docs/agent-notes/tests.md +6 -5
- package/docs/design/adapters.md +76 -0
- package/docs/design/agent-hub-architecture.md +67 -0
- package/docs/design/agent-hub.md +52 -0
- package/docs/design/bus.md +48 -0
- package/docs/design/task-board.md +86 -0
- package/docs/dogfood.md +16 -1
- package/docs/operations.md +308 -12
- package/docs/profiles.md +1 -0
- package/docs/quickstart.md +11 -3
- package/docs/sdlc.md +1 -1
- package/docs/security.md +8 -3
- package/docs/smoke.md +2 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +2 -0
- package/docs/verification/2026-10-31-codex-0166-reviewer-isolation.md +2 -0
- package/docs/verification/2026-11-02-0.28.0.md +54 -0
- package/docs/verification/2026-11-02-autonomy-t4.md +39 -0
- package/docs/verification/2026-11-02-codex-0166-reviewer-tool-call.md +53 -0
- package/docs/verification/2026-11-02-control-write-after.json +139 -0
- package/docs/verification/2026-11-02-control-write-baseline.json +127 -0
- package/docs/verification/2026-11-02-control-write-guarded.json +127 -0
- package/docs/verification/2026-11-02-control-write-outcomes.md +65 -0
- package/docs/verification/2026-11-02-orca-1.4.245-cli-contract.md +51 -0
- package/docs/verified.json +13 -13
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +338 -112
- package/src/adapters/claude-channel.ts +12 -2
- package/src/adapters/compat.ts +1 -1
- package/src/adapters/definitions/native-cli-claude-setup.ts +31 -9
- package/src/adapters/definitions/native-cli-entry-jcode.ts +33 -0
- package/src/adapters/definitions/native-cli-entry-qwen.ts +2 -2
- package/src/adapters/jcode.ts +37 -4
- package/src/adapters/qwen-dual.ts +3 -2
- package/src/cli/agents.ts +4 -2
- package/src/cli/bench.ts +4 -2
- package/src/cli/board-read.ts +25 -0
- package/src/cli/claude-homes.ts +2 -2
- package/src/cli/console-state.ts +8 -3
- package/src/cli/help.ts +6 -5
- package/src/cli/identity.ts +9 -1
- package/src/cli/init-prompt.ts +179 -0
- package/src/cli/init-settings.ts +229 -0
- package/src/cli/init.ts +102 -9
- package/src/cli/launch.ts +3 -1
- package/src/cli/main.ts +106 -35
- package/src/cli/output.ts +14 -0
- package/src/cli/setup-homes.ts +18 -3
- package/src/cli/specs.ts +150 -0
- package/src/cli/terminal-recovery.ts +35 -11
- package/src/hub/board.ts +56 -3
- package/src/hub/bus.ts +54 -7
- package/src/hub/child-process.ts +6 -4
- package/src/hub/config-apply.ts +4 -1
- package/src/hub/config-edit.ts +1 -1
- package/src/hub/config-tier.ts +1 -0
- package/src/hub/control-client.ts +28 -4
- package/src/hub/cost-governance.ts +22 -6
- package/src/hub/crash.ts +20 -0
- package/src/hub/daemon.ts +304 -56
- package/src/hub/delivery-hints.ts +6 -3
- package/src/hub/delivery-journal.ts +14 -3
- package/src/hub/hub-tools.ts +10 -6
- package/src/hub/peers.ts +1 -1
- package/src/hub/review-brief.ts +18 -0
- package/src/hub/routing.ts +7 -3
- package/src/hub/settings.ts +2 -0
- package/src/hub/specs-config.ts +43 -0
- package/src/hub/specs-task.ts +108 -0
- package/src/hub/specs.ts +1008 -0
- package/src/hub/task-list-page.ts +75 -0
- package/src/hub/task-sweep.ts +10 -1
- package/src/hub/tasks.ts +448 -40
- package/src/hub/terminal-open.ts +10 -5
- package/src/hub/tool-request.ts +131 -0
- package/src/ui/office-model.ts +17 -1
- package/src/ui/office-space.ts +1 -1
- package/templates/config.json +1 -0
- package/templates/profiles/dev.json +1 -0
- package/templates/profiles/general.json +1 -0
- package/templates/profiles/research.json +1 -0
- package/templates/profiles/writing.json +1 -0
- package/templates/specs.block.md +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ 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.28.0
|
|
8
|
+
|
|
9
|
+
- `ahub init` sets the main project settings by flag (#1100, #1065 T1): `--agents`, `--sdlc`, `--coordination`, `--memory`/`--no-memory`, `--snapshots`/`--no-snapshots`, `--pi`/`--no-pi` and, for a new project, `--autonomy inherit|off`. On an existing project only a named flag changes anything, `--dry-run` shows each change as old -> new, and an invalid value is refused with the allowed values.
|
|
10
|
+
- `ahub init` asks each setting in turn when a person runs it at a terminal on a new project with no setting flag and no `--yes` (#1110, #1065 T2). Enter keeps the default, the plan is shown before anything is written, and N, Ctrl-C or Ctrl-D write nothing. A peer, a live test, a pipe and `--dry-run` are never asked.
|
|
11
|
+
- Design specs (#1064 T1-T3: #1097, #1102, #1107). `ahub specs list|check|covers` and the read-only `hub_specs` tool inspect Markdown specs with `id`/`type` frontmatter, their links and their freshness. `hub_task_done` reports the specs covering a task and takes head-bound `spec_ack` acknowledgements. `ahub init --specs|--no-specs|--spec-review` manages the `AGENT_HUB_SPECS` block, and `ahub specs stamp` is a person-only record that a spec was reviewed against HEAD. Specs are on by default, so the next `ahub init` in an existing project adds the block unless `specs.enabled` is false.
|
|
12
|
+
- `hub_task_list` pages (#1083): an agent gets 50 compact rows by default (at most 200), newest updated first, with a `next` cursor; `ids` returns full rows for named tasks and `detail: true` full rows for a page. The person's console and `ahub board` from a terminal keep the full array; `ahub board` from an agent shell follows the pages.
|
|
13
|
+
- A task can declare `needs_network` or `needs_worktree` (#1081). Routing, assignment and `hub_task_accept` keep such work from a peer that cannot meet the need (a sandboxed Pi or local worker), `ahub route explain` names the missing capability, and a refused accept leaves no branch ref behind. A hub-made writable worktree for sandboxed peers is #1115.
|
|
14
|
+
- An urgent task can be accepted at the owner's WIP limit when the limit is reached only by offers it never accepted (#1080): the newest such offers go back to To Do, ownerless, and the planners are told. Accepted, pinned and urgent work is never taken back, and the WIP refusal now lists accepted, offered and reserved tasks separately.
|
|
15
|
+
- Hub tool writes report an unknown outcome instead of a failure when the reply is lost (#1078): `hub_task_propose` and `hub_review` take a reusable `request_id`, a retry with the same key applies once and returns the first result, and the reply names the read that settles it. The replay window is per actor; `hub_delivery_done` keeps the delivery journal's own idempotence.
|
|
16
|
+
- A busy agent can settle its own `needs_review` delivery with `hub_delivery_resolve` (completed or discard, with the revision `hub_inbox` shows and a public reason), and `hub_inbox` reads new messages queued behind a held one without releasing the hold (#1079). A row carrying an invocation stays person-only, and each refusal names its cause and next step.
|
|
17
|
+
- Failed deliveries leave the queue (#1096: #1098, #1103): `ahub queue discard --peer` also clears unsettled failed rows (`--state failed` for those alone), `queue list` hides settled rows unless `--all`, and resolving a settled row says `already resolved by <action>` instead of `stale revision`.
|
|
18
|
+
- Crash-recovery notices in `ahub status` clear once their condition is gone (#1082): a line about a peer goes when that peer attaches again, and the in-flight count follows the crash's own deliveries still in needs_review.
|
|
19
|
+
- A start request during a recovery hold names the hold, whether the peer already runs, and the next step (#1088, #1104).
|
|
20
|
+
- Peer terminals open in the Orca worktree whose root is this project's root, not the worktree of the terminal that started the hub (#1089, #1105), and hub-spawned children no longer inherit Orca routing variables (#1094). The Orca 1.4.245 CLI contract was rechecked read-only (#1090, #1108).
|
|
21
|
+
- A Jcode TUI launch refusal names its cause and the next step, the daemon logs the cause code, and `ahub status` says how to get the TUI back after recovery (#1085, #1093).
|
|
22
|
+
- A backlog card with a future planned start is not offered, pulled or reported stalled before that time (#1076, #1092).
|
|
23
|
+
- `ahub setup` keeps project- and local-scope plugin rows it did not install and applies the plugin semver check only to agent-hub rows (#1068, #1069, #1073, #1074).
|
|
24
|
+
- Refusals and audit lines name autonomy, usage, deliverables and cost-scope causes with their allowed values (#1084, #1091, #1101).
|
|
25
|
+
- Reviewers check that every new or changed error message states its cause and its next step: REVIEW.md and the shipped profiles' review checklists (#1095, #1109).
|
|
26
|
+
- acon Space keeps an attributed task's card past the office rendering cut (#1038, #1075).
|
|
27
|
+
- Design specs dogfood in this repository (#1064 T4, T5: #1117, #1119): five draft specs in `docs/design/` (goal, architecture, bus, task board, adapters), a successor line on the historical design ledger, and the `AGENT_HUB_SPECS` block in AGENTS.md. All stay draft until the person reviews them; enabling `specs.stamps_file` is a local step for the person.
|
|
28
|
+
- Codex 0.166.0 is a verified reviewer version (#1071). Pin `@staix/agent-mem` 0.19.0 (was 0.18.0, #1072); it shares its index and hook records with 0.18.0.
|
|
29
|
+
|
|
7
30
|
## 0.27.0
|
|
8
31
|
|
|
9
32
|
- Let the configured conductor record release consent on the person's explicit version-naming instruction (#1063). Receipt and audit retain the real asker; exact candidate/docs/origin bindings, restart revocation and live-test refusal remain.
|
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.28.0, control protocol 17 (accepts 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
|
|
@@ -37,7 +37,10 @@ The plugin also feeds the console's Space view (Flat, Room and 3D): agents becom
|
|
|
37
37
|
at their desks, the task board's columns become zones, and task links and recent messages
|
|
38
38
|
become links. The scene carries labels and states only, never message bodies or details.
|
|
39
39
|
|
|
40
|
-
`ahub init --dry-run --json` shows what init would write.
|
|
40
|
+
`ahub init --dry-run --json` shows what init would write. On a new project at a
|
|
41
|
+
terminal, `ahub init` asks each main setting with its default before writing; flags
|
|
42
|
+
such as `--agents`, `--sdlc` or `--no-specs` set them directly, and `--yes` takes the
|
|
43
|
+
defaults. The
|
|
41
44
|
[Quickstart](docs/quickstart.md) walks through a first session and a working day.
|
|
42
45
|
|
|
43
46
|
## What you get
|
|
@@ -66,6 +69,11 @@ become links. The scene carries labels and states only, never message bodies or
|
|
|
66
69
|
source. A unit is covered by its owner's `hub_study_handoff` with evidence, not by
|
|
67
70
|
a done task. Experiment attempts are recorded with `hub_experiment_record` and read
|
|
68
71
|
with `ahub study experiments`, each value naming where it came from.
|
|
72
|
+
- **Design specs.** Markdown specs with `id` and `type` frontmatter name the paths
|
|
73
|
+
they govern. `ahub specs check` validates their links and reports specs that are
|
|
74
|
+
stale against their covered sources, the hub names the specs covering a task, and
|
|
75
|
+
agents read them with `hub_specs`. Only a person stamps a spec as reviewed
|
|
76
|
+
(`ahub specs stamp`).
|
|
69
77
|
- **Shared memory.** Decisions, contracts, fails and findings are kept through the
|
|
70
78
|
first-party [`@staix/agent-mem`](https://github.com/STAIxBWLB/agent-mem) library,
|
|
71
79
|
in process with no claude-mem worker (short- and long-term tiers, a typed graph,
|
|
@@ -127,7 +135,7 @@ uses the home must run a version that understands the new layout.
|
|
|
127
135
|
[#258](https://github.com/STAIxBWLB/agent-hub/issues/258)).
|
|
128
136
|
|
|
129
137
|
Live checks that still need a real account or a person are listed in the
|
|
130
|
-
[release verification report](docs/verification/2026-11-
|
|
138
|
+
[release verification report](docs/verification/2026-11-02-0.28.0.md) and the
|
|
131
139
|
[smoke checklist](docs/smoke.md).
|
|
132
140
|
|
|
133
141
|
## Documentation
|
|
@@ -52,7 +52,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
52
52
|
- Codex reports each thread it adopts, started or resumed (`onThread`, the `native_thread` event), before any turn on it starts: the upgrade planner (Codex's `session.zeroTurn` in `src/adapters/definitions/native-recovery.ts`, read through `nativeZeroTurn` in `src/cli/upgrade-runtime.ts`, #215) gives each codex `turn_start` to the thread adopted last before it and plans a thread it saw start with no turn (and no rollout) as a fresh start.
|
|
53
53
|
- Peer DOING (#878): an adapter reports a native session title or its plan's in-progress item only through `onDoing` (set by `Bus.add`), with `reset` when it binds a new session or thread; never put that text in a log line, event, message, status notice or memory capture. Codex reads only `thread/name/updated` and `turn/plan/updated` of the bound thread, ACP only a `plan` update inside the current turn; never guess a value for a peer without such a source.
|
|
54
54
|
- Codex 0.154.0 `agentMessage` items carry `text` and `phase` (not `content[]`); only the last non-`commentary` message of a turn is shared.
|
|
55
|
-
- Hide `hub_delivery_retry` from delivery-only ACP/local tool lists and omit its retry hint. Treat `AGENTHUB_DELIVERY_ONLY` only as an exposure hint; the daemon must refuse by the actual attached adapter, and native children must not inherit the marker. A Claude push that cannot reach its model reports `failed_safe`; if the daemon refuses that optional receipt state, try `needs_review` and retain uncertainty when neither receipt is confirmed.
|
|
55
|
+
- Hide `hub_delivery_retry` from delivery-only ACP/local tool lists and omit its retry hint. Treat `AGENTHUB_DELIVERY_ONLY` only as an exposure hint; the daemon must refuse by the actual attached adapter, and native children must not inherit the marker. A Claude push that cannot reach its model reports `failed_safe`; if the daemon refuses that optional receipt state, try `needs_review` and retain uncertainty when neither receipt is confirmed. Keep `hub_delivery_resolve` on every list, delivery-only included: settling one's own `needs_review` row is not a retry (#1112).
|
|
56
56
|
- ACP `onActivity` is a closed, transient person-only projection. Never publish it through the bus, log/event writers, UI, tools or memory. Emit only for an active turn; latch private envelopes and associated PII work, including unreadable privacy state, and discard pending chunks on turn teardown. Native terminal references do not authorize filesystem/terminal RPC reads.
|
|
57
57
|
- ACP quota refusals use the pinned agent contract in `src/adapters/acp-quota.ts`, fenced by native identity and current turn generation. Report hard quota and require the hub's explicit pause confirmation before settling a pre-acceptance refusal as `failed_safe`; retain `needs_review` when work already started, and keep unknown refusals on the ordinary failure path. Sanitize agent refusal text before hub-written output.
|
|
58
58
|
|
|
@@ -65,6 +65,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
65
65
|
- `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.
|
|
66
66
|
- 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.
|
|
67
67
|
- Spawn a native peer with `peerChildEnv(peer, source)` so its own `AGENTHUB_PEER_ID` replaces inherited agent markers. Sandbox tool environments carry their executing peer too. Preserve the native shell environment policy; Codex's `CODEX_THREAD_ID` supplies the fallback after filtering. Before changing marker detection or launcher environments, read `docs/verification/2026-10-09-agent-shell-t0.md` for pinned source and live-probe limits.
|
|
68
|
+
- `peerChildEnv` drops every `ORCA_*` variable by default (#1094). Only a native that runs in the person's own pane passes `{ preserveOrca: true }`: `src/cli/launch.ts` and the Qwen CLI entry. A version probe, the process table or another helper the hub spawns uses `withoutOrcaEnv(childEnv(...))`, never `{ ...process.env }`.
|
|
68
69
|
- Launch ids and peer ids (agent-hub#1036): `peerChildEnv` gives a spawned peer `AGENTHUB_PEER_ID`; a Claude session's launcher also puts `AGENTHUB_LAUNCH_ID` into the environment every shell and MCP helper it spawns inherits, and a tools hello that names a Claude channel peer is a distinct session `<peer>~<origin>` only when it PRESENTS a launch id that is not the current launch record's (`sessionHello`). A hello with NO id stays the peer: the native facts hook, a helper and a person's shell connect that way and must keep working (#636, #525-5), so `ControlClient` never injects an inherited id. Nothing is refused: a new adapter that gives its tools socket a peer id keeps its marker admission, and adds its own launch id and `sessionHello` branch only when it wants the same distinction. The daemon labels an envelope an adapter publishes itself with the launch read by shape from `managedLaunchId` (channel) or `ownedLaunchId` (Jcode) (rule: `daemon.md` "Sender provenance"): keep those names, and never admit or authorize anything by them. This makes sessions distinguishable, it does not authenticate them: a same-user process can always act as the user.
|
|
69
70
|
- Spawn with `peerChildEnv(<instance id>)`, not the kind name. An ACP `kind` comes only from the host declaration, never from the id suffix or agent text.
|
|
70
71
|
|
|
@@ -100,7 +101,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
100
101
|
- TUI launchers (Kimi, Jcode, agy) write the plugin bundle the hub reports in `status` into their MCP entry, and the CLI's own copy only when the hub reports none (#502). Only the Jcode start path checks the entry against the daemon's own `SERVER_JS` (`jcodeMcpReady`); Kimi TUI admission does not compare it.
|
|
101
102
|
- Native CLI versions (#691): every version gate asks `assessVersion` in `src/adapters/compat.ts`; no other file holds a version constant. Add a version to `verified` only with a receipt in `docs/verification/`, and point the entry's `receipt` at the receipt a later version should repeat (the daily `scripts/agent-compat-watch.ts` names it, #849); a `line` is for contract gates only (an isolation gate never assumes a newer version compatible); the person's `ahub agents trust` admits one exact version on this machine and never skips a runtime proof; `ahub agents` with anything but `--json` stays `console` in `classifyPeerCommand` and refused in `liveTestRefusal` (`src/cli/identity.ts`). A version refusal starts with `VERSION_REFUSAL` and names only versions and the trust command; keep it that way so the daemon may show it.
|
|
102
103
|
- Jcode (#529): accept the native version only through `jcodeVersionSupported` (compat `jcode` line, 0.98.0 up to 1.0.0), in the daemon preflight (`jcodeVersionAsync`: off the event loop, owning its process group, output capped at 64 KiB), the doctor probe (`jcodeVersion`) and ACP `expectedAgent`; never pin the minor, and lower `line.below` when a release breaks the contract. Every owned jcode process (probe, serve, ACP and TUI) gets `JCODE_OWNED_ENV`, and the private socket name ends in `.sock`.
|
|
103
|
-
- Jcode TUI (#464): use only the current owned launch's top-level `--socket`/`--resume` tuple after native saved-session proof. Prove the save only through `savedSession` (no-follow, single-link regular file, size-bounded, unchanged across the read, its `id` and `messages` matching) and refuse a requested model, provider profile or session that differs from the running launch with `JcodeOptionMismatch`, which names the flag, never its value (#559); both the launcher and `--print-command` report it through `refuseNamedOption` (#574). Hand the tuple only to a person's launcher through `native_tui_launch` (admission: `daemon.md`); previews (`--dry-run`, or `--print-command` from anyone but a person) start nothing and never resolve it. Never synthesize a save or prompt to induce one. Ctrl-C during the save wait and frontend close retain ACP; preserve session/load identity refusal and headless owned recovery metadata, and never record the TUI as a managed terminal (`recordTerminalLaunch`): Jcode has no terminal recovery adapter (#564).
|
|
104
|
+
- Jcode TUI (#464): use only the current owned launch's top-level `--socket`/`--resume` tuple after native saved-session proof. Prove the save only through `savedSession` (no-follow, single-link regular file, size-bounded, unchanged across the read, its `id` and `messages` matching) and refuse a requested model, provider profile or session that differs from the running launch with `JcodeOptionMismatch`, which names the flag, never its value (#559); both the launcher and `--print-command` report it through `refuseNamedOption` (#574). Hand the tuple only to a person's launcher through `native_tui_launch` (admission: `daemon.md`); a refused tuple throws `JcodeTuiRefusal` with one closed `JcodeTuiRefusalCode` per check, never a path, socket, session or launch id. The launcher turns a returned code into words only through `jcodeTuiCauseMessage` (#1093). The headless-after-recovery hint is `JcodePeer.statusNotice` (fixed `JCODE_HEADLESS_AFTER_RECOVERY`), shown only after a crash report or a resumed session and gone once a ready TUI launch was served (`tuiServed`); previews (`--dry-run`, or `--print-command` from anyone but a person) start nothing and never resolve it. Never synthesize a save or prompt to induce one. Ctrl-C during the save wait and frontend close retain ACP; preserve session/load identity refusal and headless owned recovery metadata, and never record the TUI as a managed terminal (`recordTerminalLaunch`): Jcode has no terminal recovery adapter (#564).
|
|
104
105
|
- Jcode steer (#572): reach a busy owned jcode only through `nativeSteer` on its private server socket, never by cancelling its turn: `ping` must list `soft_interrupt_ids`, `subscribe` uses `target_session_id` (the ACP session id) and `continue_on_disconnect`, `state` must report `is_processing` (a soft interrupt sent while idle starts a turn by itself), then `soft_interrupt {content, urgent: false, client_id}`. `canSend` rechecks the captured turn, busy state, owned server process, socket identity and session immediately before the write. Every refusal before the send is `failed_safe` with nothing sent; a sent message that is not acknowledged, or whose turn cannot be confirmed, is `needs_review`, never `failed_safe` or `completed`. Read `currentTurn()` before sending and adopt only through `AcpPeer.adoptIntoTurn(turn, ...)`, which refuses a different or ended turn; adopted senders join the turn answer's audience (`replyParent` and `replyAudience` over the turn envelopes, so the hop cap holds), and each steer receipt is `accepted` at once and settles with the turn (`completed` on a normal end, `needs_review` on a cancel, failure, stop or watchdog). Headless Kimi and Qwen have no steer: status reports their waiting important messages only through `importantWaitingStatus` (`src/hub/daemon.ts`) past `budget.stale_min`. A turn that ends and restarts between jcode's `state` reply and the write still receives the message, held `needs_review` and adopted into nobody: keep that ceiling until upstream offers an atomic per-turn steer, and never add a cancel-to-deliver path.
|
|
105
106
|
- Jcode MCP set (#572): `jcode acp` and `jcode serve` start every server named in `<JCODE_HOME>/mcp.json` and the project's `.jcode/mcp.json`, `.mcp.json` and `.claude/mcp.json`, and no flag or variable selects them (`--mcp-tools`, `JCODE_MCP_TOOLS`, `JCODE_DISABLED_TOOLS` and `JCODE_DISABLE_CLAUDE_MCP` do not). Do not paper over it: `ahub doctor` lists the extra names through `jcodeExtraMcpServers` (`src/adapters/definitions/native-cli-jcode-mcp.ts`), names only, those four paths only, no home search, no link followed. Below the project root or home, `.jcode` and `.claude` must be real directories and the file a regular file (per-component `lstat`, an `O_NOFOLLOW` open and an `fstat` identity recheck); a system alias at or above the configured root stays allowed. Revisit if upstream adds a server selector.
|
|
106
107
|
- Extra Jcode instances (#685, #368): one `JcodePeer` per declared id (at most 8), each with its own `jcode_home`, socket and state. The person authenticates inside Jcode; the hub passes `-p`, `--model` and `--provider-profile` exactly as declared, handles no model credential or URL, and only checks that the home is a directory the person owns (`jcodeHomeRefusal`: no read or write under it). Never put `AGENTHUB_PEER_ID` in the shared `.jcode/mcp.json` entry (`jcodeMcpEntry`): every owned launch supplies it through `peerChildEnv(<id>)` or the TUI launch env, together with its `AGENTHUB_LAUNCH_ID`. The MCP server with no `AGENTHUB_PEER_ID` in tools mode refuses every tool and never defaults to a peer id. The daemon admits a `tools` hello for a peer whose definition is `owned-acp` only when it carries the launch id of that peer's current owned launch (`JcodePeer.ownsLaunch`, from `jcodeLaunches`); never trust the env alone. `jcode --as <id>` never opens a TUI. Declare the semantic `instances.nativeHome` on a definition, never compare the kind name (`registry-guard`); it is a fixed-builtin capability that `assertNativeCapabilityScope` refuses elsewhere. `checkJcodeInstanceHomes` (`src/hub/instances.ts`, run once at daemon startup) refuses a home equal to or nested in the base Jcode's or another instance's; compare homes only through its `canonicalHome` (`realPath`, case-folded on a volume that folds case, inode), never `resolve()` text. The preview of `--as <id>` passes the instance to `previewJcode` so it shows the daemon's real flags.
|
|
@@ -141,3 +142,4 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
141
142
|
- 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`.
|
|
142
143
|
- The floor (#999 T3, `action_policy.floor`) is the only place the classifier is told a push or pull request is ordinary: `classifyAction(..., floor?: FloorFacts)` runs `floorPush` and `floorPullRequest` only for a bare command (an `unwrapped` wrapper, `-c`, `--git-dir`, `--work-tree`, an expansion or a `cd` it cannot place keeps the class), and without a `FloorFacts` argument it is byte-identical to before. The flag is read only through `ActionBudgets.floorOn()`; the daemon's `floorFacts(peer)` supplies the facts per execution root (the default branch from the same authoritative remote read as the task shell, cached and refreshed at every `action_policy` reload; `inOwnRepository` only when git proves the top level is that root and its origin is the project's repository). A fact that is missing or unproven is the strict case; never add an allowed form to `floorRefspec` or `floorPullRequest` without a test that pushes the matching unforced and forced, default-branch, tag and opaque variants. A new allowed class change belongs there and in `test/action-floor.test.ts`, never in the shared `classify` branches.
|
|
143
144
|
- Floor facts hold only until the first earlier segment on the line that could change them. In `floorInert` (`src/hub/action-class.ts`), `FLOOR_INERT_PROGRAMS` and `FLOOR_INERT_GIT` are allowlists: any other program or git subcommand, a `FIND_WRITERS` primary, `rg --pre` or a glob argument ends the facts for the rest of the line. Before adding a program or git subcommand to either allowlist, show that no form of it can write a file, git config or a ref, or run another command, and add a test in `test/action-floor.test.ts` where its writing form still asks. Adding to `FIND_WRITERS` only makes the floor stricter. Revisit if the facts are ever re-read per segment.
|
|
145
|
+
- 2026-11-02 (#1089): a terminal the hub opens for a start through Orca (`src/hub/terminal-open.ts`, #269) is opened in the worktree `resolveStartTerminalWorktree` finds by asking `orca worktree list` for the one whose path equals this project's root, never in `ORCA_WORKTREE_ID`: that variable names the terminal this process was started from, which is another project's when `ahub --project`, the dashboard manager or an upgrade brought the project up. `startTerminalCandidate` (the environment or a recorded launch) only answers the synchronous "could Orca be used" question in `available()`. The match is exact because `ahub <peer>` compares the terminal's worktree root with the project root by equality; a parent, a child and an archived entry are not matches, two matches and a cut-off list that lacks the root are refusals, and each refusal names the cause and the next step and carries no path. Verified against Orca 1.4.245: `worktree list --json` rows carry `id` and `path`, with `truncated` and `totalCount` beside them. A change to that shape is found by the fake-Orca test in `test/start-modes.test.ts` only if its fixture is updated with it, so recheck the real output when Orca moves (the read-only probe is `orca worktree list --json`; never create or write to a live pane).
|
package/docs/agent-notes/bus.md
CHANGED
|
@@ -19,7 +19,7 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
|
|
|
19
19
|
- Limits admit what is sent: the envelope `newEnvelope` builds (a reply inherits its parent's sender, `digest` resolves to the originals, `capPriority` applies), never the raw `to` and priority. Admit first and build later, and implicit replies all count as broadcasts.
|
|
20
20
|
- A reply is addressed, never broadcast. An adapter that answers a delivery passes `to: replyAudience(envs)`; anything else with an `inReplyTo` inherits that envelope's sender. Leaving `to` out or empty fans the message out to every peer. The control `send` takes a nonempty string `to` as that one peer and refuses any other non-array shape or bad item before publishing (#963); never turn a malformed `to` into a broadcast.
|
|
21
21
|
- `digest` is not a peer: addressing a reply at the envelopes the peer was handed sends it nowhere once a delivery was condensed. The bus resolves `digest` back through `lastDelivery.originals`; anything else that reads a reply's `to` has to do the same.
|
|
22
|
-
- A pull-only peer (a Claude session without channel pushes, Kimi in its own TUI, or agy) is never drained by a push; `Bus.pull` hands it its queue oldest first up to `bus.pull_budget_bytes` (#489 H3; unset is `PULL_BUDGET_BYTES`, 64 KiB of `frame` text, #713; `takeBudget`), the first item always going so one huge envelope cannot stall it, and a poison one still alone. A push peer, and an attached channel session that is not pull-only, keep `take`'s `bus.digest_max` (at most `DIGEST_MAX`): never raise that for a pull. The daemon's `inbox` reply carries `waiting` and the plugin says `N more waiting: call hub_inbox again before you start work`. An attached channel session may also pull its own queue through `hub_inbox` while busy (#493);
|
|
22
|
+
- A pull-only peer (a Claude session without channel pushes, Kimi in its own TUI, or agy) is never drained by a push; `Bus.pull` hands it its queue oldest first up to `bus.pull_budget_bytes` (#489 H3; unset is `PULL_BUDGET_BYTES`, 64 KiB of `frame` text, #713; `takeBudget`), the first item always going so one huge envelope cannot stall it, and a poison one still alone. A push peer, and an attached channel session that is not pull-only, keep `take`'s `bus.digest_max` (at most `DIGEST_MAX`): never raise that for a pull. The daemon's `inbox` reply carries `waiting` and the plugin says `N more waiting: call hub_inbox again before you start work`. An attached channel session may also pull its own queue through `hub_inbox` while busy (#493); journal completion rules still apply. Drain and pull retain recovery, pause and lifecycle `deliveryHeld` fences; an explicit `hub_inbox` pull may pass only a `needs_review` uncertainty hold (#1079), returning newly queued work and bounded held-row metadata while leaving held originals and automatic delivery held. Orphaned dispatching/accepted deliveries, storage failure and unknown hold evidence still refuse. Both paths use `remember()` (so `reply_to` resolves), and a drain rechecks `deliverable()` after condensing because such a session can take the peer meanwhile. A pull is journaled `completed` in the same transaction as the bus snapshot, never `accepted`; a failed one puts back only that peer's queue and preface and never reloads the snapshot.
|
|
23
23
|
- Record exact task-offer reads only through `DeliveryJournal.completeDelivered` on an active-fenced adapter completion, correlated reply or actual pull, atomically with completion. Capture original offers before terminal pruning and record their ids, never condensed ids; accepted receipts, generic transitions, operator/hub dispositions and restored completed rows are not read evidence.
|
|
24
24
|
- The `onImportantQueued` nudge for a pull-only peer (#398) is advisory: it never dequeues, steers, journals or settles, and a throw from it never stops an enqueue.
|
|
25
25
|
- Merge recall and restored prefaces through `mergePrefaces`/`uniqueMemoryLines`; preserve distinct context and new notes, deduplicate memory identities, and keep colliding short prefixes distinct. Use `recallDisplayText`/`recallMemoryLines` before display caps: suppress only matching automatic approval outcomes, retain nonempty rejection details, preserve id headers and complete-word excerpts, and keep token trimming at line boundaries. Keep these changes display-only.
|
|
@@ -28,8 +28,8 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
|
|
|
28
28
|
- `resolveDelivery` mutates queues in place; a refused or failed resolution puts back only that peer's queue and preface and keeps every pause. Never reload a snapshot on a live bus. Keep the selected delivery id and journal revision fixed in console row actions; retry/discard/completed never change a task role or cancel a native turn.
|
|
29
29
|
- Recovery-deferred hub notices (`deferredHub`, #478) are accepted work kept outside the queues and the carried-queue integrity projection: never put them in `queues` or the integrity view while held. Every read of pending work includes them (`pendingEnvelopes`, `withdraw`, `revokeSupervision`, `queuedBacklogIds`, `get`, reset). Past `MAX_DEFERRED_HUB` refuse the publish; never drop a kept notice.
|
|
30
30
|
- 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).
|
|
31
|
-
- Do not offer recipient retry to delivery-only transports (`PeerAdapter.deliveryOnly`, set by the local worker and every ACP product) or pass ordinary messages through their hold;
|
|
32
|
-
- 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
|
|
31
|
+
- Do not offer recipient retry to delivery-only transports (`PeerAdapter.deliveryOnly`, set by the local worker and every ACP product) or automatically pass ordinary messages through their hold; explicit inbox reads retain the narrow #1079 exception. A recipient with no active turn needs the person to resolve its hold. Keep current-adapter refusal authoritative and the ephemeral MCP exposure marker advisory; pull-only Kimi TUI retains retry.
|
|
32
|
+
- 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 keep busy-peer retry consent limited to requeueing 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).
|
|
33
33
|
- 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.
|
|
34
34
|
- `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.
|
|
35
35
|
- 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`).
|
|
@@ -43,4 +43,4 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
|
|
|
43
43
|
- Concurrent-edit fan-out is linear in affected tasks: retain each notice's own-task stale/owner fence, but never persist the same change-set notice once for every task pair. Preserve per-task-pair research evidence separately (#576).
|
|
44
44
|
|
|
45
45
|
- Completed-change notices (#713): `Tasks.tellCompleted` publishes one notice per recipient per done through `whileOpenFor`, naming all of that recipient's overlapping tasks, and only for tasks the recipient accepted (`ownerAcceptance` plus an in-work state): never one per overlapping task, never for an offered or proposed task, never again for a repeat done without superseding. The envelope carries `refs.completedFor` (the done task); `Bus.enqueue` replaces an earlier queued, never-attempted notice for the same `completedFor` and recipient through `discardQueued` (a `superseded completed-change notice for task #N` journal outcome), and never touches a user message or another hub notice. `relevant` keeps a grouped notice while any of its tasks is still open for the recipient. Do not widen `completedFor` to anything but the done task id.
|
|
46
|
-
- `resolveDelivery` (#713): a `queued` delivery was never handed to the peer, so a busy peer
|
|
46
|
+
- `resolveDelivery` (#713): a `queued` delivery was never handed to the peer, and a `failed` one was refused before it ran (#1103), so a busy peer blocks discarding neither; `dispatching` and `accepted` stay refused as `active delivery`, and unrelated resolutions keep the busy fence. Only the recipient may explicitly complete/discard its own `needs_review` row through `resolveOwnDelivery` with an observed revision and reason (#1079), recording the peer actor through the existing journal audit. Another peer, remote-room disposition and active dispatching/accepted row refuse; this never replays the originals. Every other operator resolution stays fenced while busy. `resolveOwnDelivery` refuses with its own `PreEffectToolRefusal`, which names the cause and the next step, never through `resolutionRefusal`. A row that carries an invocation stays person-only (#1112). The daemon names the refusal from a closed list (`resolutionRefusal`), never from free error text. `queue discard --peer` goes through `resolveDelivery` row by row, reading each row's current revision just before its discard, never the revision listed at the start. Without `--state` it takes the peer's `queued` rows and its unsettled `failed` rows (wire `states`, sent only to a hub whose `features` list `queue_discard_states`; `needs_review` still needs an explicit `--state`). A row a resolution already settled (`settledIds()`) is history: `queue list` hides it unless `all`, and resolving it again is refused as `already resolved by <action>`, never `stale revision` (#1098, #1103).
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
- Conductor operational refusals (#812, #839): throw `OperationalAuthorityError` (`src/hub/operational-authority.ts`) for an authority refusal, never a plain `Error`. Only it sets `authorityRefused: true` and its scope on the reply, which the control `reply` wrapper audits once as a refused `agent_cli` event; any other failure of a labelled request keeps its own error with `authorityRefused: false` and is never audited as refused, and the CLI's `fail` audits only when `authorityRefused` is not `false`. An unreachable or dead hub is "authorization unavailable", never a missing conductor role. `publicOperationalView` is cached on the board status revision, config revision, routing and the private-path policy: a new input to that projection joins the cache key.
|
|
14
14
|
|
|
15
15
|
- Peer DOING (#878): `observeDoing` is the only writer and `doingRows` the only reader (`src/hub/daemon.ts`, helpers in `src/hub/peer-doing.ts`). Decide privacy at observation (`contextPii` or a refusing or throwing `tasks.publicMetadataText` latches a stub) and again at display (an open PII task owned or reviewed, or a private turn); reuse those screens, never add one. `status()` adds it only with `includeDoing` (the `status` reply and the dashboard snapshot), so `writeStatus` never writes it to `status.json`; the conductor's `hub_status` gets the same rows, each non-stub value framed by `publicConductorStatus` as untrusted (`sanitize`d text, `from`, `untrusted: true`, `doingNote`); never give an agent DOING text without that frame. Claude's title comes from the transcript path the facts hook reported and `claudeTranscript` validated, read through `readAiTitle` (1 MiB per read, lines over 64 KiB skipped); its current item only from PreToolUse input, never the transcript; every cached TaskUpdate subject, current or not, is screened when it is learned (`contextPii` or `tasks.publicMetadataText` refusing or throwing, via `ClaudeTodoSource`), keeps that verdict and passes it to `observeDoing` as `learnedPrivate`. A doing failure never changes a facts reply.
|
|
16
|
+
- Crash-recovery lines (#1111): `recoverAfterCrash` adds a line only through its `report(line, notice)`. Tag a line that asks something of a peer's owner with `peer`; it clears when that peer attaches. Tag the headline with `inFlight`; its count is read live from `crashLostIds`. A line about what recovery found or did gets neither and stays. Status shows only `crashView()` (`openCrashNotices`), never the raw list. `crashReported`, which the Jcode TUI hint reads, never clears.
|
|
16
17
|
|
|
17
18
|
- Conductor console ops (#933): a tools caller's `CONDUCTOR_TASK_OPS` request (`github_import`, `intake_approve`, `schedule_run|pause|resume`, `task_unassign`) and `goal_link` are admitted by `admitOperational` in `onMessage` before Tasks sees them, and the task op runs inside `operationalContext`, which `TasksDeps.operational` reads; never let `taskOpBody` admit a non-person caller to a console op any other way. `conductor_operational` `admit` audits a `CONDUCTOR_LOCAL_OPERATIONS` form once; `authorize` only rechecks the role. `task_catalog_role` from a tools caller passes the peer to `Tasks.setRole` (never `USER`), and is refused for a child, external, read-only or PII-context caller.
|
|
18
19
|
|
|
@@ -22,10 +23,11 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
22
23
|
|
|
23
24
|
- 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).
|
|
24
25
|
- `hub_invoke` over the control WS (#445, #991): the daemon binds each admission to its socket and `rid` with an `AbortController` (`invocationRequests`, at most 64 per connection; a duplicate `rid` refuses). `request_cancel` and socket close abort it, and every check after an await confirms the socket is open and still maps that `rid` to the same controller before any effect. Clients go through `ControlClient.request`, whose `controlRequestTimeout` raises a `hub_invoke` wait to `INVOCATION_CONTROL_TIMEOUT_MS`; never call it with a shorter bound.
|
|
26
|
+
- Control write outcomes (#1106, `src/hub/tool-request.ts`): `toolWriteOperation` is the one list of writes whose transport result can be lost (`hub_task_propose`, `hub_review`, `delivery_complete`). After a timeout, a cancel or a closed socket, `ControlClient` answers `outcome: unknown` with the settling read (`toolWriteUnknownReply`), never a plain failure. Replay goes only through `ToolWriteRegistry`, keyed by `requestId` and `toolWriteScope`, with one bucket per actor. `toolWriteCurrent` rechecks authority and privacy before a retained result is returned. The channel strips MCP `request_id` before `taskOp`, and `Tasks` never reads it. An explicit key is refused before sending to a hub without `tool_write_idempotency`. `delivery_complete` keeps the journal's idempotence and never uses the registry. A new retryable write joins `toolWriteOperation`; never add a second key path.
|
|
25
27
|
- `peer_stop` is the console's or an admitted conductor's (`admitOperational(sock, "stop")`) and stops, through its adapter, only an owner whose adapter reports `hubOwned === true` and whose `form` is not `tui` (today headless ACP, Pi, local, print-reviewer and Claude implementer); a present `undefined` form refuses. 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.
|
|
26
28
|
- 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.
|
|
27
29
|
- 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.
|
|
28
|
-
- 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`. Beyond `list`, the `queue` op is the console's or an admitted conductor's (`admitOperational`, `show`/`resolve`/`discard` 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`).
|
|
30
|
+
- 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`. Without `--state` the confirm counts the peer's queued and unsettled failed rows; the console sends that form only when the hub's `features` list `queue_discard_states`, otherwise it sends `--state queued` (#1103). Beyond `list`, the `queue` op is the console's or an admitted conductor's (`admitOperational`, `show`/`resolve`/`discard` 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`).
|
|
29
31
|
- Console Usage panel (#683 U3): tab `7`, text from `src/cli/console-usage.ts` (pure strings, no terminal or state import), rendered in `console-state.ts`, fetched in `console.ts` `refresh()` only while panel 7 is open. It reads the `usage` op and nothing else: no event-log read, no board field beyond the task id selected in Tasks. A refusal shows `usage unavailable`, never empty usage; an unknown counter is `unknown`, never 0; a private task is `#id [pii]` from the op's `taskPii`, never a title. The `usage` op folds with `catchUp(250)` and answers `behind: true` when it is not done: never answer from one pass of a big log. Keep Left/Right from 7 wrapping like Settings so the `1`..`5` cycle (#309) stays pinned, and keep PAUSE and 7D out of the width drop list.
|
|
30
32
|
- Dashboard snapshot `view` (#683 U4): the daemon's `ui_snapshot` and the manager's forward pass a request's `view` only through `dashboardView` (`DASHBOARD_VIEWS` in `src/hub/ui.ts`); keep that list equal to the page's `DASHBOARD_VIEWS` in `src/ui/index.html`. Any other value is dropped, never forwarded.
|
|
31
33
|
- acon plugin (#935): `src/ui/plugin.ts` never bypasses `uiAction`'s whitelist or `DASHBOARD_VIEWS`. It reaches project hubs only through `projectConsole` (`src/hub/manager.ts`), the same listing and forwarding `ahub ui --all` uses; widen neither for the plugin. Its inbox verbs map to the hub's own `permit` with an option the request offers. Its panel is built from `renderDashboard()` by exact rewrites that throw when a target line moves: when you edit a rewritten line in `src/ui/index.html` (post, the cookie helper, `$`, the theme `apply`, the view key listener, the title update or the landmark tags), update `panelAssets()` in the same PR. `lifecycle.ts` is loaded lazily there, because it reaches the daemon and CLI files acon's import scan reads. `ahub setup` registers it only through `registerAconPlugin` (`src/cli/acon-register.ts`): `console/plugins/ahub.json` under the hub home names the module and its sha256 (directories 0700, written to a temp file and renamed); the package export `./ui-plugin` is pinned by `scripts/check-package.mjs`. The Space panel (#1012, `src/ui/office-space.ts`) maps only the #264-allowlisted `officeModel` scene, pinned by the #264 marker fixture in `test/acon-plugin.test.ts`; never feed it an envelope body, task detail, path or symbol. A Space snapshot answers within acon's 5 s: the listing gets half of `SPACE_BUDGET_MS` and falls back to the last listing (a late one still replaces it), projects are read concurrently and a late one is left out of that poll, the cached office model is keyed by the daemon's `instanceId` (a restarted daemon restarts its ui sequence at zero), and `pollMs` stays under acon's 3000 ms link gap.
|
|
@@ -45,7 +47,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
45
47
|
- 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.
|
|
46
48
|
- 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.
|
|
47
49
|
- Default agent set (#608): `start.agents` is read only by the CLI at a person's `ahub up` and by the dashboard manager's start (`afterStart` in `src/hub/manager.ts`), through the console `peer_start` and so through `hubStart` (`src/cli/start-agents-run.ts`). The daemon never reads it: do not add a start from it to daemon startup, restart, recovery, `reloadConfig` or upgrade, and keep the `AGENTHUB_RECOVERY_OPERATION`, `--live-test` and console-caller guards on the `up` body together (each overlaps another, and removing one is caught only because the test removes them all). It is in `ALL_MACHINE_LOCAL` with an empty list kept as a value (`EMPTY_IS_VALUE` in `config-trust.ts`): an empty list means start none, never the default. `src/hub/start-agents.ts` and `config-apply.ts` stay free of file-system imports because the schema reaches the Claude channel bundle. `defaults.json` gained `start.agents`; `writeAutonomyDefaults` and `writeMachineStartAgents` each keep the other half, and any other key still makes the whole file untrusted.
|
|
48
|
-
- `ahub setup` reads Claude Code's state from `--json` listings and takes one step at a time, re-reading after each; never match paths or names by substring.
|
|
50
|
+
- `ahub setup` reads Claude Code's state from `--json` listings and takes one step at a time, re-reading after each; never match paths or names by substring. It installs and judges only the `user`-scope agent-hub plugin row; a `project` or `local` row is reported as preserved and never removed, and a marketplace removal that would remove one is blocked, not run (#1073).
|
|
49
51
|
- A copy of the git index keeps the original's mtime (`snapshot()`): git's racy-entry check compares entries with the index file's time in whole seconds, and a fresh copy makes a same-size edit look clean.
|
|
50
52
|
- State files that clients read (`status.json`, `control-token`) are written after the port is bound, and `status.json` via temp file + rename.
|
|
51
53
|
- Open native memory through the shared profile-aware helper with an explicit current selector binding; never fall back to shared v1 after invalid/missing generation metadata. Check that binding at hub call-site entry and before candidate/counter writes, including callbacks retained across awaits; do not intercept store methods with proxies/private hooks or claim atomic in-flight cutover. Quiesce and close consumers before migration/selection changes, then reopen through the supported lifecycle. Keep the sidecar at the independent memory root, not inside the selected generation. Report memory availability separately from bus health; never hot-rebind or migrate as a startup repair (`docs/memory.md` §Selected memory profile).
|
|
@@ -97,7 +99,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
97
99
|
|
|
98
100
|
- A Kimi one-launch TUI admission ends on its owning detach, roster removal, matched launcher cleanup or the unclaimed 30 s deadline. An old socket or launcher never clears a newer admission. Pin --mode tui in the hub-opened terminal argv.
|
|
99
101
|
- `native_hook_launch` (#543) is console-only and needs a definition with `hooks.factsHook`; one launch per peer. Refuse `begin` during recovery or shutdown, off roster or while the peer is attached. `end` needs the matching `launchId` and is the only request besides `status` and `kill` a stopping hub admits; closing the launcher's socket ends its launch. Accept `native_facts` only from the tools role for the current `instanceId` and `launchId`, the latched native session and socket generation, and workspace paths whose `realPath` is the project. Close a `ws-pull` peer's hello without `channel: false` with 4403.
|
|
100
|
-
- `native_tui_launch` (#464) is console-only and resolves only a running owned `JcodePeer` under its own kind id (`tuiLaunch`). Refuse it while stopping, during recovery, off roster, during that peer's roster change or requested stop, or when `jcodeMcpReady` fails against the daemon's `SERVER_JS`; a `binding` must name the current `launchId` and `sessionId`. Failures answer fixed errors with details withheld (a `JcodeOptionMismatch` names only the flag in `option`, #559), and the launch tuple (socket, env) goes only into that console reply, never into `status`, `status.json` or a log. A launcher sends it only when the hub's `features` list `native_tui_launch`.
|
|
102
|
+
- `native_tui_launch` (#464) is console-only and resolves only a running owned `JcodePeer` under its own kind id (`tuiLaunch`). Refuse it while stopping, during recovery, off roster, during that peer's roster change or requested stop, or when `jcodeMcpReady` fails against the daemon's `SERVER_JS`; a `binding` must name the current `launchId` and `sessionId`. Failures answer fixed errors with details withheld (a `JcodeOptionMismatch` names only the flag in `option`, #559) plus one closed cause `code` (a `JcodeTuiRefusalCode`, or `hub_busy`, `not_running`, `mcp_stale`, `bad_request`), logged as one `hub.log` line (#1093), and the launch tuple (socket, env) goes only into that console reply, never into `status`, `status.json` or a log. A launcher sends it only when the hub's `features` list `native_tui_launch`.
|
|
101
103
|
|
|
102
104
|
- When preserving a recovery source, copy only the exact-pinned `@staix/agent-mem` package through `preserveRuntimeDependencies` in `src/cli/recovery-package.ts`; include its identity and bytes in `packageDigest`. Never copy the whole node_modules tree or link a preserved source to a mutable installation. Verify preserved sources with `ownDependencies: true` so a missing or symlinked memory copy cannot resolve an ancestor installation. Verify staged releases with `dependencyRoot` bound to their retained install directory; never resolve beyond it. Keep no-dependency release digests compatible.
|
|
103
105
|
|
|
@@ -135,11 +137,13 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
135
137
|
- Anything the daemon starts on a timer and registers only with `startupCleanup` is not stopped by a normal `stop()`: `startupCleanup` runs on a failed startup only. Clear the timer and close the store in `stopOnce`, and wait for a pass in flight before the board closes (`schedulesPass`, #512a). A console op that runs the same writer on demand (`schedule_run`, #335) joins that wait through `manualSchedulePasses`; add any new on-demand entry to it, or `stopOnce` closes the board and the store under it.
|
|
136
138
|
- Corpus (#510a, `src/hub/corpus.ts`, `corpus-config.ts`): `corpus.paths` is machine-local and `corpus` is never editable from settings. Keep `corpus-config.ts` free of file-system imports, because the config schema (`config-apply.ts`) reaches the Claude channel bundle. A refusal of a root is a reason string, never a thrown error and never a partial read: evidence the check cannot read refuses, and only a completed read certifies a folder. The vault check is `.obsidian` at the real root, in every ancestor and anywhere below (any entry of that name), always on the `realPath`. A folder the pii, deny or local.deny rules protect is never listed, so it cannot be shown vault-free: the root that holds it is refused, never certified with that subtree skipped (a protected file is only skipped).
|
|
137
139
|
- Domain packs (#513a, `src/hub/packs.ts`): `ahub init --profile` resolves a shipped profile name first, then a pack through `loadPacks` (shipped: `templates/packs/` listed in `src/hub/shipped-packs.ts`; a project pack in `.agenthub/roles/<name>.pack.json` replaces a shipped one of the same name); keep `parsePack` closed (exact fields, bounded, plain ASCII, ids through `criterionId` from `src/hub/workflow.ts`) and never give a pack a field that carries a command, a path or an authority. The preview names a pack by ids only, never rubric or check text, and the apply goes through `packOverrides`, which `resolveProfile` validates. Reads use `lstat` and `O_NOFOLLOW` and refuse a hardlinked file, as the role catalog does. A pack whose roles are not in `loadRoleCatalog` is refused whole.
|
|
140
|
+
- `ahub init` settings (#1100, #1110, #1107): a settable key is one row in `INIT_SETTINGS` (`src/cli/init-settings.ts`, pure, closed value sets). Derive peer rows from `ADAPTERS`. Never add a row for an executable, secret, model, provider, instance or permission. The guided prompt runs only when `promptGate` holds: a person at a TTY, a new project, no setting flag, no `--yes` and no `--dry-run`; never for a delegated peer or a live test. It returns flag values and writes nothing, and its plan takes the autonomy row from `autonomyInheritanceChange`, the same helper `--dry-run` uses.
|
|
138
141
|
|
|
139
142
|
- Campaign dispatch (#597): keep `research_dispatch` additive to `research_records`; any stored campaign requires it, because old campaign stop policies also need current dispatch enforcement. Preserve the conditional dispatch metadata/handoff integrity digest and every other recovery feature guard. A study-dropped owner offer (#603) is a `research_dropped_offers` row written through `Board.recordResearchDroppedOffer` from `Tasks.relevant`, never an in-memory map: a restart between the drop and the approval must still re-offer. It joins `researchDispatchIntegrity` only when rows exist (so a project without one keeps its digest), requires a `research_units` row for its task and a peer-shaped owner, and `studyReoffer` clears it only after the offer is published or the record is stale.
|
|
140
143
|
|
|
141
144
|
- Experiment storage and runner registration (#598, #600): preserve `research_experiments`, its conditional integrity digest and every earlier research/calendar/autonomy guard. Read check evidence only through this project's host reader; no record input creates a measured field. Accept `research.runners` only in untracked `.agenthub/config.local.json`; refuse shared or tracked raw keys even when empty. Keep registration person-only, with whole-block tier2 structural edits and atomic scoped reload; never admit conductor writes. Consume registrations through `TasksDeps.researchRunners`, revalidating the active accepted configuration and current source trust each call; never adopt raw disk edits through that getter. While stored runner state or an active/raw runner key exists (empty counts), require `research_runners`. Include `researchRunners` in release-equality integrity only when stored receipts exist, using their count and stored-row digest alone; never bind accepted registrations or raw config presence to board-state equality across restart.
|
|
142
145
|
- `ahub study` (#601, `src/cli/study.ts`, `src/cli/study-output.ts`): `study.ts` is pure argv parsing (ids `rc-` and twelve hex digits, revisions as positive integers, `--reason` takes the rest of the words) and runs before any connection; `main.ts` reads the contract file against `invokedFrom` with a 64 KiB bound and sends it to the hub, which validates it. `show` and `experiments` (#602: `--campaign`, `--attempt` or `--plan --baseline --variant`, the same read as `hub_experiment_show`) are `allowed` in `classifyPeerCommand` through `peerReadForms`; draft, revise, approve, approve-protocol and close are `console`, and the audit label names at most the subcommand (approve-protocol is labelled `study`; a campaign id, a protocol hash and a contract path are the person's text). The default output is for a person and `--json` prints the hub's text byte for byte (`test/output-cli.test.ts`); a new `study` form needs its `--json` branch and a renderer, never a `TEXT_DEBT` entry.
|
|
146
|
+
- `ahub specs` (#1097, #1107, `src/cli/specs.ts`): `list`, `check` and `covers` are read-only and run without a daemon. `specs stamp` records a person's review: `classifyPeerCommand` refuses it from every agent shell, live-test delegation included, and it needs a TTY even with `--yes`. Never add a delegated or conductor form of it.
|
|
143
147
|
|
|
144
148
|
- Experiment comparisons (#599): preserve the separate conditional `research_comparisons` floor and integrity for plans, exact protocol/evaluator approvals and causal protocol-context rows. Never backfill legacy order from timestamps or carry approval to a changed hash. Keep the approval op under the console-only `study_` dispatch guard.
|
|
145
149
|
- Doctor memory capture (#1008, #1025): the `memory capture: <tool>` rows come only from `amem setup <tool> --json`, a preview that writes nothing (`readMemorySetup` in `src/cli/main.ts`: no shell, 10 s, 256 KiB), judged by the pure `nativeMemoryCaptureRow` (`src/adapters/definitions/native-cli-doctor.ts`). A plan not in the documented shape, a non-zero exit, a timeout or unparsable output is unknown, never ready; never read dotfiles text or a client's config, and do not scrub the caller's identity before the call.
|
|
@@ -20,6 +20,7 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
|
|
|
20
20
|
- The planner "board changed" notice (#758, only with the lifecycle on) is built from what was added to a card's history since the last scan (`Tasks.groomNotices`, `groomScan`), never from the status revision alone: skip the peer whose own entry it was (`entry.by`), tell only the card's planners (`taskPlanner`, #525-2), send a planning revision or dependency change (a `planning` entry, or `deps` that differ from the last scan whatever wrote them: a propose with `after`, a split) and a card's creation (a peer's propose, a split child, a generated card) at task weight, since new work and its dependencies are what a plan review consolidates, and replace an unsettled copy (`sendGroomNotice` withdraws it by the `The board changed` prefix) so a planner meets the latest revision once, and send state moves, reviews and assignments at `fyi` weight (recorded, no turn), at most once per planner per current `tasks.groom_fyi_window_min` (default `GROOM_FYI_WINDOW_MS`, 10 minutes); read the host config at each notice flush, never capture it at construction. The hub's own bookkeeping entries (`by` is `hub` with no `planning`: lifecycle links, idle sweeps, holds, plan observations) tell nobody and spend no window; a deps change with no new entry has no known author, so no planner is skipped for it. Scan every configured planner, not only the attached ones, so a change made while one was offline is kept for it. The first notice a planner gets after a hub start is task weight. Keep the scan out of the status revision and never read history that is not new.
|
|
21
21
|
- Planner work-ready notices (#785 P1a, `writePlannerNotices`): a write that can free a reserved owner's WIP or release backlog (done, approval, decline, cancel, backlog return or pull, untouched return, pull undo) takes `capacityBefore` (the `wip()` of the task's owner and reservation) before its board write and calls `writePlannerNotices` after it. Each rostered planner of the task (`taskPlanner`) gets one ids-only `task` envelope starting `Planner work ready:`: `capacity_released: <peer> WIP <n>; reserved ready #<id>|none` for a peer whose WIP dropped, and, when the write approved the task, `backlog_ready: #<id>` for each backlog dependent with nothing left to wait for. Merge the lines of that planner's unsettled copies, read back from `bus.pendingEnvelopes` (they survive a restart; never keep them in memory), withdraw them, then publish, so at most one stays queued per planner; all of it runs inside `releaseNotice`. The notice only wakes the planner: never pull, reserve or assign from it, and never put a title or note in it.
|
|
22
22
|
- Opt-in auto pull (#785 P1c, `tasks.auto_pull`, `Tasks.queueAutoPulls`, `autoPull`, `topReservedReady`): `writePlannerNotices` collects the peers whose WIP this write lowered and, only when `TasksDeps.autoPull()` is true (read at each release, so a reload applies to the next one), queues one `autoPull(peer)` per peer on a promise chain, so two releases never pick the same card and each job re-reads the board. It takes the top backlog card explicitly reserved for that peer (`topReservedReady(peer, undefined, true)`, the rule the notice's `reserved ready #id` line also uses; never an unreserved card, never PII, which `topReservedReady` skips for a pull), finds the card's own planner of record (`taskPlanner(card)[0]`, which must be rostered) and calls `pullPlanned(HUB, input, { onBehalfOf: planner })`. `onBehalfOf` is accepted only with `by === HUB`; the planner's authority is what `planningAuthority` checks, the actor in history and the planning telemetry stays `hub`, and the planner binding records the planner it acted for. Every other check is `pullPlanned`'s own, unchanged, and a refusal is one console notice with the public title (never detail). A held or recovering hub, a storage error or the setting off pulls nothing, and that is checked twice: at entry and again by `pullPlanned`'s `opts.guard` (`autoPullAdmitted`) wherever authority is rechecked, which includes right before the assignment or board write after the awaited PII screen and `planReviewGap`, so turning the setting off or starting recovery or shutdown mid-admission writes nothing and reports nothing. `autoPullsPending()` counts queued and running pulls and `recoveryReady` waits for it (also named in the recovery view's blockers), as it does for `checksPending`. The key is in `ALL_MACHINE_LOCAL` (`config-trust.ts`) so a committed config is ignored for it, and it is not in `CONDUCTOR_SETTING_KEYS`: an agent never turns it on. Never add a timer or sweep that pulls: the only trigger is the write that freed the slot.
|
|
23
|
+
- Planned start (#1092): `plannedStartPending` (`src/hub/task-sweep.ts`) is the only test that a card's `plannedStart` is still ahead. Every automatic path skips such a card: dependent readiness, `backlog_ready` notices, the reserved-ready pick, auto pull and its `guard`, a hub-actor (`by === HUB`) offer, pull or assign of an ownerless backlog or proposed card, and the idle sweep. The planner digest shows it as `scheduled`. A new automatic path asks it too.
|
|
23
24
|
- Per-head CI watcher (#785 P1b, `src/hub/ci-watch.ts`, `Tasks.watchCi`, `ciCompleted`, `reofferHeldReview`): when a `done` leaves a review whose admission was not ready at the task's exact full-commit head (or a review is held as premature in `sendReview`), `CiWatchers` starts at most one watcher per task and head, with chained unref'd `setTimeout`s and no interval. Each poll is one `gh pr view` through the `sdlc.gh` port (`pollCi`); it ends on a passed result for the required checks, on `CI_WATCH_DEADLINE_MS`, on `stopCiWatchers` (daemon `stopOnce`), or silently when `ciWatchCurrent` fails (the task left `in_review`, its head or PR changed, the lifecycle is off, it turned private). `complete`, `review` (both verdict writes) and `cancel` cancel it. Each (head, outcome) is told once (`settled`), and only `passed` settles a head for good (#540): after `failed` or `incomplete` the watcher keeps reading for a rerun of that head (the first pending read after each newly told result is a new run attempt and renews the deadline once; later pending or unreadable reads and an unchanged failed result never renew it, so the watcher always ends; a deadline after a told result ends silently), and a later `hub_task_done` at that head starts one again. A newer head replaces the watcher. The result is one ids-only `ci_completed:` task envelope at `status` weight to the owner and the reviewer (`fyi` is never delivered to a peer, so it would not reach them), a console notice for `user`, and for a deadline a console notice only. A passed or incomplete result re-offers a review held as premature once (`heldReviews`, keyed by task, head, reviewer and review revision) after a fresh `reviewRequestAdmission` says ready; a failed one re-offers nothing. No task text goes in the notice, no PII task is watched, and nothing is restored after a hub restart (the next `hub_task_done` starts one). Never add a second periodic gh read outside this watcher.
|
|
24
25
|
|
|
25
26
|
- Completed-change notices (#713, one per recipient per done, accepted work only, superseding by `completedFor`): rule in `bus.md`.
|
|
@@ -72,6 +73,7 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
|
|
|
72
73
|
- Plan conformance (#481 S2, `src/hub/plan-conformance.ts`): compare only literal repository-relative `plan.paths` entries (file or directory prefix, no glob) with complete PR file metadata; `completePlanInventory` requires a full head, `pathsComplete` and `typesComplete` (false on any rename or copy). Root, absolute-unverified or unsafe entries, a missing plan, a truncated inventory and a failed read are `unknown`, never covered; never read patches or the filesystem. An untouched planned path is advice; a changed path outside the plan is a departure, and `planConformanceBlockers` blocks enforce approval on `unknown` and on an unaccepted departure. Plan `proof` and `risks` (32 strings of 512 characters) cover no file. `Board` records `planRevision` only while `trackPlanRevisions` (mode not off), separate from backlog `planningRevision`.
|
|
73
74
|
- Departure consent is written only by `Tasks.acceptPlanDeparture` for the person (`ahub sdlc accept-departure <task> <head> <reason...>`; no peer tool) on a currently known departure at the current PR head, never on `unknown`, and never for a PII task. It binds repo, PR, head, plan revision and fingerprint, the inventory, `planScopeIdentity` and this host's `departureGeneration`, so a restart or a change to any of them invalidates it (`validPlanDepartures`). Status and merge-queue reads never write the board: they only note observations and invalidations in memory (`observedPlans`, `invalidatedPlanDepartures`), which `planObservation()` attaches to the next owner or reviewer transition write. Off captures no observation or revision; a plan or refs change still invalidates an existing acceptance.
|
|
74
75
|
- The review coverage gate (#520) pins `refs.commit` and the sdlc receipt and rechecks them in `valid()`; an unavailable brief is never an empty one (enforce refuses, advise notes). Read brief attributes and rules at the merge base (`--attr-source`, `check-attr --source`, `git show <base>:`), never from the head or the checkout.
|
|
76
|
+
- Design-spec coverage on `hub_task_done` (#1102): `inspectTaskSpecs` inspects at the declared full head through `TasksDeps.inspectSpecs`. A `specReport` is current only for that head and `specRefBinding`. `spec_ack` is refused for PII or private-turn work and while specs review is off. Each id and reason is screened (`publicMetadataText`, `withholds`) before it is written. In `require`, an unavailable inspector refuses the done, because unknown coverage is never covered.
|
|
75
77
|
- A `routing.reviewOnly` definition is refused as owner inside `assign()`'s eligibility check, for every class, nomination and reservation; keep it there, not at call sites. A definition with `reviewer.unavailable` is refused as reviewer there too (code `policy`); unavailable never means pass.
|
|
76
78
|
- A definition's `routing.allowedRoles` is a fixed role ceiling (#543): `assign()` refuses a peer whose required role (reviewer for the reviewer role or the `review` class, planner for `plan`, otherwise implementer) is outside it, with code `policy`, and `loadConductorPolicy` drops project `roles` entries outside it with a config notice. `planner` also needs `projection.plannerEligible` (#525-1): `loadConductorPolicy` drops it from a kind's entry and `effectiveInstanceRoles` from an instance-id entry. Never widen either from project config.
|
|
77
79
|
- Reviewer pool (#519): `selectReviewerPool` is pure and called only inside `assign()`, which builds the candidates (declared reviewer-only `routing.reviewOnly` instances) and their blocked reasons; Tasks passes only `reviewPool` (`pool`, `rerotate`, `failed`, `required`). With no fresh reviewer declared and no configured `review.pool`, routing stays the legacy path. The seed is the linked PR number, else the task id; compute its modulo over configured kinds with declared candidates, including blocked/offline/paused/quota/failing candidates, and do not renumber by availability; append undeclared-kind trace entries (`no declared reviewer`) after the declared walk without assigning them modulo shares or changing a selected reason; the implementer's family is skipped while another can run, and a delta review keeps the last `changes_requested` family unless `rerotate`. `required` (an explicit `review.pool`) exhaustion goes to the person with `reviewBlocked`; only the optional default pool may fall back to an eligible planner, other family first. Record the choice through `reviewMetadata` (`reviewerFamily`, `reviewerReason`, including `same-family fallback`), never in note text alone. PII tasks, `review`-class tasks and a reviewer a person named (`reviewer changed`, reason `manual`) stay outside the pool.
|
|
@@ -103,6 +105,9 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
|
|
|
103
105
|
- Backlog issue planning (#759): allow only `fields.sdlc.repo`/`issue` on unstarted ownerless backlog through `Tasks.planBacklog`, scoped to the effective planner/person; a PII card's identity is person-only and a public one must pass `publicMetadataText`, both pre-effect. Keep arbitrary lifecycle receipts refused on backlog. Reuse lifecycle identity invalidation, keep the planning write atomic and recheck after awaits. Store identity-only undo values; never revive old spec acceptance. Omitted/empty planning fields with a bounded reason record a note-only revision and retain normal pins/frozen/stale guards.
|
|
104
106
|
- Grooming duplicate candidates (#759): suppress only pairs related by directed dependency reachability, either direction, using bounded public ID/dependency context. Include approved intermediate phases when available; a walk that hits its bound marks the digest truncated. Keep overlap findings, unrelated same-issue duplicates and privacy filtering; never infer follow-ups from prose or supersession text.
|
|
105
107
|
|
|
108
|
+
- Declared needs (#1081, 2026-11-02): `needs_network` and `needs_worktree` are task signals set only by an explicit `true` in `Tasks.propose` (never inferred from text). `assign()` in `routing.ts` reads each as a refusal code (`network`, `worktree`) against `PeerCapabilities.network` / `.worktree === false`, and `Tasks.declaredNeedCheck` repeats both at accept, before any write: that check is what stops a sandboxed peer from accepting work, failing at `git worktree add` (#270) and leaving an empty branch ref (#621, #565). `peerCapabilities` in `daemon.ts` is the only producer: unsandboxed peers are `true` for both, hub-sandboxed peers have `worktree: false` (always, with the #270 reason) and `network` from `sandboxNetwork !== false` (the one `local.bash_network` setting). Unknown (no entry) is never a refusal. Keep the four lists together when you add a need: `RoutingRefusalCode`, `TASK_STALL_CODES`, the `requireCapabilities` filter and `declaredNeedCheck`. A hub-made writable worktree for a sandboxed peer is the separate spec issue #1115, because it moves the sandbox write boundary; do not build it as part of a need.
|
|
109
|
+
- Paged `hub_task_list` (#1083, 2026-11-02, `src/hub/task-list-page.ts`, the `hub_task_list` case in `daemon.ts`): an agent (`by !== USER`) always gets `{tasks, total, next?}` and the console gets the bare array unless it names `limit`, `cursor`, `ids` or `detail`; keep that split, because `ahub board`/`ahub tasks` and `test/cli-json-guard.test.ts` read the array. Build every row with the existing `listRow` (it starts from `tasks.publicView`, so PII stays a stub and the `board-column-started-projection` seed text is unchanged) and only then reduce it with `compactRow`; never build a compact row from the raw `Task`. The cursor is a keyset (`updated.id`), not an offset. A new caller of `hub_task_list` as a peer reads `.tasks`, or passes `ids` for one task. Do not raise the 200 cap or the default 50 without re-running the 600-task size test.
|
|
110
|
+
- Urgent accept and the WIP refusal (#1080, 2026-11-02): `acceptanceCapacity` takes an `urgent` mode, and only `accept` passes `"displace"`; `acceptReviewed` and the early check in `accept` pass `"check"`, which never writes. `displaceOffers` runs immediately before the accept write, after every gate (spec, plan, study), so a refused accept can never have taken back an offer. It takes back only offers the owner never accepted (`displaceable`: state proposed, owned, not pinned, not urgent, no `taskHasStarted`), newest handed first, and only as many as the limit needs; if they are not enough it takes none and refuses. The refusal comes from `wipSplit` and names accepted, offered and reserved separately, with a next step. Do not widen `displaceable` to accepted or urgent work, and do not call the displace mode from any other path.
|
|
106
111
|
- Planner WIP breakdown (#784 AC3, `Board.plannerWipBreakdown`, `Tasks.wipBreakdown`): a read-only split of `plannerWip` into `active`, `reviewWait` and `reserved` for status, the console Peers detail and the dashboard legend. Read it from ONE grouped query for every peer (the status snapshot builds on a hot path); never loop `wip()` per peer or per task there. Admission, `reservedHold`, `acceptanceCapacity` and the planner pull keep reading `wip()` / `plannerWip` unchanged, and the three parts must add up to it: change the two queries together. The new `taskWip` fields are additive; renderers keep printing the plain count when an older hub sends none.
|
|
107
112
|
- Review request CI evidence (#824, `Tasks.hubVerifiedBlock`, `ReviewReadiness.checks`): `sendReview` renders one `Hub-verified at <head>` block from the `admission` it already carries and never makes a `gh` call of its own (the AC5 test pins the admission path to its three reads: a fourth call fails it). Its words come only from the hub's readiness read and recorded `check passed` events, never the builder's summary or a `hub_sdlc_record` claim; a PII task and a request with no admission get no block. A producer of `ReviewReadiness` (`readReviewReadiness`, `reviewReadinessFromEvidence`) must fill `checks` or the block names no checks. The checklist's `{check}` line drops "run the affected tests yourself" only for a ready admission with no `check passed` event; keep the #223 R2 withheld summary.
|
|
108
113
|
- Verdict mirror (#786 T3, `src/hub/sdlc-mirror.ts`, `Tasks.mirrorVerdict`): one `COMMENT` review per (task, head, verdict), called after the approved and changes_requested writes in `Tasks.review`, never awaited and never throwing (a failure is a number-only notice). The only review write is `gh api repos/<repo>/pulls/<pr>/reviews -f event=COMMENT -f commit_id=<head> -f body=<text>`, pinned by `assertMirrorArgv`: never `APPROVE`, `REQUEST_CHANGES`, another endpoint or a method flag, and keep the `test/sdlc-mirror.test.ts` argv test. Idempotency lives on GitHub (list the PR's reviews for the headline at that head), not on the task, so it adds no board or receipt field and survives a restart; an unknown read posts nothing. The note goes out only when public (`!isPii`, not withheld, not a private turn, `publicMetadataText`); a PII task mirrors the headline alone. `mirrorVerdict` reads the PR head and state again AFTER the review listing and judges on that last read with nothing awaited before the post (a head checked before an await is not trusted after it; the same invariant as the guarded merge's dispatch): a new read in that module means a new re-read after it and a fake-gh test that moves GitHub during it. An approval's mirror settles before `autoMerge` starts (`autoMerge(task, after)`), so keep that order. It follows `auto_merge` through `mergeModeFor`.
|
|
@@ -8,12 +8,13 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
|
|
|
8
8
|
- Tests that are not about batching build the bus with `batchMs: 0`; with the default 15 s window a lone status envelope looks like a lost message.
|
|
9
9
|
- A test that writes an executable shim and then times a product call through it (a `git` wrapper on `PATH`, as `test/memory-code-head-684.test.ts` does) runs the shim once by absolute path first, outside the measured region: the first exec of a newly written executable costs about 250 ms on macOS (#519). Never widen the product's bound or weaken the assertion for it.
|
|
10
10
|
- A test timeout that fires while `Bun.spawnSync` runs can start the next test inside spawnSync's own event loop (Bun 1.3.14 and 1.4.2; background: README). `scripts/check.sh` runs tests with a 20 s timeout and under `scripts/hang-watch.sh`; a test that spawns and reads the process table gets a timeout of its own, never above 20 s: an explicit larger timeout overrides the gate's `--timeout 20000`, so shorten the scenario instead.
|
|
11
|
-
- Sandboxed commands and the MLX server get a fixed environment (`PATH`, `HOME` and their own keys), so test knobs for their fakes travel in a wrapper script or argv, not in `process.env
|
|
11
|
+
- Sandboxed commands and the MLX server get a fixed environment (`PATH`, `HOME` and their own keys), so test knobs for their fakes travel in a wrapper script or argv, not in `process.env`. Daemon-owned peer children use `peerChildEnv`, which drops `ORCA_*` and replaces agent identity markers; a person-owned native launch preserves only its own pane's `ORCA_*` context while dropping inherited agent identity.
|
|
12
12
|
- Bind agent identity markers explicitly in native/CLI fixture children. The full gate starts the test runner without inherited agent markers (including `AGENTHUB_DELIVERY_ONLY`) and without an `ahub claude` session's hub variables (`AGENTHUB_PROJECT_DIR`, `AGENTHUB_STATE_DIR`, `AGENTHUB_INSTANCE_ID`, `AGENTHUB_LAUNCH_ID`); a new variable a native session exports and a fixture child reads joins that `env -u` list in `scripts/check.sh`. Never weaken production marker detection to make them pass.
|
|
13
13
|
- A fixture that spawns the real channel plugin (`src/adapters/claude-channel.ts` or `plugins/agent-hub/server.js`) to act as a peer runs it with `cwd` at the hub's project root, made with `realPath` (the macOS temp dir is a symlink). The plugin chooses its hub through `channelProject` (rule: `daemon.md`), so from another cwd it joins the wrong hub, joins as a distinct session or connects nowhere. Only tools mode with both `AGENTHUB_STATE_DIR` and `AGENTHUB_PROJECT_DIR` set skips that check.
|
|
14
14
|
- Orca environment (#613, #1048): the `test/hub-home.ts` preload also deletes every inherited `ORCA_*` variable, and `scripts/check.sh` unsets them before `bun test`. A run from an Orca pane otherwise reaches the person's Orca through `ORCA_WORKTREE_ID`, `ORCA_TERMINAL_HANDLE` and `ORCA_CLI_COMMAND` (the launcher's four names are `ORCA_LAUNCHER_ENV` in `src/hub/orca-env.ts`, the one list; `personEnv` drops the whole prefix) and opens real tabs and agents there. A test that needs one sets it itself after the preload and restores it; a test of the launcher uses a stub `ORCA_CLI_COMMAND` and never the real one. `test/orca-env-guard.test.ts` runs a real `ahub up` with fake values and a recording stub and must see zero calls.
|
|
15
15
|
- `bunfig.toml` preloads `test/hub-home.ts`, which first clears inherited `AMEM_HOME`, gives every test run (the gate and `bun test <file>` alike) a temp `AGENTHUB_HOME` unless one is set, and always replaces `KIMI_CODE_HOME` with a temp dir, an inherited value included (#285); scoped tests may set their own `AMEM_HOME` or `KIMI_CODE_HOME` after preload. Hub memory (#258) is on with any project config, and no test may write the person's `~/.agenthub`. Do not remove it; a test that needs its own home sets and restores the variable. The preload keeps an inherited `AGENTHUB_HOME`, which an agent shell exports as the person's hub home and `scripts/check.sh` does not unset: run `bun test` and the gate with `env -u AGENTHUB_HOME`.
|
|
16
|
-
- `test/registry-guard.test.ts` statically scans `src/` against the committed `test/registry-guard.baseline.json` (#451); a test never writes, regenerates or repairs that baseline, and a failing guard is fixed in the source by moving the policy into a definition (rule: `docs/agent-notes/adapters.md`); the baseline stays `[]`. Scanner behavior is pinned on inline sources in `test/registry-guard-fixtures.test.ts`, never on edits to real files.
|
|
16
|
+
- `test/registry-guard.test.ts` statically scans `src/` against the committed `test/registry-guard.baseline.json` (#451); a test never writes, regenerates or repairs that baseline, and a failing guard is fixed in the source by moving the policy into a definition (rule: `docs/agent-notes/adapters.md`); the baseline stays `[]`. Scanner behavior is pinned on inline sources in `test/registry-guard-fixtures.test.ts`, never on edits to real files. A new table, hint or flag list in generic code that names a peer (`"pi"`, `isJcodePeer`) is a new group: derive it from the definitions instead (`ADAPTERS.filter(...)`, a port such as `statusNotice`). Missed twice on 2026-11-02 (PR #1093 `jcodeTuiHint`, PR #1100 `INIT_SETTINGS`), both found only by CI's shard 3; run `scripts/check.sh --changed` or `bun test --timeout 20000 test/registry-guard.test.ts` before pushing a new peer-shaped table (a bare `bun test` times out at 5 s on this file).
|
|
17
|
+
- A daemon fixture whose `cwd` is the repository root sees this checkout's own design specs (`docs/design/`): a test that is not about specs sets `specs: { ...DEFAULT_CONFIG.specs, enabled: false }`, or runs in a temp project, so a new spec never changes its output; never absorb a `Specs covering` line into an unrelated expectation (#1117). The committed `AGENT_HUB_SPECS` block in AGENTS.md is pinned to `specsBlock('', true)` by test/repository-specs-block.test.ts; regenerate it with `ahub init --specs`, never edit it by hand (#1119).
|
|
17
18
|
- A fixture that needs a port takes it from a free-port probe, never a fixed number, which collides with whatever holds it on a shared runner (#548). The probe binds `127.0.0.1`, matching the loopback server that takes the port (#232).
|
|
18
19
|
- `ahub upgrade` without `--to` asks the registry (`npm view`) and hands a target newer than the CLI to `bun x` (#272): a test that reaches either puts a fake `npm` first on `PATH` and a recording stand-in for `process.execPath`, as `test/upgrade-cli.test.ts` does. The interactive screens are tested through a scripted `ScreenIO`; a CLI test that needs them marks `process.stdin` and `process.stdout` as terminals in a wrapper script.
|
|
19
20
|
- Cross-process fixtures wait for the consuming client to process its handshake (the channel connected diagnostic), not merely the daemon's attached/idle state. Deadline-routing fixtures hold and release responses at the fallback decision; do not place a fixed sleep between deadline fractions and assume scheduler headroom (#287). Receipt-order fixtures use the normal watchdog for ordinary I/O, trigger expiry after observed native acceptance, and await a consumed transport barrier before asserting on late results.
|
|
@@ -24,13 +25,13 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
|
|
|
24
25
|
- A test that proves a daemon answers while work is still running asserts ordering (the answer arrived before the producer finished), never an elapsed wall-clock bound: the daemon promises no millisecond figure for `status`, so a bound measures the runner, not the product (#531); a request timeout in such a test is a hang guard only.
|
|
25
26
|
- A tight wall-clock bound in a test must be sized for the slowest CI runner, or replaced by a barrier (#559). Hosted macOS shards run each test about 1.5 times slower than the old single leg (and git-heavy tests about 7 times), so three unchanged tests tipped over once each after #954: `test/path-policy.test.ts` (the #541 hardlink loops took 10.6 s idle for three layouts in one test and 27 s on a shard, past the 20 s limit: the loops are now five parts of 1000 iterations per layout, 5000 per layout kept, about 0.8 s per part), `test/code-facts.test.ts` (a 400 ms deadline raced the fake CLI writing its child marker, which takes about 0.3 s idle and 1.4 s at 30-way load: the test now holds the deadline timers by the configured value, waits for the marker and fires the analysis's own deadline callback, a barrier with the real deadline code; because the held deadlines never fire by themselves, its `finally` must `close()` the reader and await the analysis before the fixture is removed, and a second test pins that teardown) and `test/smoke-pty.test.ts` (the macOS PTY bridge hung inside `Bun.spawn` with a `terminal` when its child wrote and exited before the call returned: Bun up to 1.4.3, oven-sh/bun#45300, fixed in 1.4.4; 26 of 96 bare-Bun runs at 12-way parallel, none of 180 on 1.4.4. A longer timeout cannot help a process that never starts and a retry only lowers the odds, so the bridge starts its child behind a gate it opens after `Bun.spawn` returns and the retry helper is gone (#560); the bridge also stops its child when its caller dies, and a test names a hung bridge with `runPtyWith` instead of relying on load). When a test needs a bound, measure the slow tail under 30-way local load, size the bound from it with the number in a comment, and prefer a readiness signal (a marker file the fake writes before it hangs, with the deadline fired after it) over a shorter deadline or a longer margin: a margin still depends on the runner's speed, a barrier does not.
|
|
26
27
|
- A request that a test expects a stop or shutdown to cut gets its rejection handler where the request is created, never after the awaited teardown: a late `catch` races the reset and the run fails on an unhandled rejection.
|
|
27
|
-
- Run `bun scripts/seeded-check.ts` before reporting the seeded-guard gate complete. After changing seeded guard logic, run its affected seeds even when search-anchor counts are unchanged. Put asynchronous calls inside `resolves`/`rejects` expectations so a mutated refusal fails a named assertion. Keep the `scripts/seeds.json` cases sequential; require the same named test green before its seeded assertion fails. Seed rot, survivors, setup/compiler failures, timeout/watchdog termination and process leaks fail the gate. A change that moves or rewrites a line a `scripts/seeds.json` case searches for updates that case's search and replacement in the same PR (seed rot fails the gate), keeping its mutant effect and named test. Never run the full seed runner from a unit test or mutate the source checkout; inspect preserved failed fixtures before removing them. CI runs it as four sequential Linux shards (`--shard i/4`) beside the ordinary check, separately from `scripts/check.sh`; the required `seeded guards` job passes only when every shard passed, or when the decision job reused a verified identical-tree full gate (`admission == 'reuse'`). Keep that job name: branch protection and `scripts/ci-gates.mjs` read it (#457). On seed rot the checker names the anchor's symbol at a base commit and where it is now (`seedRotContext`): `--base <commit>` sets that base (CI passes the PR base from a `fetch-depth: 0` checkout; keep both), and without it the base is `merge-base origin/main HEAD`. The context is evidence only: it never relaxes the one exact match, and an unavailable base says so instead of guessing.
|
|
28
|
+
- Run `bun scripts/seeded-check.ts` before reporting the seeded-guard gate complete. After changing seeded guard logic, run its affected seeds even when search-anchor counts are unchanged. Put asynchronous calls inside `resolves`/`rejects` expectations so a mutated refusal fails a named assertion. Keep the `scripts/seeds.json` cases sequential; require the same named test green before its seeded assertion fails. Seed rot, survivors, setup/compiler failures, timeout/watchdog termination and process leaks fail the gate. A change that moves or rewrites a line a `scripts/seeds.json` case searches for updates that case's search and replacement in the same PR (seed rot fails the gate), keeping its mutant effect and named test. `scripts/check.sh`, `--changed` included, never runs the seeds, so rot shows only in CI's `seeded guards` job (the `profile-cli-dry-flags` anchor needed a search-text update in both #1100 and #1110, 2026-11-02). Before pushing a change to a file `scripts/seeds.json` names, confirm every anchor still matches once: `bun -e 'const f=require("fs");for(const x of JSON.parse(f.readFileSync("scripts/seeds.json","utf8")).seeds){const n=f.readFileSync(x.file,"utf8").split(x.search).length-1;if(n!==1)console.log("seed rot:",x.id,n)}'` prints nothing when every anchor is intact. Never run the full seed runner from a unit test or mutate the source checkout; inspect preserved failed fixtures before removing them. CI runs it as four sequential Linux shards (`--shard i/4`) beside the ordinary check, separately from `scripts/check.sh`; the required `seeded guards` job passes only when every shard passed, or when the decision job reused a verified identical-tree full gate (`admission == 'reuse'`). Keep that job name: branch protection and `scripts/ci-gates.mjs` read it (#457). On seed rot the checker names the anchor's symbol at a base commit and where it is now (`seedRotContext`): `--base <commit>` sets that base (CI passes the PR base from a `fetch-depth: 0` checkout; keep both), and without it the base is `merge-base origin/main HEAD`. The context is evidence only: it never relaxes the one exact match, and an unavailable base says so instead of guessing.
|
|
28
29
|
- `scripts/lib/ts-ast.ts` is the one TypeScript AST helper for scripts and source-scanning tests (`parseSource`, `callSites`, `symbolSpans`, `enclosingSymbol`, `findSymbol`; inline HTML scripts keep their offsets). It gives lexical locations only, never resolved call edges, and stays dev tooling: never import it from `src/`.
|
|
29
30
|
- The native branch ratchet (`test/daemon-native-branches.json`) is reviewed data, never regenerated by a test (rule: `adapters.md`). `test/daemon-native-proof-gates.test.ts` runs daemon code cut out of `src/hub/daemon.ts` by its text, from ` function frontendProxyState(` to ` const manualPaused =` and from ` async function changePermission(` to ` async function confirmPermissionDefault(`, and the ` const isSupportedPeer =` and ` const readOnlyPeer =` bindings: renaming, reordering or reindenting them updates the test in the same PR. Test Claude launch permission proof through `createClaudeLaunchProof` in `src/hub/claude-launch-proof.ts`; never restore the removed daemon wrapper just to slice it in a fixture.
|
|
30
31
|
|
|
31
32
|
- `scripts/check.sh` pins the test environment (#610): `GIT_CONFIG_GLOBAL=/dev/null`, `NO_COLOR` unset, `TERM=xterm-256color`, temp root under the OS per-user temp dir, agent markers cleared. A test that needs a person's git config or color settings sets them itself. `--changed` selects tests with `scripts/changed-tests.mjs`: changed tests (nested suites included) plus every test that names a changed file directly; docs need none; any other changed file no test names, and any gate, package or shared-fixture change, runs everything.
|
|
32
33
|
- `scripts/check.sh` holds the repo's heavy-job slot (`<git common dir>/ahub-heavy.lock`, #398) for the whole run, so a test never runs the real gate; `test/check-lock.test.ts` runs a copy in a scratch repo.
|
|
33
|
-
- The whole-suite watchdog is `scripts/hang-watch.sh PID SECONDS [PROGRESS_FILE QUIET_SECONDS]` (#115, #516). `scripts/check.sh` sends bun's stderr (where it prints results) through a fifo to `scripts/progress-tee.sh FILE`, which forwards every line unchanged and appends one byte to FILE per `(pass)`, `(fail)`, `(skip)` or `(todo)` line and per test file header (under mawk only with `-W interactive`: mawk reads a pipe in blocks, so without it nothing counts until the input ends, #883); the watchdog stops the suite when FILE has not grown for `AHUB_CHECK_QUIET_S` (default 300, set in `.github/workflows/check.yml`, above the 120 s of the longest explicit test timeout). That is the hang detector, so a slow suite that keeps passing tests is never cut, and a test that logs without finishing is not progress. `AHUB_CHECK_HANG_S` (default in `scripts/check.sh`, hosted values for both OSes in `.github/workflows/check.yml`; #378, #593) is only the backstop for a run that keeps printing results while hung: raise it, with the job's `timeout-minutes` (room above it for setup), only when a healthy run nears it, never for a hung test, and keep the per-test 20 s timeout. The two-argument form (no file) is the old total-time bound, still used by the CI timing job and `scripts/seeded-check.ts`. A fifo and a filter of its own, never a pipe (`$!` must stay bun's pid) and never a process substitution (it cannot be waited on with a bound); `scripts/progress-tee.sh` is in the `--changed` FULL list. `test/hang-watch.test.ts` covers the watchdog, the filter and the fifo wiring.
|
|
34
|
+
- The whole-suite watchdog is `scripts/hang-watch.sh PID SECONDS [PROGRESS_FILE QUIET_SECONDS]` (#115, #516). `scripts/check.sh` sends bun's stderr (where it prints results) through a fifo to `scripts/progress-tee.sh FILE`, which forwards every line unchanged and appends one byte to FILE per `(pass)`, `(fail)`, `(skip)` or `(todo)` line and per test file header (under mawk only with `-W interactive`: mawk reads a pipe in blocks, so without it nothing counts until the input ends, #883); the watchdog stops the suite when FILE has not grown for `AHUB_CHECK_QUIET_S` (default 300, set in `.github/workflows/check.yml`, above the 120 s of the longest explicit test timeout). That is the hang detector, so a slow suite that keeps passing tests is never cut, and a test that logs without finishing is not progress. `AHUB_CHECK_HANG_S` (default in `scripts/check.sh`, 2250 s since #1086: 1.5 times the longest of two measured full runs on dgx02, 1487 s of test time; hosted values for both OSes in `.github/workflows/check.yml`; #378, #593) is only the backstop for a run that keeps printing results while hung: raise it, with the job's `timeout-minutes` (room above it for setup), only when a healthy run nears it, never for a hung test, and keep the per-test 20 s timeout. The two-argument form (no file) is the old total-time bound, still used by the CI timing job and `scripts/seeded-check.ts`. A fifo and a filter of its own, never a pipe (`$!` must stay bun's pid) and never a process substitution (it cannot be waited on with a bound); `scripts/progress-tee.sh` is in the `--changed` FULL list. `test/hang-watch.test.ts` covers the watchdog, the filter and the fifo wiring.
|
|
34
35
|
- A seed's named test must run on Linux, where the seeded job runs: a darwin-only test reports no result there and fails the job (#382). Cover a macOS-only protection with its regression test instead.
|
|
35
36
|
|
|
36
37
|
- The `closing-refs` workflow (`.github/workflows/closing-refs.yml`, #760) runs `scripts/check-closing-refs.mjs` (PR number from argv or `PR_NUMBER`) on `opened`, `edited`, `reopened` and `synchronize`: it reads `gh pr view --json closingIssuesReferences,body,title,commits` and fails when GitHub would close an issue the title and body only mention with `Refs`, a negation or a qualifier (`Refs #15. Does not close #15.` and `Refs #15. Partially fixes #15.` fail, `Refs #15.` and `Fixes #15` pass), or when a commit message has a closing keyword, negated or not, for an issue the title and body do not mean (a squash merge carries it). An issue linked only in the sidebar passes. A keyword is meant only when nothing earlier in its sentence (ending at `.`, `!`, `?`, `;` or a line break, never a comma) matches `NEGATION`; widen that list, never narrow it to let a qualified close pass (background: README). `mentioned`, `keywordReferences`, `intentionalCloses`, `closingMisuse` and `commitMisuse` are pure and covered by `test/closing-refs.test.ts` with the real `gh` shape (`repository.name`, `repository.owner.login`); its command test puts a fake `gh` first on `PATH`, never the real one. Run it by hand with `GITHUB_REPOSITORY=<owner/repo> node scripts/check-closing-refs.mjs <number>`.
|
|
@@ -39,7 +40,7 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
|
|
|
39
40
|
|
|
40
41
|
- A daemon test that writes a config file and then calls `settings_reload` passes `configWatch: false` (`DaemonOptions`, #546), unless the watcher is what it tests. The daemon's file watcher (100 ms poll, 100 ms debounce) publishes the same bytes, so whichever of the two runs second reports `applied: []`, and a watcher pass between a preview and its confirm replaces the confirmation token. Never lengthen a sleep or loosen `applied` for it; the watcher has its own tests in `test/settings-reload-daemon.test.ts`.
|
|
41
42
|
- A daemon test of a tool that reaches a public host passes the host-only transport seam (`DaemonOptions.webFetchTransport`, #553) and asserts what the daemon handed the tool, never a real round trip or a longer wait. The tool, the proxy URL and the allowlist stay the daemon's own; only the request is replaced. The proxy's own behaviour has its tests in `test/proxy.test.ts` and the web-proxy block of `test/web-fetch.test.ts`.
|
|
42
|
-
- The preload
|
|
43
|
+
- The preload leaves `HOME` unchanged and clears inherited `CLAUDE_CONFIG_DIR` and `CODEX_HOME`; tests that need fixture selectors set them after preload. A test that spawns `ahub setup`, `ahub autonomy` or `ahub init` under machine defaults, or reaches `discoverClaudeHomes` or the profile hook, sets `HOME`, `CLAUDE_CONFIG_DIR`, `AGENTHUB_HOME` and `AMEM_HOME` to temp directories, as `test/autonomy-machine-cli.test.ts` does; unit tests pass `{ home, env }` instead of `process.env`. A test that runs `ahub doctor` through the CLI also sets `CODEX_HOME` to a temp directory and puts a fake `amem` first on PATH, because doctor runs `amem setup <tool> --json` (#1025, `test/doctor-memory-capture.test.ts`).
|
|
43
44
|
- Raising the `@staix/agent-mem` exact pin moves its version literal in `test/memory.test.ts`, `test/memory-profile.test.ts`, `test/memory-profile-daemon.test.ts` and `test/doctor-model-aliases.test.ts` in the same PR; that is the only test edit a pin raise makes.
|
|
44
45
|
- Keep CI's Bun version sourced from `.bun-version` through the pinned setup-bun action; update the version file rather than adding per-job version literals.
|
|
45
46
|
- Release verification requires both OS checks and the aggregate `seeded guards` success for an identical tree, a tag matching `package.json` on `origin/main`, and fresh ancestor-bound documentation stamps. Preserve the separate npm OIDC publish job and GitHub Release write job; neither job gets the other's credential authority.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: adapters
|
|
3
|
+
type: module
|
|
4
|
+
title: Native Adapters and Spawned Agents
|
|
5
|
+
status: draft
|
|
6
|
+
parent: agent-hub-architecture
|
|
7
|
+
paths:
|
|
8
|
+
- src/adapters/**
|
|
9
|
+
- src/pi/**
|
|
10
|
+
- src/hub/peers.ts
|
|
11
|
+
- src/hub/permission-mode.ts
|
|
12
|
+
- src/hub/tool-refusal.ts
|
|
13
|
+
- src/hub/child-process.ts
|
|
14
|
+
- src/hub/lifecycle.ts
|
|
15
|
+
- src/hub/start-mode.ts
|
|
16
|
+
- src/hub/terminal-open.ts
|
|
17
|
+
- src/memory/capture.ts
|
|
18
|
+
- src/hub/native-hook-launch.ts
|
|
19
|
+
- src/hub/peer-catalog.ts
|
|
20
|
+
- src/cli/configured-acp-entry.ts
|
|
21
|
+
- src/cli/agy-mcp.ts
|
|
22
|
+
- src/cli/kimi-mcp.ts
|
|
23
|
+
- src/cli/jcode-mcp.ts
|
|
24
|
+
- scripts/benchmarks/teardown.ts
|
|
25
|
+
- src/hub/action-enforce.ts
|
|
26
|
+
- src/hub/read-only-shell.ts
|
|
27
|
+
- src/hub/read-only-shell-proof.ts
|
|
28
|
+
- src/hub/operational-authority.ts
|
|
29
|
+
- src/hub/permission-payload.ts
|
|
30
|
+
- src/hub/claude-shell-wrapper.ts
|
|
31
|
+
- src/hub/task-shell-command.ts
|
|
32
|
+
- src/hub/task-shell-default.ts
|
|
33
|
+
- src/hub/task-shell-proof.ts
|
|
34
|
+
- src/hub/task-shell-sandbox.ts
|
|
35
|
+
- src/cli/claude-shell-wrapper.ts
|
|
36
|
+
covers: [adapters]
|
|
37
|
+
tags: [agent-hub, dogfood]
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
# Native Adapters and Spawned Agents
|
|
41
|
+
|
|
42
|
+
This module records how vendor protocols meet the hub. For operational rules see the [adapter](../agent-notes/adapters.md), [daemon](../agent-notes/daemon.md), [local-worker](../agent-notes/local-worker.md), and [model](../agent-notes/models.md) notes, and the [new-adapter guide](../adding-an-ai-tool.md).
|
|
43
|
+
|
|
44
|
+
## Registry and host boundary
|
|
45
|
+
|
|
46
|
+
Each peer kind is described by an `AdapterDefinition` in the registry. Per-kind launch, protocol, and permission details stay with that definition; the daemon consumes registry fields rather than spreading vendor-name branches through policy. `PeerAdapter` is the runtime interface for delivery, turn state, permission acknowledgement, lifecycle, and optional host evidence. The host owns identity, admission, audits, task role checks, and effects; an adapter reports facts but cannot certify its own authority. This keeps a new transport from changing common policy accidentally ([#451](https://github.com/STAIxBWLB/agent-hub/issues/451), [#625](https://github.com/STAIxBWLB/agent-hub/issues/625)).
|
|
47
|
+
|
|
48
|
+
Optional ports have explicit meaning: absent keeps the generic path, while present-but-unknown refuses where the port gates a safety decision. Native evidence stays with the party that can observe it: the daemon retains host launch and socket-generation records, while a protocol adapter retains in-protocol evidence such as a thread's native sandbox or approval state. This prevents a configured adapter from asserting proof it does not possess ([#625](https://github.com/STAIxBWLB/agent-hub/issues/625)).
|
|
49
|
+
|
|
50
|
+
Adapters translate different interaction surfaces without changing the peer's task identity. Claude's plugin channel connects through MCP and the hub control service; Codex is proxied through app-server; ACP peers share an ACP transport; Pi uses its extension bridge. Kimi defaults to a pull-only TUI, with headless ACP as a separate selected form under the same peer id. The TUI does not inherit headless push, session, or recovery behavior. Keeping forms distinct lets the hub preserve the native user interaction model while enforcing one role and delivery policy ([#316](https://github.com/STAIxBWLB/agent-hub/issues/316), [#625](https://github.com/STAIxBWLB/agent-hub/issues/625)).
|
|
51
|
+
|
|
52
|
+
## Permissions, execution, and process ownership
|
|
53
|
+
|
|
54
|
+
Permission modes control whether native actions prompt; they do not widen a sandbox or path grant. The local worker's file tools use path guards, and its command tools run through the host's OS sandbox. Vendor-native tools use their vendor/runtime permission and isolation boundaries; the hub does not claim to confine every ACP or TUI tool. This distinction prevents an approval-mode setting from being mistaken for filesystem or network confinement ([#242](https://github.com/STAIxBWLB/agent-hub/issues/242), [#39](https://github.com/STAIxBWLB/agent-hub/issues/39), [#270](https://github.com/STAIxBWLB/agent-hub/issues/270)).
|
|
55
|
+
|
|
56
|
+
When the hub starts a child, the host constructs its environment, binds its launch and generation, and owns its process-group lifecycle. Stop and recovery require current evidence for the process actually started; a PID, adapter label, successful start call, or stale session record is not enough. This keeps teardown from signaling a person's unrelated process and prevents a late native callback from acquiring a replaced session's authority ([#115](https://github.com/STAIxBWLB/agent-hub/issues/115), [#625](https://github.com/STAIxBWLB/agent-hub/issues/625)).
|
|
57
|
+
|
|
58
|
+
Permission decisions are bound to the authenticated caller and current session. A hub CLI call inside an agent session acts as that peer, not as the person; role and task checks still apply at each tool boundary. Permission modes are person-controlled, and a conductor role does not grant them. This avoids turning a received message into user consent ([#193](https://github.com/STAIxBWLB/agent-hub/issues/193), [#242](https://github.com/STAIxBWLB/agent-hub/issues/242)).
|
|
59
|
+
|
|
60
|
+
## Model, account, and memory boundaries
|
|
61
|
+
|
|
62
|
+
Subscription-based native CLIs keep their own account authentication; those credentials are not pooled through the hub. Hub-owned inference, including internal summaries and local-worker model calls, uses the in-process model route and OmniRoute provider boundary. Prompt text may inform configured routing, but it cannot grant credential access or replace host routing policy. This separates native subscription identity from hub service credentials ([#553](https://github.com/STAIxBWLB/agent-hub/issues/553)).
|
|
63
|
+
|
|
64
|
+
The PII screen is distinct from ordinary optional inference. It may use an on-device model; a gateway screen requires positive on-campus admission and `onCampusOnly`. If no allowed path returns a closed `clear` or PII verdict before the deadline, the result is unknown and handled as private. This keeps network and model uncertainty from becoming a disclosure ([#198](https://github.com/STAIxBWLB/agent-hub/issues/198)).
|
|
65
|
+
|
|
66
|
+
Runtime memory capture and recall use the in-process `@staix/agent-mem` package, screened and invoked by host integration. claude-mem is off by default and import-only; there is no worker HTTP or model-summary path in the runtime. This avoids a parallel capture route with different privacy checks ([#752](https://github.com/STAIxBWLB/agent-hub/issues/752), [agent-mem#13](https://github.com/STAIxBWLB/agent-mem/issues/13)).
|
|
67
|
+
|
|
68
|
+
## Compatibility and external modules
|
|
69
|
+
|
|
70
|
+
Native CLI compatibility is centralized in `compat.ts`. Each gated operation asks that policy for a verified, in-contract, person-trusted, or refused result; person trust for one exact version does not skip a separate isolation or runtime proof. This lets upstream versions change without turning an untested protocol assumption into support ([#691](https://github.com/STAIxBWLB/agent-hub/issues/691)).
|
|
71
|
+
|
|
72
|
+
External adapter modules are an explicit, person-owned, machine-local opt-in. The daemon verifies and pins the captured bundle at startup, then admits only the bounded headless ACP instance contract through host-managed launch and current-owner checks. Module code runs in the daemon trust domain; pinning is integrity evidence, not a sandbox. Modules cannot supply native reviewer engines, proof ports, permission callbacks, provider selectors, or arbitrary recovery authority. This ceiling limits what imported code can claim and prevents live reload from replacing the executable catalog ([#754](https://github.com/STAIxBWLB/agent-hub/issues/754)).
|
|
73
|
+
|
|
74
|
+
## Evidence boundary
|
|
75
|
+
|
|
76
|
+
Compatibility probes and fake transports verify source contracts, not an account login, live model turn, native permission prompt, or operating-system isolation. Those receipts remain separate because protocol inventory alone cannot prove runtime behavior ([#625](https://github.com/STAIxBWLB/agent-hub/issues/625), [#754](https://github.com/STAIxBWLB/agent-hub/issues/754)).
|