@staix/agent-hub 0.34.1 → 0.35.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 +47 -0
- package/README.md +35 -6
- package/docs/adding-an-ai-tool.md +2 -2
- package/docs/addons/claude-mem.md +4 -4
- package/docs/addons/open-code-review.md +1 -1
- package/docs/agent-notes/README.md +6 -0
- package/docs/agent-notes/adapters.md +9 -5
- package/docs/agent-notes/budget.md +1 -1
- package/docs/agent-notes/bus.md +1 -1
- package/docs/agent-notes/daemon.md +4 -3
- package/docs/agent-notes/local-worker.md +1 -1
- package/docs/agent-notes/models.md +2 -0
- package/docs/agent-notes/tasks.md +1 -1
- package/docs/agent-notes/tests.md +5 -0
- package/docs/design/adapters.md +110 -2
- package/docs/design/agent-hub-architecture.md +63 -1
- package/docs/design/agent-hub.md +6 -0
- package/docs/design/bus.md +18 -0
- package/docs/design/execution-reference.md +146 -0
- package/docs/design/task-board.md +10 -0
- package/docs/dogfood.md +33 -12
- package/docs/events.md +38 -20
- package/docs/memory.md +21 -7
- package/docs/operations.md +539 -24
- package/docs/quickstart.md +12 -4
- package/docs/research.md +71 -13
- package/docs/roles.md +1 -1
- package/docs/sdlc.md +9 -7
- package/docs/security.md +87 -11
- package/docs/smoke.md +14 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +8 -3
- package/docs/verification/2026-11-07-compat-python-startup-1248.md +51 -0
- package/docs/verification/2026-11-07-compat-receipt-harness-1248.md +105 -0
- package/docs/verification/2026-11-07-compat-receipt-producers-1248.md +96 -0
- package/docs/verification/2026-11-07-compat-workflow-1248.md +94 -0
- package/docs/verification/2026-11-07-daemon-startup-1232.md +169 -0
- package/docs/verification/2026-11-07-jcode-receipt-reconstruction-1248.md +118 -0
- package/docs/verification/2026-11-07-kimi-delayed-usage.md +34 -0
- package/docs/verification/2026-11-07-kimi-review-watch-1284.md +115 -0
- package/docs/verification/2026-11-08-agy-1.5.3.md +96 -0
- package/docs/verification/2026-11-08-agy-acp-t0.md +56 -0
- package/docs/verification/2026-11-08-hub-visual-1058.md +161 -0
- package/docs/verification/2026-11-08-jcode-0991-model-setter-1317.md +143 -0
- package/docs/verification/2026-11-08-kimi-252-model-setter-1317.md +137 -0
- package/docs/verification/2026-11-08-kimi-tui-completion-1282.md +45 -0
- package/docs/verification/2026-11-08-kimi-wire-yield-1283.md +31 -0
- package/docs/verification/2026-11-08-live-model-override-1317.md +250 -0
- package/docs/verification/2026-11-08-qwen-0269-acp.md +138 -0
- package/docs/verification/2026-11-08-qwen-0269-model-setter-1317.md +91 -0
- package/docs/verification/2026-11-09-0.35.0.md +129 -0
- package/docs/verification/2026-11-09-headless-records-1320.md +157 -0
- package/docs/verification/claude-permission-transport-native.py +10 -3
- package/docs/verification/claude-shell-wrapper-native.py +8 -8
- package/docs/verified.json +245 -15
- package/package.json +3 -3
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/kimi-hook.js +20397 -0
- package/plugins/agent-hub/kimi-review-policy-worker.js +217 -0
- package/plugins/agent-hub/server.js +2701 -664
- package/scripts/check-docs.mjs +12 -1
- package/src/adapters/acp-model-policy.ts +56 -0
- package/src/adapters/acp.ts +175 -5
- package/src/adapters/agy/entry.ts +8 -5
- package/src/adapters/agy/hooks.ts +65 -15
- package/src/adapters/channel-peer.ts +33 -1
- package/src/adapters/claude-implementer.ts +16 -3
- package/src/adapters/codex-appserver.ts +118 -8
- package/src/adapters/codex-review-isolation.ts +3 -1
- package/src/adapters/compat.ts +11 -6
- package/src/adapters/definitions/builtin-metadata.ts +8 -1
- package/src/adapters/definitions/native-cli-entry-kimi.ts +76 -7
- package/src/adapters/definitions/native-cli-preview-common.ts +2 -2
- package/src/adapters/definitions/native-cli-preview-kimi.ts +9 -2
- package/src/adapters/definitions/reviewer-variants.ts +3 -1
- package/src/adapters/kimi-review-policy-worker.js +2 -0
- package/src/adapters/kimi-review-policy-worker.ts +85 -0
- package/src/adapters/kimi-review-policy.ts +68 -0
- package/src/adapters/kimi-review.ts +41 -41
- package/src/adapters/kimi-tui-facts.ts +153 -0
- package/src/adapters/kimi-wire-usage.ts +33 -8
- package/src/adapters/local-worker.ts +59 -20
- package/src/adapters/model-token.ts +3 -0
- package/src/adapters/pi.ts +27 -5
- package/src/adapters/registry-types.ts +34 -3
- package/src/adapters/registry.ts +1 -0
- package/src/adapters/reviewer-policy-watch.ts +86 -0
- package/src/adapters/reviewer-print.ts +80 -18
- package/src/cli/agents.ts +1 -1
- package/src/cli/facts-hook.ts +4 -2
- package/src/cli/help.ts +3 -2
- package/src/cli/identity.ts +3 -0
- package/src/cli/kimi-facts-hook.ts +11 -0
- package/src/cli/kimi-plugin.ts +157 -0
- package/src/cli/main.ts +48 -9
- package/src/cli/model.ts +52 -0
- package/src/cli/output.ts +23 -1
- package/src/cli/reset.ts +1 -1
- package/src/cli/status-lines.ts +8 -4
- package/src/hub/budget.ts +1 -1
- package/src/hub/bus.ts +40 -2
- package/src/hub/code-facts.ts +10 -3
- package/src/hub/completions.ts +3 -1
- package/src/hub/config-apply.ts +2 -1
- package/src/hub/control-client.ts +1 -1
- package/src/hub/daemon.ts +1050 -106
- package/src/hub/deliverable-files.ts +38 -5
- package/src/hub/dogfood-profile.ts +1 -1
- package/src/hub/envelope.ts +1 -1
- package/src/hub/events.ts +28 -18
- package/src/hub/execution-lineage.ts +120 -0
- package/src/hub/execution-reference.ts +36 -0
- package/src/hub/hub-tools.ts +21 -2
- package/src/hub/instances.ts +4 -3
- package/src/hub/kimi-events.ts +46 -0
- package/src/hub/kimi-usage.ts +132 -29
- package/src/hub/lifecycle.ts +31 -15
- package/src/hub/limits.ts +13 -4
- package/src/hub/manager.ts +15 -2
- package/src/hub/memory-policy.ts +2 -1
- package/src/hub/memory-sync-attestation.ts +54 -0
- package/src/hub/memory-sync-state.ts +20 -5
- package/src/hub/model-overrides.ts +115 -0
- package/src/hub/native-hook-launch.ts +12 -3
- package/src/hub/operational-view.ts +2 -2
- package/src/hub/peers.ts +27 -0
- package/src/hub/permission-mode.ts +1 -1
- package/src/hub/person-figures.ts +88 -0
- package/src/hub/report.ts +38 -4
- package/src/hub/research-dataset.ts +158 -0
- package/src/hub/research-memory.ts +68 -0
- package/src/hub/research-quality.ts +118 -0
- package/src/hub/research-record.ts +279 -0
- package/src/hub/research.ts +126 -27
- package/src/hub/restart.ts +14 -0
- package/src/hub/settings.ts +3 -0
- package/src/hub/startup-event-seeds.ts +51 -0
- package/src/hub/startup-readiness.ts +65 -0
- package/src/hub/startup-timing.ts +38 -0
- package/src/hub/tasks.ts +29 -9
- package/src/hub/ui.ts +9 -5
- package/src/hub/visual-assets.ts +50 -0
- package/src/hub/visual.ts +257 -0
- package/src/memory/brief.ts +169 -14
- package/src/memory/capture.ts +2 -1
- package/src/memory/recall.ts +46 -5
- package/src/memory/record-provenance.ts +82 -0
- package/src/memory/records.ts +116 -0
- package/src/models/route/adec.ts +190 -0
- package/src/models/route/config.ts +37 -3
- package/src/models/route/decision-evidence.ts +83 -0
- package/src/models/route/decision.ts +143 -0
- package/src/models/route/runtime.ts +13 -6
- package/src/ui/index.html +134 -6
- package/src/ui/plugin.ts +65 -4
- package/templates/AGENTS.block.md +1 -1
- package/templates/config.json +1 -1
- package/templates/profiles/general.json +1 -1
- package/templates/profiles/research.json +1 -1
- package/templates/profiles/writing.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,53 @@ 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.35.0
|
|
8
|
+
|
|
9
|
+
- Managed headless peers can author compact amem Record capture rows from final answers (#1320), following the package records policy and existing privacy fences. Record lines are removed from shared replies; bounded memory preference uses proven generated-note IDs and anchors without deleting unrelated notes or promoting capture into curated evidence.
|
|
10
|
+
- Hub memory sync supplies amem's public placement attestation per pass (#1173). Typed writer refusals retain their closed category/cause and next step in status and the dashboard, including when the transport returns a generic or quiet result. Placement activation remains a person-run action.
|
|
11
|
+
- Exact-pin the first-party agent-mem core to 0.23.1 (refs #1320; PR #1335 raised it to 0.23.0, PR #1339 to 0.23.1). An agent-mem 0.22.1 drain silently discards Record format-3 spool entries and refuses the `records` config key, so upgrade every hub and amem that shares a memory home together.
|
|
12
|
+
|
|
13
|
+
- adec shadow route metadata accepts an optional pinned `decider.version` (#554, agent-decide#6), while preserving unknown when absent and refusing moving `latest` aliases.
|
|
14
|
+
- Optional hub `decision` routes consult a loopback agent-decide policy service (#554), default off and shadow-first. Shadow preserves the existing stage model/effort; proposed alias/effort appears only in route events. Enforcement is explicitly unavailable until the #554 follow-up. Evidence and events are bounded and privacy-screened, sensitive turns are campus-gated, and ordinary routes and explicit model selections are unchanged.
|
|
15
|
+
- Person-only next-turn model overrides (#1317, PRs #1321, #1323): `ahub model set <peer> <model>`, `ahub model clear <peer>` and `ahub model list [--json]`, or a preview and confirm in dashboard settings, store a runtime override in hub.db that the Claude implementer, Codex app-server and reviewer, local, Pi and headless Kimi Code 2.5.2 ACP peers apply at their next turn, with `model_change` audit events and effective/source/pending status. An explicit Codex client model still wins; raw and pull-only TUIs and other unsupported peers (Qwen and Jcode ACP included) refuse a live set with restart guidance. Native provider-backed model acceptance and real dashboard interaction are unverified.
|
|
16
|
+
|
|
17
|
+
- Peers can post bounded visuals (Mermaid diagrams, comparisons, tables, before/after views) with `hub_visual` and read one back with `hub_visual_show` (refs #1058, PR #1315). Delivery is console-only by default and other peers get only an id stub; private and PII contexts and remote recipients refuse. acon renders them (WebP needs acon 0.1.7 or later) and the standalone UI shows a text or source fallback; standalone browser and native all-peer journeys are unverified.
|
|
18
|
+
- Kimi TUI turns complete on the native final transcript row instead of the Stop hook (#1282, PR #1309): completion waits until native background work is quiet, a lost launcher closes the run once as interrupted, and the launch-scoped plugin is installed and cleaned under fail-closed ownership checks. Any bridge setup failure falls back to the plain TUI. Live receipts passed on Kimi 2.5.2.
|
|
19
|
+
- Kimi wire usage that arrives after `turn_end` is drained (#1281, PR #1291): one bounded delayed drain per peer (250 ms, 1 s, 2 s) attributes late rows to their captured turn, or `unattributed` when ambiguous, and updates usage, tokens and the soft window exactly once without changing earlier `turn_end` totals. Eight ordinary Kimi 2.5.2 turns produced no delayed row, so the delayed path is covered by fixtures only.
|
|
20
|
+
- Scheduled Kimi wire-log drains read one bounded slice per callback and yield between slices (#1283, PR #1313), so a large backlog no longer holds the daemon loop. In the benchmark the longest synchronous slice fell from 34 ms to 3.3 ms for the same 2 MB.
|
|
21
|
+
- Fresh Kimi reviews run their periodic isolation check in one owned worker off the daemon loop (#1284, PR #1296). The full synchronous fence still runs before spawn and at every review token, task-MCP and verdict boundary, and a worker result never grants authority. No production latency claim is made.
|
|
22
|
+
- Antigravity (agy) fixes (#1280, PRs #1286, #1294, #1295): hook transcript reads are skipped when no fact offer is pending, a terminal non-normal Stop no longer leaves agy busy, and auto-update is suppressed at launch. Write and Edit calls are credited to agy's facts, 1.5.x `SYSTEM_MESSAGE` readback works, `GEMINI.md` is protected as an instruction file, transcript token counts are recorded as usage, and SessionStart and PreInvocation tell agy how many messages wait (counts only). `agy acp` has only a model-free 1.5.3 probe; ACP peer wiring and a Stop continuation on important mail are not implemented.
|
|
23
|
+
- Detailed status names the current owner of an attached channel peer (#1310, PR #1311): `ahub status` prints `<peer> owner: pid N; attached <age>; native session <id|unverified>`, and `ahub status --json` carries it as the optional `ownerAttachment`. It is withheld from `status.json` and every non-console reader. No real Kimi or Claude plugin attach was observed.
|
|
24
|
+
- Startup reads `events.jsonl` once for its three seed consumers and logs one numeric `startup timings ms:` line (refs #1232, PR #1285). After 12 seconds `ahub up` reports that the hub is still starting, with its pid, and keeps waiting; at the 120-second bound it exits unsuccessfully and leaves the process running. The historical 11 to 16 s startup was not reproduced, so no general speedup is claimed.
|
|
25
|
+
- The managed AGENTS block and the three profile blocks tell peers to keep their turn open until every check they started has exited and its result is handled (#1333, PR #1334); filing `hub_task_done` without waiting for hosted CI is unchanged. Other projects get it after upgrading and running `ahub init` again.
|
|
26
|
+
- Bounded execution reference and lineage contracts (#1220 T1, PR #1336): `ExecutionReferenceV1`, neutral JSON conformance vectors and a pure lineage reader whose pages hold at most 100 nodes, 200 edges and 256 KiB, with HMAC-bound continuations; an unauthorized absent or denied reference reads `unavailable`. Contracts only: admission, runtime and console surfaces (T2, T3) are not in this release.
|
|
27
|
+
|
|
28
|
+
- Research record v2 and dataset export (#1277, PRs #1290, #1312): every task in a terminal board state, cancelled included, gets a record with review, delivery, merge, specs, memory, person, stall and handoff blocks, `null` where a value cannot be derived; v1 rows still load. `ahub research export --dataset <dir>` writes per-project `tasks.jsonl`, `manifest.json` and `quality.json` (per-field unknown counts, quota gaps, budget hand-offs, token attribution share), family names only, to a directory in the home outside every project. No live hub ran with research enabled.
|
|
29
|
+
- Research memory and person figures (#1279, #1278, PRs #1297, #1298): per-task rendered, delivered, cited and fail-note counts and a retry signal come from agent-mem's own sidecar counters, with new `mem:<id>` citation sites in review notes, `hub_remember` and spec acknowledgements. `permission`, `question`, `completion` and `agent_cli` events carry task attribution, and the research `person` block and `ahub report --by task` share one count. Permission and question flows were checked against fakes only.
|
|
30
|
+
|
|
31
|
+
- Optional Orca companion plugin in `integrations/orca/`, not part of the npm package (#1198, PRs #1299, #1300, #1301, #1302): it binds an exact workspace on this Mac, opens the dashboard, `ahub console` or `acon ui`, and shows a read-only status projection on click. It never focuses or reads an existing terminal and refuses remote and Windows workspaces. Its support matrix lists no combination as supported: every native receipt is a person-run leg not yet run.
|
|
32
|
+
- Optional Herdr mod in `integrations/herdr/`, not part of the npm package (#1199, PRs #1303, #1304, #1305, #1306): it lists the focused machine's registered projects, opens `ahub console` in a new tab or the local dashboard, shows a click-only status card, and focuses or peeks an existing local console only when exactly one live terminal matches on two reads. It is one action more than the manual baseline when the pane is already in the right project. No combination is supported yet, remote is fixture-only, and all native receipts are person-run legs not yet run.
|
|
33
|
+
|
|
34
|
+
- Compat receipt automation (#1248, PRs #1256, #1264, #1272, #1275, #1288): `scripts/compat-receipt.ts` runs model-free receipts only on a disposable runner (a personal Mac is refused) with exact acquisition, integrity binding and bounded execution. The Kimi and Qwen producers are promoted, a Jcode producer is reconstructed (its 0.99.1 Linux control is partial, two legs fail), and the drift watcher can run receipts on hosted runners and propose verification PRs that are never auto-merged. A receipt is a compatibility signal, not a security attestation; PR publication was exercised against fake REST only, and these PRs change no COMPAT, trust or verified version.
|
|
35
|
+
- Exact agy 1.5.3 (PR #1314) and Qwen ACP 0.26.9 (PR #1322) join the verified versions (refs #843), from isolated model-free native macOS arm64 receipts. agy 1.5.2 and 1.5.4 stay refused and qwen-dual stays 0.26.1. Account, model, TUI and native permission behavior remain unqualified, as do agy ACP and Qwen on Linux and Intel Macs.
|
|
36
|
+
|
|
37
|
+
- CI defers the four macOS shards of a stacked pull request, one whose base is another PR's branch rather than `main` (#1307). GitHub runs at most 5 macOS jobs at once on this account, and a stacked PR is rebased and rerun once it targets `main`, so its earlier macOS run never served as the merge gate. The required `check (macos-latest)` check fails for such a PR with the reason and the fix, so a skipped required check can never pass. A PR retargeted to `main` without a new commit gets its macOS run by a push or by closing and reopening it. Ubuntu, seeded guards, `main` pushes and identical-tree reuse are unchanged.
|
|
38
|
+
- CI installs npm 11.20.0 before each gate shard, the version the release workflow publishes with, and `scripts/check-package.mjs` refuses any other `npm pack --json` shape with the cause and `npm --version` instead of a TypeError, so a local check under npm 12 stops there (#1257, PR #1258). A weekly workflow keeps the "CI flake ledger (check.yml)" issue of shard jobs that failed and then passed on the same head, and `scripts/refresh-test-timings.ts --check` reports shard weights (PR #1259). The macOS typecheck step gets a 5120 MiB heap ceiling (#1318, PR #1319).
|
|
39
|
+
- The budget gate defaults to 0.99 instead of 0.9 (#1292). A peer now checkpoints and pauses only when a fresh quota window reaches 99%; it resumes below 89%, and a release cut is held only below 1% headroom. A provider's hard refusal still pauses at once. `ahub init` no longer writes `"gate": 0.9` into a new project's tracked config, so new projects follow the default. Existing projects keep the 0.9 their tracked `config.json` pins until the key is removed or a machine-local value is set. `budget.gate` is now a registered setting: `ahub settings set budget.gate <0..1>` from a terminal or an `ahub ui --settings` session writes `config.local.json` and applies from the next quota reading without a restart; an ordinary dashboard session and the conductor cannot set it.
|
|
40
|
+
- Documentation is kept current (#1268): the quickstart's GitHub install tag, the Bun floor and the agent-mem pin are generated from `package.json` and `.bun-version` (`bun scripts/readme-facts.ts`, part of `scripts/check.sh`); `docs/verified.json` now covers every page under `docs/` and `docs/addons/` except the smoke ledger and the retired GSD analysis, and CI warns about stale stamps on every pull request; stale statements about claude-mem, research campaigns, design specs, events, roles and the SDLC readbacks are corrected; a release notifies the family site when `SITE_DISPATCH_TOKEN` is set.
|
|
41
|
+
- `release-prepare` renders the generated doc facts blocks (the quickstart install tag and the version pins) after the version bump, and `--dry-run` lists the blocks it would change, so a release PR passes `scripts/readme-facts.ts` with no manual `--write` (#1273). CONTRIBUTING explains that the stale-documentation warning on an ordinary pull request is expected and cleared by the release PR's restamp (#1274).
|
|
42
|
+
- `scripts/spec-dogfood-report.ts` reports how often PRs that touch a dogfood spec's paths also change the owning spec (#1064 T6, PR #1270). The first five dogfood days read 41% (21 of 51), below the 54% monolith baseline and the 70% target.
|
|
43
|
+
- Test-only flake fixes, no product change: the replaced process environment is restored when a staged test times out (#1262, PR #1267), two fake-ripwire fixtures record CodeFacts step times on failure (#1261, PR #1265), trace reads wait for a complete record (#1263, PR #1269), the empty rooms rotation fixture awaits live roster adoption (#1287, PR #1289), UI inbox refresh races are gated without sleeps (#1324, PR #1325), and the Herdr AC9 test no longer depends on the host (PR #1316).
|
|
44
|
+
|
|
45
|
+
- 0.35.0 N-1: published 0.34.1, the latest 0.34.x patch, pins agent-mem 0.22.1; this hub exact-pins agent-mem 0.23.1. Control remains protocol 17 (descriptor floor 16). The release N-1 fixture and matrix now pin published 0.34.1/core 0.22.1; the separate protocol-16 fixture stays 0.25.0.
|
|
46
|
+
- Wire and protocol: the control protocol window, room headers, codecs and RoomStore are unchanged; nothing under `src/rooms` or `src/broker` changed. The daemon adds two feature labels, `model_overrides` and `visuals`, and a console-only `model` control message; the dashboard's `model_preview`/`model_confirm` actions are served by the same daemon. A 0.35.0 `ahub model` checks the label and refuses against a 0.34.1 hub with an upgrade-and-restart step; a 0.34.1 CLI never sends it. Envelopes gain an optional local `refs.visual`; visuals refuse remote recipients, so nothing new crosses a room.
|
|
47
|
+
- MCP tools: two tools are added, `hub_visual` and the read-only `hub_visual_show`; no existing tool schema changed. A 0.34.1 plugin on a 0.35.0 hub simply lacks them. The current plugin lists them without a feature check, so against a 0.34.1 hub a call is refused as an unknown task operation (source reading, not a run). The execution reference and lineage contracts (#1336) add no tool, control message, table or command. Model overrides (#1321) are a person-only CLI and dashboard surface, not an MCP tool.
|
|
48
|
+
- Files and format floors: hub.db remains schema 2 and RoomStore remains schema 5. Two additive hub.db tables, `model_overrides` and `visuals` (with two indexes), are created on first use without raising the format; a 0.34.1 daemon opens the database and does not read them. Restart snapshots gain optional `modelOverrides` and `visuals` integrity digests. The machine's research store now writes `agent-hub.research/v2` records and still reads v1; a 0.34.1 reader skips v2 rows, so its `ahub research` and export omit records 0.35.0 wrote.
|
|
49
|
+
- Rollback: controlled recovery to a target without these labels, which is the `ahub upgrade --to 0.34.1` path, refuses before effects while model override rows exist (clear them with `ahub model clear <peer>`), while stored visual records exist (there is no clear command), or while any config file has an explicit `visuals` block, including one written by `ahub settings set visuals.retention_days` (remove it). A Kimi 2.5.2 TUI launched by 0.35.0 installs a launch-scoped plugin under `<kimi home>/plugins/managed/agent-hub` and removes it on exit; a 0.34.1 launcher neither installs nor removes it, so a copy left by a crash stays until a later 0.35.0 launch replaces or cleans it (source reading, not a run).
|
|
50
|
+
- Shared memory home: Records are on by default in agent-mem 0.23.x. Upgrade every amem CLI, native hook and ahub that shares a memory home together. 0.23.1 keeps `INDEX_VERSION` 3, the capture-row schema and the hook spool prefixes of 0.22.1, but adds the `Record` capture event, and this hub's managed headless peers capture Records unless amem `records: false` applies. agent-mem's own rollout note says a 0.22.1 hook drain silently discards a Record format-3 spool entry without a counted loss, and the 0.22.1 config parser refuses the `records` key even when it is `false`. A 0.34.1 hub embeds 0.22.1, so it is one of those consumers: setting `records: false` while a 0.34.1 hub or a 0.22.1 amem still uses the home breaks that consumer's configuration. agent-mem's alternative, a person-appended `home` reader floor at 0.23.0, makes 0.22.1 consumers, the 0.34.1 hub included, refuse the home instead. This hub supplies placement attestation on each sync pass (#1173); a 0.34.1 hub on an active-placement home still declines sync as before. Activation, floors and writer renewal stay person-run; this preparation performs none of them.
|
|
51
|
+
- CLI: adds `ahub model set <peer> <model> | clear <peer> | list [--json]` and `ahub research export --dataset <dir>`, and `ahub up` prints a still-starting notice while it waits for readiness. A 0.34.1 CLI keeps its own behavior against a 0.35.0 hub: no new flag, command or JSON field is required, and the two new feature labels are additive.
|
|
52
|
+
- Evidence ceiling: the N-1 fixture uses the unmodified published 0.34.1 package (core 0.22.1) for the control/plugin round trip in both directions and the isolated broker/member tests. It does not call `hub_visual`, `ahub model` or Record capture, does not open a hub.db or memory home written by 0.35.0 with 0.34.1, and does not run a rollback; those statements come from source reading. These claims do not establish real-account inference, a live Kimi TUI launch, a real memory home or two-machine operation; the release verification report records execution results separately.
|
|
53
|
+
|
|
7
54
|
## 0.34.1
|
|
8
55
|
|
|
9
56
|
- `ahub upgrade` works with npm 12 again (#1253, PR #1254, task 766). npm 12 wraps `npm view --json` output for one package in a one-element array. The default target ("unexpected latest release") and `--to` ("unexpected release identity") both refused it. Both now share one parser that accepts the npm 11 value and a one-element array of it, and still refuses any other shape, with a message naming the `npm view` command to check. Until this release, an npm 12 machine can upgrade by putting npm 11 first on PATH for the run.
|
package/README.md
CHANGED
|
@@ -5,12 +5,12 @@ 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.35.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
|
|
12
12
|
|
|
13
|
-
You need [Bun](https://bun.sh) 1.4.2 or later on PATH.
|
|
13
|
+
You need [Bun](https://bun.sh) <!-- facts:bun-floor -->1.4.2<!-- /facts:bun-floor --> or later on PATH.
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
bun add -g @staix/agent-hub && ahub setup
|
|
@@ -28,6 +28,11 @@ ahub say "Each of you: reply with your name."
|
|
|
28
28
|
ahub ui # the web dashboard
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
If a verified daemon child is still starting after 12 seconds, `ahub up` reports
|
|
32
|
+
its PID and keeps waiting for readiness. Completed starts write phase timings to
|
|
33
|
+
`hub.log`; [startup diagnostics](docs/operations.md#install-and-start) explains
|
|
34
|
+
the 120-second outer bound and the next step for unconfirmed readiness.
|
|
35
|
+
|
|
31
36
|
`ahub setup` also registers agent-hub as a plugin of the family console
|
|
32
37
|
([agent-console](https://github.com/STAIxBWLB/agent-console), `acon ui`): the same dashboard
|
|
33
38
|
appears as a panel for every registered project, and pending approvals, reviews assigned
|
|
@@ -47,9 +52,21 @@ defaults. The
|
|
|
47
52
|
|
|
48
53
|
## What you get
|
|
49
54
|
|
|
55
|
+
- **Bounded visual explanations.** Ordinary local peers can post a diagram,
|
|
56
|
+
comparison, table or before/after view with `hub_visual`. acon renders the
|
|
57
|
+
screened record; the standalone dashboard shows text/source, and peers receive
|
|
58
|
+
only a stub. [Limits and privacy](docs/operations.md#bounded-visual-attachments-1058).
|
|
50
59
|
- **Peers, not a pipeline.** Any agent can message the others. Messages are batched
|
|
51
60
|
into digests, priorities decide what interrupts a turn, and delivery survives
|
|
52
61
|
restarts.
|
|
62
|
+
- **Optional decision routing.** Hub-owned routes can consult a loopback adec
|
|
63
|
+
policy in shadow mode while keeping the existing model choice unchanged.
|
|
64
|
+
See [configuration and privacy limits](docs/operations.md#optional-adec-decision-route-554).
|
|
65
|
+
- **Live model selection.** A person can set a supported peer's next-turn model
|
|
66
|
+
with `ahub model set`, without restart or reload. Headless Kimi 2.5.2 has an
|
|
67
|
+
exact-version ACP setter receipt; other ACP kinds remain unsupported.
|
|
68
|
+
Active turns stay unchanged;
|
|
69
|
+
[status distinguishes pending and unsupported selection](docs/operations.md#live-peer-model-overrides-1317).
|
|
53
70
|
- **A shared task board.** Work is proposed, accepted, done and reviewed through hub
|
|
54
71
|
tools. Roles split planning, implementation and review (up to eight planners,
|
|
55
72
|
each task bound to one), `routing.toml` picks
|
|
@@ -65,22 +82,31 @@ defaults. The
|
|
|
65
82
|
another project running agent-hub on this machine goes onto that project's board
|
|
66
83
|
with `ahub task handoff` (agents: `hub_task_handoff`), and its state changes come
|
|
67
84
|
back as task messages.
|
|
85
|
+
Fresh Kimi reviews run periodic isolation checks off the hub event loop;
|
|
86
|
+
verdict and task-tool authority still require the complete synchronous fence
|
|
87
|
+
([measurements and limits](docs/verification/2026-11-07-kimi-review-watch-1284.md)).
|
|
68
88
|
- **Research campaigns.** `ahub study` keeps a multi-unit study contract as a draft
|
|
69
89
|
(a planner drafts it with `hub_study_draft`); only the person approves it, which
|
|
70
90
|
creates one task per unit, and `ahub study show` reads coverage per unit and
|
|
71
91
|
source. A unit is covered by its owner's `hub_study_handoff` with evidence, not by
|
|
72
92
|
a done task. Experiment attempts are recorded with `hub_experiment_record` and read
|
|
73
|
-
with `ahub study experiments`, each value naming where it came from.
|
|
93
|
+
with `ahub study experiments`, each value naming where it came from. Covered by
|
|
94
|
+
source and fixture tests only so far: no real campaign has run on this repository
|
|
95
|
+
([dogfood](docs/dogfood.md#operations)).
|
|
74
96
|
- **Design specs.** Markdown specs with `id` and `type` frontmatter name the paths
|
|
75
97
|
they govern. `ahub specs check` validates their links and reports specs that are
|
|
76
98
|
stale against their covered sources, the hub names the specs covering a task, and
|
|
77
99
|
agents read them with `hub_specs`. Only a person stamps a spec as reviewed
|
|
78
|
-
(`ahub specs stamp`).
|
|
100
|
+
(`ahub specs stamp`). Fixture-tested only so far: this repository's own specs in
|
|
101
|
+
`docs/design/` are unstamped drafts ([dogfood](docs/dogfood.md#operations)).
|
|
79
102
|
- **Shared memory.** Decisions, contracts, fails and findings are kept through the
|
|
80
103
|
first-party [`@staix/agent-mem`](https://github.com/STAIxBWLB/agent-mem) library,
|
|
81
104
|
in process with no claude-mem worker (short- and long-term tiers, a typed graph,
|
|
82
105
|
git sync between machines). Agents get
|
|
83
106
|
it as a recall preface, as briefs on a task, and through bounded search tools.
|
|
107
|
+
Managed headless peers can save compact `amem:` records from final answers; the
|
|
108
|
+
hub follows amem's `records` policy and removes record lines from shared replies
|
|
109
|
+
([details](docs/memory.md#agent-authored-records-1320)).
|
|
84
110
|
- **Local and on-prem models.** Pi and the local worker can run on DGX or local MLX
|
|
85
111
|
through an authenticated relay. Tasks marked as PII stay on the on-prem worker.
|
|
86
112
|
- **Guard rails.** The local worker and Pi run in a sandbox, and their writes and
|
|
@@ -96,7 +122,8 @@ defaults. The
|
|
|
96
122
|
the project's own peer appears as `<peer>~<origin>` with none of that peer's
|
|
97
123
|
roles: sessions are distinguishable, which is provenance, not authentication.
|
|
98
124
|
Budgets pause an
|
|
99
|
-
agent before it runs out of quota
|
|
125
|
+
agent before it runs out of quota (at 99% of a window by default, the
|
|
126
|
+
`budget.gate` setting), and recovery never replays an
|
|
100
127
|
effect whose outcome is unknown.
|
|
101
128
|
- **Operations.** A terminal console and a local dashboard, one hub per repository or
|
|
102
129
|
worktree with a machine-wide view, live settings reload, machine-local schedules
|
|
@@ -131,6 +158,8 @@ machine, and the hub tells the planner when an installed version is not verified
|
|
|
131
158
|
Before you migrate a memory home with `amem`, read
|
|
132
159
|
[memory profiles and rollout](docs/memory.md#selected-memory-profile). Every hub that
|
|
133
160
|
uses the home must run a version that understands the new layout.
|
|
161
|
+
Machine sync uses amem's public writer attestation; status and the dashboard
|
|
162
|
+
show an unverified writer's cause and the person-run next step.
|
|
134
163
|
|
|
135
164
|
## Where it is going
|
|
136
165
|
|
|
@@ -143,7 +172,7 @@ uses the home must run a version that understands the new layout.
|
|
|
143
172
|
[#258](https://github.com/STAIxBWLB/agent-hub/issues/258)).
|
|
144
173
|
|
|
145
174
|
Live checks that still need a real account or a person are listed in the
|
|
146
|
-
[release verification report](docs/verification/2026-11-
|
|
175
|
+
[release verification report](docs/verification/2026-11-09-0.35.0.md) and the
|
|
147
176
|
[smoke checklist](docs/smoke.md).
|
|
148
177
|
|
|
149
178
|
## Documentation
|
|
@@ -42,7 +42,7 @@ agent-hub supports five transport architectures (`transport` in `AdapterDefiniti
|
|
|
42
42
|
An adapter exports an object conforming to `AdapterDefinition` in `src/adapters/registry-types.ts`. Read that type as the source of truth: most blocks are declared inline there, so there are no separate `Adapter*Definition` types to import, and some members are required. Its top-level fields are:
|
|
43
43
|
|
|
44
44
|
- Required: `api` (always `1`), `kind`, `displayName`, `family`, `transport`, `roles`, `trust` (`onPrem`, `hubSandboxed`), `start`, `identity` (`mcp`), `permission`, `session`, `turnEnd`, `cli`, `display`.
|
|
45
|
-
- Optional: `optional`, `defaultOnRoster`, `enabledSwitchStartOnly`, `instances`, `attach`, `reporting`, `routing`, `budget`, `configuration`, `capabilities`, `hooks`, `reviewerVariant`, `reviewer`.
|
|
45
|
+
- Optional: `optional`, `defaultOnRoster`, `enabledSwitchStartOnly`, `instances`, `attach`, `reporting`, `routing`, `budget`, `configuration`, `capabilities`, `hooks`, `reviewerVariant`, `reviewer`, `title`, `titlePersistence`, `questionWaitMs`, `settle`, `modelOverride` (the person's next-turn model override boundary, fixed builtins only), `headlessInstances`, `headlessVariant`, `projection`.
|
|
46
46
|
- Registry load runs `assertDefinitionCoherence` (`src/adapters/registry.ts`) on every definition and reviewer variant, so an incoherent combination fails at load, not at run time.
|
|
47
47
|
|
|
48
48
|
### Key Sub-Objects
|
|
@@ -88,7 +88,7 @@ The `ahub <kind>` surface: `command`, `startWords(form)`, and for a fixed builti
|
|
|
88
88
|
`instances.mode` is `headless` (needs a headless `start.create` factory) or `external` (a channel instance started from the person's terminal). A kind with no `instances` policy (Jcode) takes no extra instance, and `validateInstances` refuses a declaration of one. `attach` holds the pull-only forms, external-hello admission and the refusal texts for a TUI that attaches without a driven peer.
|
|
89
89
|
|
|
90
90
|
#### Peer capability members
|
|
91
|
-
The runtime peer (`PeerAdapter`, `src/hub/peers.ts`) exposes optional members that the daemon reads instead of checking its class (#625 phase 6): `sessionGeneration` (native session identity: absent keeps the owner-token fallback, present but undefined is unknown and refuses bounded checks), `checkpointUsesSessionGeneration` (opt out of generation-fenced checkpoints, as Pi does), `permissionModeState` (the effective native approval state, or `unknown` / `unmanaged`) and `setPermissionMode(mode)` (resolves only after the native side acknowledges). Native safety proofs (Codex proxy and restoration, Claude launch generation) stay in the daemon.
|
|
91
|
+
The runtime peer (`PeerAdapter`, `src/hub/peers.ts`) exposes optional members that the daemon reads instead of checking its class (#625 phase 6): `sessionGeneration` (native session identity: absent keeps the owner-token fallback, present but undefined is unknown and refuses bounded checks), `checkpointUsesSessionGeneration` (opt out of generation-fenced checkpoints, as Pi does), `permissionModeState` (the effective native approval state, or `unknown` / `unmanaged`) and `setPermissionMode(mode)` (resolves only after the native side acknowledges). A driven peer publishes its turn's final answer through `BasePeer.finalAnswer` rather than `onMessage`, so the bus strips `amem:` Record lines before sharing it (#1320); the Record instruction reaches only the owners the daemon binds (see [memory](memory.md#agent-authored-records-1320)). Native safety proofs (Codex proxy and restoration, Claude launch generation) stay in the daemon.
|
|
92
92
|
|
|
93
93
|
#### `budget`
|
|
94
94
|
Defines quota and context window consumption readers:
|
|
@@ -4,9 +4,9 @@ Status 2026-10-12: keep optional. No measured effect yet.
|
|
|
4
4
|
|
|
5
5
|
## What it does to collaboration
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
7
|
+
- Through the hub: nothing at runtime. Session-start recall, task briefs, the local worker's capture and `hub_remember` all use the in-process amem store (`@staix/agent-mem`); the hub makes no claude-mem worker request and ignores `memory.worker_url` (`ahub doctor`: "in-process, no claude-mem worker").
|
|
8
|
+
- Native plugin: claude-mem installed as a Claude Code or Codex plugin runs its own hooks inside those sessions; the hub neither reads nor writes them. The add-on fingerprint records it when it is enabled there, so `ahub research addons` and `ahub bench run --addon claude-mem=on|off` can compare on against off.
|
|
9
|
+
- Import: `ahub memory import claude-mem` is a person's one-way, read-only copy of past hub observations into hub memory (`docs/memory.md`). The bench memory arms `claude-mem` and `both` are historical labels for `memory.enabled`, not a live claude-mem path.
|
|
10
10
|
|
|
11
11
|
## Evidence
|
|
12
12
|
|
|
@@ -18,7 +18,7 @@ Status 2026-10-12: keep optional. No measured effect yet.
|
|
|
18
18
|
| Part | Decision | Basis |
|
|
19
19
|
| --- | --- | --- |
|
|
20
20
|
| Recall at session start | keep optional | no measurement; the hub already reaches every peer through `recallFor` |
|
|
21
|
-
| Capture of tool use | keep optional | no measurement; capture
|
|
21
|
+
| Capture of tool use | keep optional | no measurement; this is claude-mem's own native capture, not the hub's (the hub's capture is local and deterministic), and PII rules keep the hub's PII work out of every observer |
|
|
22
22
|
| Long-term store, tiers, graph of notes | adopted natively in #258 | the user's request for cross-project and cross-machine memory, not a measured effect; #258 AC11 measures it (memory off, claude-mem only, native graph) |
|
|
23
23
|
|
|
24
24
|
Revisit when an arm comparison has at least 5 attempts per arm and measure, or field data has 5 tasks on each side.
|
|
@@ -4,5 +4,5 @@ Status 2026-10-12: keep optional. No measured effect yet.
|
|
|
4
4
|
|
|
5
5
|
- What it does: the OCR delegation gate selects files and rules for a review that the host agent then performs; in this workspace it gates merges.
|
|
6
6
|
- Expected effects to measure: review rounds and reviewer unmet items per approved task, `regressed` and `reverted` labels after approval.
|
|
7
|
-
- Evidence: none collected; the fingerprint records it
|
|
7
|
+
- Evidence: none collected; the fingerprint records it when the plugin is enabled in Claude Code or in Codex `config.toml`. Arms owed: `all-on`, `open-code-review-off`, `all-off`.
|
|
8
8
|
- Decision: keep optional. The hub's reviewer role and review checklists (#35) are the native counterpart; no part is adopted without data.
|
|
@@ -90,3 +90,9 @@ Review of PR #1050 found that including accepted runner configuration and raw-ke
|
|
|
90
90
|
Review of PR #1052 found that full-index trust sweeps and hashing every retained capture under the registry writer lock made ordinary startup refuse large repositories or starve other project writers. Per-start scoped trust and bounded quarantine transactions separate execution authority and deletion safety from maintenance cost.
|
|
91
91
|
|
|
92
92
|
The ledger review of PR #1052 found that applying the startup admission wait to worker completion could strand the ledger in running state after contention or a crash. Signed process ownership, an atomic dead-owner replacement, a normal worker wait and read-only doctor paths keep deferred maintenance recoverable and visible.
|
|
93
|
+
|
|
94
|
+
On 2026-11-09, the #1320 Record read projection used the global configuration
|
|
95
|
+
revision. A roster-only reload then discarded a held public recall response, so
|
|
96
|
+
the existing #315 preface test lost its preface even though the independent
|
|
97
|
+
off-roster attach guard remained correct. The memory/privacy-specific read
|
|
98
|
+
revision preserves both behaviors; capture still uses its stricter turn fence.
|
|
@@ -6,6 +6,8 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
6
6
|
|
|
7
7
|
## Adapter definitions, the registry and external modules (#451)
|
|
8
8
|
|
|
9
|
+
- Optional live model selection must preserve ordinary no-override delivery when its native surface is unsupported or unverified. A retained desired override must instead refuse before any prompt. Test both paths through the actual daemon factory; capability metadata alone cannot prove runtime readiness.
|
|
10
|
+
|
|
9
11
|
- Keep per-kind native policy in `src/adapters/definitions/` and consume it through the registry. Private CLI implementations accept only fixed host-bound ports; keep identity, audit, lifecycle admission and effect ordering in the host. Except for the verified person-owned A4 startup-module contract, do not add config-selected callbacks or runtime imports from registry leaves into eagerly initialized hub/CLI modules; use type-only ports for existing host planners.
|
|
10
12
|
- Adapter registry (#451): kind-specific policy lives in that peer's `AdapterDefinition` under `src/adapters/definitions/` (runtime definitions `NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN` in `builtin-metadata.ts` plus `qwen.ts`; the daemon constructs every native driver through `start.create` and `DAEMON_PORTS`). Outside `src/adapters/`, read a semantic field of `definitionOf(id)`: never a raw peer-id comparison, a resolved kind (`kindOf`, `definitionOf(...).kind`) or a constant alias compared with a native name, a vendor-keyed object, a peer-name array, or a closed-list import from `peer-catalog.ts`, `start-mode.ts` or `permission-mode.ts`; the guard resolves aliases and import/re-export bindings. `test/registry-guard.baseline.json` is `[]` and stays empty: a new site moves into a definition, never into the baseline. The only exceptions are the cross-release literals `KNOWN_PEERS`, `OPTIONAL_PEERS` and `START_MODE_PEERS`, which `test/registry-guard.test.ts` holds equal to the derived lists. `LaunchContext.nativeCommand` carries only validated config argv (or its redacted preview), never model input; a definition's `permission.grants` text is what `permissionGrant` shows, ahead of the built-in wording. Keep native mode-id maps in `definitions/*-native.ts`, free of registry factories (`test/adapter-import-order.test.ts`).
|
|
11
13
|
- Registry load runs `assertDaemonEngine` and `assertNativeCapabilityScope` before publication. A definition that is not a fixed builtin (object identity in `fixedBuiltinDefinitions`, never a kind name) may not declare `start.acpCommand`, `start.upStart`, `attach.admission`, `budget.resetSoftTokensOnResume`, `session.crashIdentity`, `session.nativeRevert`, a `pi-rpc` or `claude-implementer` daemon port, `identity.currentChild`, `routing`, `projection`, `reviewerVariant`, `headlessVariant`, `reviewer`, `hooks.factsHook`, `cli.entry`, `cli.preview`, `cli.usage` or `instances.modelSelect`. Only `definitionOf(id, instances)` picks a kind's `reviewerVariant`, for a declared extra with `start_mode: "headless"`, roles exactly `["reviewer"]`, and either a variant whose `reviewer.declaration` is `"automatic"` (Claude, agy) or `review_mode: "fresh"` (Codex, Kimi); without that opt-in a reviewer-only Codex or Kimi instance keeps its native adapter. It picks a kind's `headlessVariant` (Claude, #525-4) for a declared headless extra whose roles include `implementer` and hold only `implementer` and `verifier`. Metadata-only reviewer variants never establish native peer support; `isSupportedPeer` shares the registry runtime/validated-instance predicate with the catalog and daemon. A new field that depends on fixed-builtin assumptions or host-global native state joins that list (`test/adapter-capability-scope.test.ts`). `trust.onPrem` admits PII work (rule: `tasks.md`).
|
|
@@ -53,6 +55,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
53
55
|
- 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.
|
|
54
56
|
- 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.
|
|
55
57
|
- Session titles (#1055, spec `docs/design/adapters.md` "Session title slots"): only a fixed builtin may declare `title` or `titlePersistence` (`assertNativeCapabilityScope`), and only a `title: "hub"` definition (Pi, local) is offered `hub_set_title` (`sessionTitleTools`); keep it off Claude, Codex, Kimi and ACP tool lists and native auto-approval names. Write a title only through `setSessionTitle` (unnamed-only for an agent, `current()` rechecked right before the effect) and report it only through `onDoing("title", ...)`; never pass a title call to `observeTool` or capture, and never store a derived unnamed label.
|
|
58
|
+
- Final answers (#1320): a driven adapter publishes a model's final turn answer through `BasePeer.finalAnswer` (the bus's `onFinalAnswer`), never `onMessage`, and passes `complete` only for a normal completion. `onMessage` skips Record capture and lets `amem:` Record lines reach peers unstripped. Decorate a turn's fixed prompt with `turnRecordPrompt`, never native client configuration.
|
|
56
59
|
- Codex 0.154.0 `agentMessage` items carry `text` and `phase` (not `content[]`); only the last non-`commentary` message of a turn is shared.
|
|
57
60
|
- 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).
|
|
58
61
|
- 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.
|
|
@@ -98,7 +101,8 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
98
101
|
|
|
99
102
|
## TUI launchers, native facts and Antigravity
|
|
100
103
|
|
|
101
|
-
- Kimi TUI is a pull-only WsPeer under the one kimi id, with no hub busy/watchdog/permission/quota machinery. Only the person's launcher merges or removes its project MCP entry; a daemon headless start refuses a stale entry and never edits the file. Scrub mode/channel markers from the native child; managed AGENTS instructions and tool descriptions carry the pull rules, not MCP initialize instructions.
|
|
104
|
+
- Kimi TUI is a pull-only WsPeer under the one kimi id, with no hub busy/watchdog/permission/quota machinery. Only the person's launcher merges or removes its project MCP entry; a daemon headless start refuses a stale entry and never edits the file. Scrub mode/channel markers from the native child; managed AGENTS instructions and tool descriptions carry the pull rules, not MCP initialize instructions. Completion facts (#1282) do not change that pull-only transport: no push, steer, watchdog, permission or automatic quota handoff.
|
|
105
|
+
- Kimi TUI completion facts are admitted only on compat `kimi-tui` (2.5.2) through `native_hook_launch`, a launch-scoped plugin under the native home, and `KIMI_TUI_FACTS_HOOK`. The launcher uses async spawn so its control socket stays live, previews plugin writes, and cleans only that launch's files. A different launch's plugin is replaced only after a new begin succeeds and its recorded owner pid+signature is proven gone; an active or unknown owner is never replaced, including another project's launch sharing the native home. Replacement and cleanup share one fail-closed lock per native home; a stale lock is never removed automatically. Begin or setup failure prints the cause and next step and launches the plain TUI without completion receipts. Quiet background/final-turn re-reads back off from 250 ms to 1 s and the Stop deadline also arms them. `turn.prompt` proves origin, `turn.ended` proves the outcome; `prompt.completed`/`aborted` are optional consistency rows, and notification turns may omit prompt ids. Never treat Stop, a final message chunk, a heartbeat or an empty observed task list as proof. Background inventory is read from every agent's task files and stays unknown on any malformed or unreadable row. Final-turn and inventory re-reads must not emit prompt, response, error, task command or title text. Native-person results are private. StopFailure is failed only after final-state readback, Interrupt is quiet, and launcher end/socket loss creates at most one interrupted result.
|
|
102
106
|
- Empty-inbox polling advice (#681, `InboxPolling` in `src/hub/inbox-polling.ts`) is advisory: never refuse or delay an inbox read for it. The daemon computes it only for a pull-only peer after the pull completed, so a throw drops the advice, never the pulled envelopes; it counts only empty reads with no open, non-backlog task as owner or reviewer, and an assignment resets it. The plugin shows `[polling detected]` only for a well-formed `pollingAdvice` (count >= 3, minutes 0 to 10).
|
|
103
107
|
- 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.
|
|
104
108
|
- 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.
|
|
@@ -108,9 +112,9 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
108
112
|
- 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.
|
|
109
113
|
- 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.
|
|
110
114
|
- Jcode defers MCP tools (#912, `JCODE_DEFERRED_TOOLS_INSTRUCTION` in `src/hub/envelope.ts`): a Jcode peer lists only its built-in tools until the model searches for an MCP tool, so without a hint it reports the hub tools missing and answers in its final message. The guidance is written in exactly one place, that constant, and `JcodePeer` sends it through `jcodePreamble` (`src/adapters/jcode.ts`) ahead of the role contract, once with a session's first prompt, for the base peer and every extra instance. Never copy it into `STANDING_INSTRUCTION`, `templates/AGENTS.block.md` or a profile template (the Jcode bullet there is the same text in every template, and `test/jcode-deferred-tools.test.ts` pins both), and never change Jcode's own deferral or cache settings from the hub. The tools it names must exist, and the server name must be `JCODE_MCP_NAME`.
|
|
111
|
-
- Antigravity P1 (`src/adapters/agy/`): keep it optional, cloud, off roster by default, implementer/verifier only and pull-only. Until per-mode native prompt behavior is verified, reject hub permission/unattended mappings, headless starts and resume selectors; do not infer a quota source. The launcher refuses extra instances, any `agy --version`
|
|
115
|
+
- Antigravity P1 (`src/adapters/agy/`): keep it optional, cloud, off roster by default, implementer/verifier only and pull-only. Until per-mode native prompt behavior is verified, reject hub permission/unattended mappings, headless starts and resume selectors; do not infer a quota source. The launcher refuses extra instances, any `agy --version` not admitted by `assessVersion("agy", ...)` (`COMPAT.agy` in `src/adapters/compat.ts` is the verified-set/warning-line source of truth; exact person trust is separate, #691), and a hub without the `native_hook_launch` feature.
|
|
112
116
|
- Only the person's `ahub agy` launcher manages `.agents/plugins/agent-hub/{plugin.json,mcp_config.json,hooks.json}`. Refuse linked/foreign entries before any write; cleanup only the matching launch. Never write project-level `.agents/hooks.json` or personal Gemini settings. The plugin MCP server is named `agent-hub_agent-hub` by the pinned native client.
|
|
113
|
-
- Managed native facts use the definition's typed `NativeFactsPort` and the daemon's ephemeral `native_hook_launch` authority. Bind every observation to the daemon, launch, native session, project and current socket generation. Stop requires `fullyIdle
|
|
117
|
+
- Managed native facts use the definition's typed `NativeFactsPort` and the daemon's ephemeral `native_hook_launch` authority. Bind every observation to the daemon, launch, native session, project and current socket generation, and bind `WsPeer`'s live `nativeSession` (`bindNativeSession`) from `case "native_facts"` while keeping new `WsPeer` instances free of `nativeSession` until observed. Normal `Stop` (`NO_TOOL_CALL`) requires `fullyIdle` and a fresh validated completion row (a missing readback keeps the peer busy); terminal non-normal `Stop` reasons (`USER_CANCELED`, `ERROR`, `MAX_INVOCATIONS`, `MAX_FORCED_INVOCATIONS`, `MAX_TOKEN_BUDGET_EXCEEDED`, `TERMINAL_STEP_TYPE`, `TERMINAL_CUSTOM_HOOK`) with `fullyIdle` acknowledge any pending transcript facts/usage, clear the active turn, end any waiting cohort turn, record a closed-reason `native_turn_end` event (`reason` without `id`), settle any open #1054 run by its closed reason (pull-only `agy` opens none), and return the peer to `idle` (#1280). Native fact acknowledgement accepts only `SYSTEM_SDK` rows (`EPHEMERAL_MESSAGE` in 1.4.x, `SYSTEM_MESSAGE` in 1.5.x) in the exact session transcript path, extracts completed `PLANNER_RESPONSE` token counters (`input_tokens` excludes `cache_read_tokens`: cache reads exceeded input on 2947 of 3021 real 1.5.x rows, so `totalTokens` is their sum, not a double count) into `source: "agy_transcript"` usage events (deduped by hashed `(session, step_index)` across restarts), and advances the per-launch transcript cursor only after a scan that reached the previous cursor (or the 4 MiB lookback ceiling). `decodeAgyHook` normalizes `Read` (`view_file` `AbsolutePath`, `read_file` `Path`, with `StartLine`/`EndLine`/`ByteOffset` carried as `offset`/`limit` so partial reads never count as a whole-file view), `Write` (`write_to_file` `TargetFile`/`CodeContent`, `write_file` `Path`/`Content` when not `Append`), and `Edit` (`replace_all` only when `AllowMultiple` is true: a missing flag reads as a single replacement because no native receipt shows the default) (`replace_file_content` `TargetFile`/`TargetContent`/`ReplacementContent`, `edit_file` `Path`/`Target`/`Replacement`). The daemon records Pre/PostToolUse through `Facts.preTool`/`postTool` and offers nothing on them (#555, #1280). `INSTRUCTION_FILES` (`src/hub/permission-mode.ts`) includes `gemini.md` alongside `agents.md`, `claude.md` and `qwen.md`. Pull-only `agy` has no push, so on `SessionStart`/`PreInvocation` the daemon appends a count-only line (`[agent-hub: N message(s) waiting (M important); call hub_inbox]`, never a body or sender) to the injected message when `bus.queued(peer) > 0`, with or without a fact offer (#1280 P2). `agy acp` was probed model-free (`docs/verification/2026-11-08-agy-acp-t0.md`); its `AcpPeer` wiring waits for one authorized model-backed receipt.
|
|
114
118
|
- `terminalOwner(launchId)` (`src/hub/peers.ts`, #482) answers only for the adapter's current native TUI owner bound to that launch (Codex: the claimed link with that `launchId`, both sockets open; the launcher sends the id as the upgrade's bearer token through `--remote-auth-token-env`, because `codex --remote` refuses any path or query (#586); Pi: a started, verified TUI owner on its requested session); a start acknowledgement is never ownership. A Codex link that is not the claimed one or has a closed socket never adopts a thread.
|
|
115
119
|
|
|
116
120
|
## Reviewers
|
|
@@ -124,13 +128,13 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
124
128
|
- Every print child (reviewer and Claude implementer) is spawned, bounded and stopped only through `OwnedPrintChild` (`src/adapters/claude-print-child.ts`): its `done` settles only after the whole group is confirmed gone, and a failed stop rejects with `PrintChildCleanupError` while the owning peer keeps the handle. Never add a second spawn path to an adapter.
|
|
125
129
|
- Owned input observers (#1056): `beforeInput`/`inputReleased` (`OwnedInputObserver` in `src/adapters/claude-print-child.ts`) carry review-observer provenance only. `observerSessionId` (Claude implementer, reviewer print, local) is never `nativeSession` and never grants budget, resume, permission or task authority. `beforeInput` runs with the final target session before any prompt reaches the child or model, and a throw releases no prompt; `onInputReleased` fires only after stdin flushed for the current generation, and an observer throw never changes settlement. A fresh reviewer calls `inputFinished` only after its group is confirmed gone, its task MCP is closed and launch cleanup succeeded.
|
|
126
130
|
- Retained standby (#1174, `WsPeer` in `src/adapters/channel-peer.ts`): an opt-in (`retainStandby`) hello while another session holds the peer is kept as one of at most `MAX_STANDBY_SESSIONS` (8) standbys; `standbySessions` ids and PIDs are display only, never native-session provenance or authority. Ownership changes only through `takeover` (the person's `peer_takeover`, or the oldest standby when the owner leaves), guarded by the host's `channelTakeoverAdmission` and `claim.isCurrent()` across the async preface; the old socket closes 4405 (opt-in) or 4403 "detached by the person" and its native process is not stopped. A standby socket runs no peer operation until `ownsSocket` confirms it. A hello without the opt-in keeps newest-hello replacement.
|
|
127
|
-
- A reviewer backend is a fixed builtin `ReviewerBackend` in a definition's `reviewer`, never config. Its `prepare` must return a complete isolation receipt (matching backend id, binary, version, profile, evidence and `readOnly` true, plus `strictMcp` and `ephemeral` true, or for a `nativePolicy` backend `enforcement: "native-policy"`, `freshSession` and `hubToolsOnly` true) before every child. A `taskMcp` backend refuses without the host's `taskMcp` port and gets only that task's MCP config, never the hub-wide one. `admit(env)` and `launch.current()` are rechecked right before spawn
|
|
131
|
+
- A reviewer backend is a fixed builtin `ReviewerBackend` in a definition's `reviewer`, never config. Its `prepare` must return a complete isolation receipt (matching backend id, binary, version, profile, evidence and `readOnly` true, plus `strictMcp` and `ephemeral` true, or for a `nativePolicy` backend `enforcement: "native-policy"`, `freshSession` and `hubToolsOnly` true) before every child. A `taskMcp` backend refuses without the host's `taskMcp` port and gets only that task's MCP config, never the hub-wide one. `admit(env)` and the complete synchronous `launch.current()` are rechecked right before spawn; keep that complete fence in `reviewToken()` at every verdict/task-MCP authority boundary. A `watchCurrent` launch supplies `startPolicyWatch()` with a review-owned full-policy probe; run its serialized asynchronous checks on absolute 100 ms due times, coalescing missed ticks without overlap or an extra post-check delay. Pass only expected paths/fingerprints to its worker, never task text or inherited environment. Fence the exact child/review sequence on failure as `review policy changed`, and terminate/join the worker before private cleanup even when native stop fails; retain an unconfirmed owner for explicit stop retry; a missing or failed proof refuses, never a weaker launch or a continuing native session. The daemon puts the reviewer on the bus only after `start()` proved isolation; a failed start or a `start_failed` report marks it in `reviewerUnavailable`, which routing reads as `failing`. An unavailable reviewer variant has no reviewer backend or launch code; `startPeerBody` refuses it before preflight, independently of any native TUI launcher.
|
|
128
132
|
- Report reviewer failures (`onUnavailable`: `start_failed`, `quota`, `no_verdict`) only after the child's group is confirmed gone and only while its generation is current: a person's stop owns its hold and never reroutes. Complete a normal no-verdict delivery only after the host confirms a current re-offer; an uncertain or failed fallback stays `needs_review`, with its `REVIEWER_FALLBACK_UNCONFIRMED` reason excluded from automatic moved-review settlement; ordinary superseded holds keep their existing rule. Log one bounded no-verdict line from exit code and allowlisted hub-tool metadata only, with full refusal privacy/secret screening before sanitization and truncation; retain the first full host refusal across native echoes, and withhold differing later host refusals rather than replacing or accumulating them; stale/private context is withheld. Record only allowlisted tool names and refusal text; never retain argument objects, task/diff fields or model-message events as trace data. `quota` comes only from the backend's structured `failure(event)`, never from vendor text. An NDJSON frame over 256 KiB or more than 4 MiB of output aborts the review as `needs_review`.
|
|
129
133
|
- Codex reviewer isolation (`src/adapters/codex-review-isolation.ts`): accept only a `codex-cli` version that compat `codex-review` admits (verified or the person's trust, #691), and still run every proof below after trust; pass `-c notify=[]` (legacy notify off) on every launch; prove model-free that Apps, plugins and hooks are disabled (`features list`), ambient MCP servers are replaced by disabled stubs, and the hub server alone is enabled with exactly `hub_review` and `hub_task_show`; approve exactly those two MCP tools per tool, never with a server-wide approval default; before marking a reviewer backend/version verified, record a successful model-backed hub tool call as well as the model-free inventory (inventory alone is not verdict-delivery proof); run the native sandbox fixture probe before every launch; hash the binary once and fence binary and config files by metadata. Credential denies come only from the bounded `HOST_CREDENTIAL_PATHS` and `HOST_CREDENTIAL_ENV` lists, denied at the lexical and canonical path; refuse glob syntax or a root inside the checkout. A root that is dangling or unreadable refuses too; the reason names only its path class (`~/.docker`, `$DOCKER_CONFIG`), never a path or contents. Never pass the control token in a reviewer child's argv, env or MCP config: keep the hub state dir in its `privatePaths`. A refused `prepare` carries a `causeClass` from the closed `REVIEWER_CAUSE_CLASSES` (`version`, `binary`, `mcp_inventory`, `plugins_hooks_apps`, `sandbox_proof`, `credential_root`, `timeout`, `unknown`; `reviewer-print.ts`, #722): `prepare` tracks the proof leg it is on, and `timeout` means a metadata command ran out of its 2 s or the 12 s budget, never an abort. The host builds the reason only through `reviewerUnavailableReason(cause, detail)`: a fixed sentence per class, plus a credential-root path class (`~/.docker: a dangling link`) as the only detail. The one exception is a failed `start()` whose error is a version refusal (`VERSION_REFUSAL`) or comes from a `nativePolicy` backend: the daemon shows that sanitized message, capped at 1000 characters (#691). `ReviewerPrintPeer` classes a missing or invalid receipt as `unknown`, a `launch.current()` change before spawn as `binary`, and any other start error as `unknown`. Keep admission refusals separate from start failures: skip only a stale-bound request with one task-id log and no reroute; reroute a still-current request whose policy changed as `no_verdict`, without marking the reviewer unavailable. The daemon writes that reason to the `reviewerUnavailable` map (read by routing and shown as the `reviewer` line of `ahub status`), and a `hub.log` line names the class. Never add native CLI text, a path, a version output or a token to the class, the reason or the log.
|
|
130
134
|
|
|
131
135
|
## Headless Claude implementer (#525-4)
|
|
132
136
|
|
|
133
|
-
- `ClaudeImplementerPeer` (`src/adapters/claude-implementer.ts`) runs one owned print child per turn in its verified worktree and none while idle. Keep the native argv of `claudeImplementerArgs`: `--permission-mode default`, `--strict-mcp-config` and `--permission-prompt-tool mcp__agent-hub__hub_permission_request`; never `acceptEdits`, `dontAsk`, `bypassPermissions` or `--bare`. `--model` is added only from the instance's validated declared `model` (#844, `instances.modelSelect` on the headless variant; one `NATIVE_WORD`, headless implementer only, untracked config only), on every turn, resumed ones included; never from task, chat or tool text. `never-ask` and `--unattended` are refused at every layer (factory, `setPermissionMode`, daemon start, `validateInstances`); a mode change only changes what the host facts gate allows. A start needs a version compat `claude-implementer` admits, and each turn rechecks the binary fingerprint from `proveClaudeImplementer`.
|
|
137
|
+
- `ClaudeImplementerPeer` (`src/adapters/claude-implementer.ts`) runs one owned print child per turn in its verified worktree and none while idle. Keep the native argv of `claudeImplementerArgs`: `--permission-mode default`, `--strict-mcp-config` and `--permission-prompt-tool mcp__agent-hub__hub_permission_request`; never `acceptEdits`, `dontAsk`, `bypassPermissions` or `--bare`. `--model` is added only from the instance's validated declared `model` (#844, `instances.modelSelect` on the headless variant; one `NATIVE_WORD`, headless implementer only, untracked config only) or, ahead of it, the person's runtime override sampled at each turn boundary (`getModelOverride`, `ahub model`, #1317, rechecked with `isNativeModelToken`), on every turn, resumed ones included; never from task, chat or tool text. `never-ask` and `--unattended` are refused at every layer (factory, `setPermissionMode`, daemon start, `validateInstances`); a mode change only changes what the host facts gate allows. A start needs a version compat `claude-implementer` admits, and each turn rechecks the binary fingerprint from `proveClaudeImplementer`.
|
|
134
138
|
- Per-task native sessions live only in `ClaudeTaskSessions` (`src/adapters/claude-task-sessions.ts`, file `claudeStateFile("claude-task-sessions", id)`, ids only). A reserved UUID becomes `--resume` only after `markStarted` saw a stream-json frame naming that session; forget it only when `transcriptPersisted` answers false (unknown keeps it); an unreadable or invalid state file throws, never resets. Chat without a task ref goes to the instance's single `in_progress` task (`chatTask`), else to a `--no-session-persistence` one-off child.
|
|
135
139
|
- A private envelope refuses the delivery before spawn; an item `admit` refuses is dropped without vetoing the others. Settle `failed_safe` until the child's stdin was released (`inputReleased`), `needs_review` after; a failed group cleanup keeps the handle and stays `busy`.
|
|
136
140
|
- The child's tools-mode server (`AGENTHUB_PERMISSION_TOOL=1`) lists only `IMPLEMENTER_HUB_TOOL_NAMES`. `hub_permission_request` returns the original input only for the person's allow-once. Automatic Bash grants require the #845 host wrapper, a current owned public task/root/branch, existing read-only or closed task-command proof, and wrapper protocol 1 on request and reply; never return an automatic allow for the original native command. Consume the expiring single-use ticket before rechecking and executing with the fixed host environment, absolute executable and proven cwd; changed proof/task/child/root/branch runs nothing. Require the local-worker mutating Seatbelt profile for check/test/build with an additive deny for all project Git metadata and the canonical hub state. Only closed host Git add/commit/push and gh stay outside it; refuse configured filter/external drivers/textconv for add/commit only (global Git LFS must not disable push/gh), retain the config-include/partial-clone refusal for privileged Git/gh, and disable signing/automatic maintenance. Pin default-branch authority from the remote, never mutable local origin/HEAD. Unknown or unavailable confinement asks. Enabled action classes and `ask` still ask; malformed requests, no hub, expiry or a changed child deny. The original arguments stay in the child's request; the person sees only `permissionPayload` (`src/hub/permission-payload.ts`): the whole value withheld on any screen match, cut with `[cut, N chars]` inside 600 characters. Daemon admission of that child: `daemon.md`.
|
|
@@ -32,4 +32,4 @@ Scope: quota and native context readings, checkpoints, pause, handoff and resume
|
|
|
32
32
|
|
|
33
33
|
- Usage roll-up (#683 U2): `src/hub/usage-rollup.ts` is the only reader of `events.jsonl` for `ahub usage`, the console Usage panel and the dashboard Usage view (rules: `daemon.md`). A pass reads from the stored offset, keeps whole lines only and commits its rows with the new offset in one transaction; its 20 ms slice (`poll(maxMs)`, #719) stops only between whole records and commits the offset of the last one folded; a line longer than the cap is read whole and one past `MAX_LINE_BYTES` is skipped, so the fold always advances. Never store a title, reason or summary there; `limit_events` drops `reason`. issue/pr/release are joined from the board at query time through `TaskLinks`, never stored. A counter with no record behind it is `unknown`, not 0. Claude's total is derived as the sum of its four counters (`claudeReportedTokens`); a source whose input contains the cached part gets none. Every `usage` reply, errors included, answers the rid, and the op is for the console and a configured planner only, never during that planner's private turn. A rebuild from byte 0 must equal the incremental result (`test/usage-rollup.test.ts`). Never present unknown as 0 in a sum: an overall figure with an unknown peer is `partial` and names the peers (`total().unknownPeers`). Never drop rows silently: `limits()` reports `truncated`, `omitted` and the bound. An event-open pause the hub no longer holds is `endedUnknown` (`ahub reset` keeps the log), so `limits(peer, { live })` needs the daemon's held pauses. A poll's fold transaction is IMMEDIATE and rechecks the stored offset, so two roll-ups on one `hub.db` never fold a range twice.
|
|
34
34
|
- Daily usage (#683 U4): `UsageRollup.daily(days, now)` returns the last `days` UTC calendar days (at most 366) ending today, clipped to the first day with a record. A day inside the window with no record is a row with `empty: true`, never zeros. Per peer and day, take the native total when reported, else the counted tokens (`count`), else `unknown`; a day's total is `partial` when some peers are unknown and `unknown` when all are, never a sum that treats them as 0.
|
|
35
|
-
- Kimi's consumption (#285 phase 2): `src/adapters/kimi-wire-usage.ts` is the only reader of Kimi's wire log and `src/hub/kimi-usage.ts` the only owner of the Kimi count; `daemon.ts` supplies ports (events, budget, clock, session id) and binds the session at a turn start (no read)
|
|
35
|
+
- Kimi's consumption (#285 phase 2, delayed drain #1281, sliced drain #1283): `src/adapters/kimi-wire-usage.ts` is the only reader of Kimi's wire log and `src/hub/kimi-usage.ts` the only owner of the Kimi count; `daemon.ts` supplies ports (events, budget, clock, session id, unreferenced scheduler) and binds the session at a turn start (no read), calls `collectKimiUsage` at a turn end, a `usage_update` and an offline transition, and schedules at most three coalesced follow-ups (250 ms, 1,000 ms, 2,000 ms). With a scheduler, one invocation reads and commits one slice, then yields with a 0 ms continuation while the same cue's backlog remains (at most 16 slices per burst). Discovery is fresh on every slice; do not cache the agent set. Without a scheduler, the original synchronous 16-pass behavior remains for compatibility. Stop, offline and session replacement cancel delayed work; offline preserves the final synchronous `collect -> drop`, with rows durable only later as the documented ceiling. A native row time selects a unique interval among the most recent 32 captured turns; no unique match gives `unattributed`. Late rows never rewrite an earlier `turn_end` or increment a later turn. A successful reader slice commits offsets only after its rows are recorded; restart seeding counts only rows recorded after the last `budget_resume` of a soft-token peer (`kimiSeedRows`). Tests never reach the person's `~/.kimi-code`: `test/hub-home.ts` sets `KIMI_CODE_HOME` to a temp dir. Never feed `budget.report(kimi, tokens)` from an ACP total or an estimate, and never change `kimiCountedTokens` without the AC5 test: cache reads are recorded, not counted. An unreadable log, an unknown row shape or an untested `protocol_version` is unknown (reading dropped via `Budget.forget(peer, "tokens", "count")`, so only the count's own reading goes; one notice), never a partial count. A usage row's text never leaves the reader: four integers, a model label and the row time. A resume clears the window (`kimiUsage.reset()`).
|
package/docs/agent-notes/bus.md
CHANGED
|
@@ -12,7 +12,7 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
|
|
|
12
12
|
- `replyParent()` decides what a reply answers (highest hop, never the `hub` preface). Use it for deliveries and steers alike, or the hop cap can be reset.
|
|
13
13
|
- Report a send's outcome from `Bus.publishResult`, never from `publish()`'s bare target list: a hop-capped send answers `ok: true, dropped: "hop"` with the hop-cap notice, and the channel tool and `ahub say` print that notice, never "no peers attached" (#546). A native peer's `onMessage` returns that `PublishResult` on a hop drop, its send tool (Pi, the local worker) reports it as not delivered, and no off-roster retention note follows a hop drop (#550).
|
|
14
14
|
- What a peer is handed (`out`) and what it stands for (`originals`) differ once a delivery is condensed: the bus keeps both, registers `out` so `reply_to` resolves, puts the originals back on a `failed_safe` receipt and holds any other failure (a thrown `deliver`, a later `onFailed`, a `needs_review` receipt) as `needs_review` with its originals, for `ahub queue resolve`, and counts no attempt when the peer merely got busy while the delivery was prepared. A delivery with an `important` envelope, an invocation kind or a remote sender is never condensed; a remote recipient (`remoteId`, rooms #941) gets one envelope per delivery with its queue deadline, never a preface, batching delay, condensation or steer.
|
|
15
|
-
- Invocations (#445 C2, #986): an `invocation` or `invocation_result` (`isInvocationKind`) goes alone in `take` and `takeBudget`, is never condensed, and is never steered into: both steer sites and `steerQueued` ask `invocationTurnOpen` first. In `onMessage`, a peer's answer goes to `onAnswer`
|
|
15
|
+
- Invocations (#445 C2, #986): an `invocation` or `invocation_result` (`isInvocationKind`) goes alone in `take` and `takeBudget`, is never condensed, and is never steered into: both steer sites and `steerQueued` ask `invocationTurnOpen` first. In `onMessage`, a peer's answer goes to `onAnswer` with its marker still unparsed (a final answer has only its `amem:` Record lines stripped, #1320) and before any publish, when it names the request (`inReplyTo` in the pushed `lastDelivery` or the pulled `lastPulled`) or names nothing while the invocation turn is open. A pull is kept in `lastPulled`, never in `lastDelivery`, so it cannot replace a pushed invocation's answer parent (#1192). A taken answer, or a throwing hook, publishes, stores and emits nothing.
|
|
16
16
|
- An invocation is never replayed through ordinary retry (#991). `retryDelivery` refuses a row that holds one. An uncertain or thrown delivery holds the peer (`recoveryHeldPeers`) and calls `onInvocationUncertain` instead of `failed()`. `failed()` allows at most one automatic retry, and only when `canRetryInvocation` returns true; unknown or a throw refuses.
|
|
17
17
|
- Add an envelope kind only to `KINDS` (`envelope.ts`); the journal, restart reader, deferred-hub check and held hints read that one list. Test room provenance with `remoteEnvelope(env)`, never `from.includes("@")`: a relayed room invocation arrives `from: hub` with `refs.invocationRemote` (#992).
|
|
18
18
|
- `Envelope.session` (agent-hub#1036) marks a sender that is a distinct session, `from` being its minted `<peer>~<origin>` id (rules: `daemon.md`). It grants nothing; keep it on queued and journaled originals, because the `session_identity` recovery guard reads it from unsettled deliveries.
|
|
@@ -103,7 +103,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
103
103
|
- Use the registry's `KNOWN_PEERS` (`src/adapters/registry.ts`) for the native catalog (the `src/hub/peer-catalog.ts` literal is a cross-release probe only) and `rosterAllows` for participation. Change live membership only at the authorized roster settings write or undo, applying exactly that key. A roster key that `staleInstanceKey` matches (an undeclared `<kind>-<name>` instance id) loads inert and grants nothing; its only admitted write is deletion from `.agenthub/config.local.json` (`ahub roster remove <id>`, applied live), refused while it lives only in, or differs in, `.agenthub/config.json`; adding or setting one is refused (an unknown key on reload). Keep external ids and projects without roster settings compatible with the existing admission and lazy status rules. Fence starts and attachment before detaching or stopping a removed runtime; retain its queue, uncertain deliveries and independent pauses. Refuse removal of the conductor holder and require explicit confirmation and `--now` for an active turn.
|
|
104
104
|
|
|
105
105
|
- 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.
|
|
106
|
-
- `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.
|
|
106
|
+
- `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. Both settle an open run `interrupted` once. 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. When a launch records `nativeHome`, its transcript index must be exactly `<nativeHome>/session_index.jsonl`. Close a `ws-pull` peer's hello without `channel: false` with 4403.
|
|
107
107
|
- `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`.
|
|
108
108
|
|
|
109
109
|
- 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.
|
|
@@ -140,7 +140,8 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
140
140
|
- Share one monotonic 250 ms snapshot Git budget until the next event-loop I/O turn. Repository/HEAD probes, capture and end diffs consume the same remaining deadline; exhaustion or unreadable diff evidence records unknown trees and no attribution/undo, with one diagnostic per burst. Never substitute a stale baseline or apply a late result (#576). `DaemonOptions.snapshotClock` is host-only (tests hold it, rule: `tests.md`): never read it from configuration or a tool, production leaves it unset (`performance.now`), and the share stays 250 ms with `snapshotBurstMs` still clamped to it. Every deadline comparison goes through `budgetNow(budget)`, never `performance.now()` against `budget.deadline`.
|
|
141
141
|
|
|
142
142
|
- Channel tool-result hints (#493, #221): `withPendingHint` (`src/adapters/claude-channel.ts`) appends fresh `delivery_holds` counts, and drops the "call hub_inbox" advice whenever the same reply carries a held-delivery hint, because `bus.pull` refuses under that hold and the advice would send the session into a refused read. A `hub_inbox` result that already says how many messages remain unread adds only the important count, never a second queued count. A `hub_inbox` result that renders its own held or accepted metadata (`HELD_HINT_INCLUDED`) gets no second held hint and no polling advice, and unavailable metadata adds the inspect step instead of "Nothing is waiting" (#1172). Do not add a Claude client-version gate on steer without a probe of the older client and an optional hello field that carries the version (background: README).
|
|
143
|
-
-
|
|
143
|
+
- For optional Record read projections, bind `recordReadVersion` to memory/profile, privacy, execution cwd and effective Record policy, not the global config revision. Preserve already-admitted public recall across unrelated roster reloads; keep capture-generation and delivery-membership fences independent.
|
|
144
|
+
- Memory sync (#438): stop an in-flight `syncMemory` only through `syncStop` (`src/hub/daemon.ts`), and await `syncRun` before the memory store closes, as `stopOnce` does. agent-mem runs the sync's git in its own process group, so a process that ends without `daemon.stop()` leaves that git and its transport child holding the memory repository. A signal the hub exits on goes through lifecycle stop (`STOP_SIGNALS` in `src/hub/lifecycle.ts`: SIGINT, SIGTERM, SIGHUP, SIGQUIT), never the default action; SIGQUIT in particular must not reach its default core dump (Bun honours it: agent-mem #435 measured a 5.9 GB core file and a delayed exit). The forced exit after the shutdown deadline stays `process.exit`. Except for a latched typed writer-attestation refusal, a sync result is logged as a failure only when `quietSyncError` is false: a missing remote in either agent-mem wording (`no sync remote`, `no generation sync remote configured`) and a stop are not failures (#401); a new agent-mem wording for them extends that one predicate. Every pass gets the host screen and policy version (`currentMemorySyncPolicy`, `memorySyncPolicyVersion`, #1173); a version change during the pass records `binding`, never success. Record outcomes only through `reduceMemorySync` and publish them only as `memorySyncView` (`sync` in memory status: closed cause, counts, next step). The log line names the closed cause (`classifyMemorySyncFailure`), never the raw error text. Every pass also passes `placementAttestation` from `hostPlacementAttestation` (agent-mem `createPlacementAttestation`, #1173): build it per pass, never cache or synthesize writer facts, and record a `PlacementWriterContextError` as the closed cause `writer` with its category and cause class (`writerRefusalOf`), never as the generic cause `unknown` or a skip. A latched typed refusal wins over generic quiet, thrown or successful transport outcomes; project unrecognized SDK categories as category `unknown`, never a known category. `DaemonOptions.memorySyncTrigger` is a host-only fixture seam.
|
|
144
145
|
- Room shutdown (#1204 T3): `RoomCommands.stop()` fences new room work synchronously and returns a promise. Await it in `stopOnce` before closing shared storage: it waits for pending room work and each link's bounded offline-presence attempt before closing RoomStore. Preserve an attempted FIFO head and consume shutdown storage controls without admitting new presence or application work.
|
|
145
146
|
- Schedules timer (#512a): `schedulesTimer` in `startDaemon` is its own interval, apart from the task sweep's, armed whenever `config.schedules` or `config.folder_triggers` is non-empty and re-armed in `reloadConfig` on a change to either; never gate it on `task_sweep.enabled`. `schedules` is machine-local (`MACHINE_LOCAL`) and never editable from settings (`NEVER_EDITABLE`): a committed file or a settings write must not be able to make the hub create tasks. The daemon status carries `schedules` (`scheduleViews`) only when some are configured.
|
|
146
147
|
- 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.
|
|
@@ -159,4 +160,4 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
159
160
|
- Handoff transport (#604): `handoffOut` connects to the target as a `console` role through `ControlClient` (same token file and machine user as every control message; the hello checks the target's project root and instance), and the target answers every handoff message, an unknown one included. `handoffNotifyOrigin` hangs off `board.onChange` inside its own try, so a task move never waits for, or fails because of, another project's daemon. A target is judged by its own `status.json` features (`checkTarget`), never by a guess from its version. The inbound `task_handoff` is refused while recovery is active, counts in `taskOpsInFlight` so a recovery commit waits for it, and hands `handoffIn` a fence (recovery or `stopping`) that it asks again right before the board write, after the PII screen's await. Keep `task_handoff` in `DAEMON_FEATURES` and its recovery guard beside `research_experiments`; no `PROTOCOL` bump, because a new message type is answered as unknown by an older hub and an optional field is ignored. Handoff diagnostics (#1203 T3): a connection failure to a target is classified by `handoffConnectFailure` from the error's `code` (4426, 4404, `AHUB_CONNECT_TIMEOUT`, anything else), never from its message, and every handoff failure log goes through `handoffLog`, which keeps the last cause per target (`handoffFailures`, read by `handoffListView`) and rate-limits the line with `handoffLogGate`; a new silent `catch` in the handoff block is a defect. `handoffListView` opens no connection (registry and descriptor files only), so `--list` never waits on a target. `hub_task_handoff_sync` is a control op and not in `hub-tools.ts`: an agent never sees it, and `Tasks.handoffSyncAuthority` refuses anyone but the person or the conductor. `handoffCatchUp(only, wait)` with `wait` waits for the running pass and then runs its own, so a sync answers after a pass that began when it was asked, but never for longer than `handoffSyncBoundMs` (20 s, below the CLI's 30 s control timeout): a slower pass makes it answer `syncing: true` with the list, and a sync must never end in the generic "no answer from the hub". Handoff sequence (#1203 T2): `recordHandoffOutbound` applies a write with a `seq` only when it is newer, inside the one statement; `handoffEvent` returns `recorded: false` for a stale one before it publishes, so never publish a follow-back before the write says `applied`. `handoffCatchUp` reads `handoffOutboundOpen` (no cap; `handoffOutbound` is the capped display list, never the catch-up's input), runs one pass at a time and queues a request that arrives meanwhile (the targets it names, or all) to be drained when the pass ends, so the guard never drops a watcher's request; `handoffWatch` asks once for the whole changed set; it asks in chunks of `HANDOFF_STATES_CHUNK`, and checks `stopping` between requests; its timers go in `intervals`. A new handoff event goes only to an origin that advertises `task_handoff_seq`. Handoff key (#1203 T1): `handoffOut` sends a key only to a target whose status descriptor lists `task_handoff_key`; a timeout or closed connection (`handoffOutcomeUnknown`, which reads the client's fixed wording and anchors it) is resent once with the same key, then asked by key (`task_handoff_by_key`), then `outcome_unknown`, never `invalid`. A refusal of the resend never settles an unknown first attempt (its PII screen may still file the task): it is kept as a note and the by-key lookup still runs. The caller's deadline covers the whole recovery: `controlRequestTimeout` gives `hub_task_handoff` `HANDOFF_CONTROL_TIMEOUT_MS`, built from the same `HANDOFF_*` budgets `handoffOut` spends, so the CLI and the MCP path (both ask for 30 s) receive `outcome_unknown` with the key instead of a generic timeout; change a budget there and the deadline moves with it. Never write the outbound row before an answer, and never reset an existing one (`handoffSent`). The follow-back envelope carries `refs.handoff` and no `refs.task`: a new envelope field that names a task in another project must not reuse `refs.task`, which the bus judges against this board.
|
|
160
161
|
- Staged upgrades (#1039): retain `status.protocol` at the compatibility floor and publish `protocolMin`/`protocolMax`; negotiate the highest common control version and preserve each attached channel's actual negotiated protocol in recovery metadata. New scoped recovery receipts use schema 3 and per-project holds; keep legacy unscoped receipt behavior. Start ordinary project daemons from verified versioned release captures. Check `hub.db` with `db-schema.ts` before startup effects and every writable opener; advance its marker only with an incompatible format migration, never merely on opening with a newer binary. Before source replacement, controlled targets require `schema_guard`; preserve the retained target evidence of fully committed legacy receipts. Unguarded published binaries remain outside the bootstrap guarantee. Keep project runtime selection in bounded project-bound `runtime.json`; refuse Git-tracked or indeterminate repository state before using or replacing that authority. Updated 0.26+ up/manager/dashboard paths use its verified capture or a named version refusal. Preserve the explicit pre-0.26 published-CLI ceiling with a `ponytail:` note; never raise a DB/bootstrap floor for this selection. Compute path-scoped Git trust once per startup outside the writer lock; reuse its project-bound access for runtime reads and writes. Record selection and synchronous launch under the same registry lock used for capture publication and prune quarantine; confirm the unchanged selection and owner at readiness, and require the internal daemon to execute from that exact root. Scoped recovery must not refresh the shared manager. Prune only verified unreferenced versioned source captures, keep newest three/version, and defer on active recovery, unknown reference inventory or exhausted budgets. Schedule rate-limited maintenance after startup returns; keep only bounded metadata validation and atomic quarantine inside the registry writer lock, with candidate hashing and recursive deletion outside it. Refuse managed execution from reserved `.runtime-prune-*` paths; leave changed quarantine contents undeleted. Keep the 1 ms ledger wait on startup admission only; worker statements use the normal bounded wait. Record PID plus process signature and replace only a proven-gone running owner atomically under the admission transaction, with id/phase-fenced completion. Doctor names uncertain ledger state and leftover quarantine paths without repairing or deleting them. Reject empty recovery plans before source preservation.
|
|
161
162
|
- Questions (#1053, `src/hub/questions.ts`, `src/hub/question-desk.ts`): the store owns the row and its ONE terminal transition (`UPDATE ... WHERE state = 'open'`; of concurrent answer, stop and expiry exactly one sees `changes === 1`), the desk owns the waiting call and the hub-built envelope, and the daemon only supplies ports. `answer` and `questions` are console-role control messages that always reply (rid); a tools or peer connection never reaches `questionDesk.answer`, and nothing a peer sends (a `hub_send` body, a tool result, an envelope) is parsed as an answer. A late answer is `kind: "answer"`, `from: user`, id `answer-<set>` (so a republish after a crash is dropped by the bus's own dedupe) with `refs.question`; an expiry notice is `kind: "status"` from the hub, never an answer. Supersede keys on `from === USER && kind !== "answer"` in the bus tap, never on the body, and never on the hub's own answer envelope. Question and answer text reach nothing but `hub.db` and the envelope to the asking peer: the tap's log line, `redactBusEvent` and the `question` event all stub them, `QUESTION_TOOLS` skips capture and the progress observer skips `hub_ask`. `hub_ask` holds its control request open (`controlRequestTimeout`, `request_cancel` through the shared `invocationRequests` map, so a closed socket or an aborted turn cancels the row) and Pi's bridge lifts its idle timeout for that one call. A new peer kind gets `questionWaitMs` on its definition (fixed builtins only, `assertNativeCapabilityScope`), never a wait chosen by the caller. The `questions` feature label is a recovery label: an older target ignores the table, so keep it in `DAEMON_FEATURES`. Start repair runs once after `ready`'s locals exist (`questionDesk.repair()` just before `ready = true`), and the sweep timer is pushed only after the desk exists.
|
|
162
|
-
- Completions (#1054 T2/T3, `src/hub/completions.ts`, `src/hub/completions-store.ts`): a run opens at the `turn_start` that consumes a `from: user` delivery (never the hub's own `answer`), only for a peer whose definition declares `settle
|
|
163
|
+
- Completions (#1054 T2/T3, `src/hub/completions.ts`, `src/hub/completions-store.ts`): a run opens at the `turn_start` that consumes a `from: user` delivery (never the hub's own `answer`), only for a peer whose definition declares `settle`. An unattached pull-only peer creates none; a launcher-bound Kimi TUI opens one only after native readback proves a person origin. `settleCheck(peer)` is the ONE settle check and never throws; call it from every place a pending-work fact can change (turn end, `bus.onQueues`, a permission or question closing, an isolated check ending, `Tasks.checkEnded`, a native task notification or a native-background re-read). The bus takes a delivery out of its queue before the peer's idle reaches the state tap, so `queuedContinuations` also counts `dispatching` rows and `deliveredAwaitingTurn` (bounded to 10 s, `ponytail:` in the code); drop either and a continuation reads as nothing pending. Claude settles only on the verified native Stop (a lost channel or watchdog carries no id and never settles). Kimi TUI settles only on correlated final-turn readback plus a known-empty background inventory; a running or unknown inventory and an active native turn stay working. A `limit` or budget pause ends the run with no completion (`budget.record`), an explicit stop calls `quietStop`, and `completions` is a recovery feature label (`hasPending`: an open run or an unread completion). The store keys a completion by the run's first turn id (`UNIQUE`), so repeated restarts yield one `interrupted` completion; never reconstruct from `events.jsonl`, which cannot say who started a turn. Launcher end/socket loss also settles an open run `interrupted`, once. Receipts (#1054 T4/T5): `completion_receipts` has a UNIQUE index on `completion_id`, so `markSeen` is one compare-and-set and a mark-unread has exactly one row to delete; `result_seen` and `results` are console-role control cases (rid always) placed before `case "kill"`, and nothing else writes a receipt: never mark read from a snapshot, a status read, a listing or an envelope. The `result` need is level `watch` and is not in the `waiting`/`blocked` lists in `needs()`; keep it that way so a result never reads as something to act on. Reconstruction drops an open run whose peer has a budget pause record before it would settle it `interrupted` (persist before remove, per run), and `CompletionStore.prune` bounds the table to the newest 500 read completions at start; never prune an unread completion or an open run. The title of a result is computed at display (`resultView`), a stub when private, and no prompt or reply text is ever stored. Native-person results always stay private.
|
|
@@ -23,7 +23,7 @@ Scope: the hub-native local worker, its tools, path guard, denylist and seatbelt
|
|
|
23
23
|
- Hub tool calls (`hub_send`, `TASK_TOOL_NAMES`, `CONDUCTOR_TOOL_NAMES`) are never passed to `capture.observe`; `Tasks` alone decides what hub tool text reaches memory (#230). Every live `capture.observe` passes a host `sourceId` (`<session>.<turn>.<step>.<n>`, #752); a new host caller supplies one too. Capture stores `paths` only for `read`/`write`/`edit` and only project-relative ones: command argv is never path metadata.
|
|
24
24
|
- A tool that asks for approval passes its name to `ctx.permit`, and `write`/`edit` also the `guardPath` result, never the model's path string: also pass the original path separately through every permit wrapper, and every tool its parsed arguments as `input` (the always-ask classifier reads it, #750; rule: `adapters.md`); `ask-when-needed` grants only when both the original project-relative spelling and the real path avoid protected names through `grantablePath` (`src/hub/permission-mode.ts`). `bash` and mutating `git` pass no path. After the awaited approval `write`/`edit` run `guardPath` again and refuse a target that changed (#242).
|
|
25
25
|
|
|
26
|
-
- Honor the live `protect_instruction_files` policy in scoped grants for the
|
|
26
|
+
- Honor the live `protect_instruction_files` policy in scoped grants for the instruction names in `INSTRUCTION_FILES` (`src/hub/permission-mode.ts`: `AGENTS.md`, `CLAUDE.md`, `QWEN.md`, `GEMINI.md`); keep reads, explicit approval and never-ask unchanged. Only the settings registry writes this machine-local policy (#323).
|
|
27
27
|
|
|
28
28
|
- For sandboxed task checks, use the opt-in hub-owned bounded container runner; never widen Seatbelt, expose credentials, mount host paths or fall back to a host command. Keep spec/check arguments and results out of capture.
|
|
29
29
|
- The `bash` tool asks `ctx.permit` with the frozen input, then `ctx.shellExecutionCheck` right before the sandbox spawns (#816): `valid` runs, `ask` repeats the permit once with `{ forceAsk: true }`, `aborted` or a cancelled signal returns "turn cancelled before shell execution". `LocalWorker` binds the check to its turn generation, so a stale turn cannot spawn. A fake `ctx` in a test supplies `shellExecutionCheck` or accepts the `valid` default; never make it return `valid` for an aborted turn.
|
|
@@ -14,3 +14,5 @@ Scope: the model relay and its journal, the hub's own inference and its slots, O
|
|
|
14
14
|
- Load and cooldowns (#199) only reorder a `hub/auto` request's candidates (the route's own backend, then its fallback, as on main), in one ordering step in the relay's fetch handler; a fixed alias is never reordered. Never remove a candidate. A candidate goes first only while it is neither cooling nor `failing` (any failure, an error status or a failed stream included, for `model_relay.cooldown_s` (default 30 s) or until a success; separate from the cooldown streak). Load and pin moves are opt-in (`efficientWaitMs` absent: none) until #199 AC5 measures them. Their attempt is bounded only up to its response headers (`MOVE_FIRST_BYTE_MS`, after `omni.base()`); a cut there marks `failing` without counting toward a cooldown, and a stream that fails after the headers cannot fall back, so do not claim a move never fails a request MLX would have served. None happens while the request runs under an execution budget (`underBudget`, a read that admits nothing; a throw counts as a budget). The selector only names `prefer`; it never picks a moved backend itself. They never consider PII because the relay serves only Pi, which refuses PII work before any model call (`selectModel` in `daemon.ts`); a relay caller that can carry PII must not get a reorder toward DGX without a positive `onCampus()`. Count a cooldown failure only for a transport or startup failure outside a cooldown that was neither cancelled, refused admission, a busy MLX slot (`MlxBusyError`: load, never failure), nor a deadline the relay cut itself (`journalEntry.cut`: `"budget"` for the execution budget's, `"move"` for a moved attempt's first byte, which only marks `failing`); any HTTP answer, whatever its status, ends the streak and the cooldown (`answered`).
|
|
15
15
|
- Relay cooldown policy and journal size (`model_relay.*`, read through `modelOperationalReaders`, #984) are passed only when a relay is constructed, and `inference.backoff_min` only when `Inference` is constructed. Both are restart-bound: a reload never rewrites a running relay's cooldowns or journal, or an existing backoff deadline.
|
|
16
16
|
- MLX owner and generation identity read `ps lstart` with LC_ALL=C/TZ=UTC pinned (#177): unpinned, a record written under one environment reads as a foreign process under another, and the fail-closed paths would refuse the hub's own server.
|
|
17
|
+
|
|
18
|
+
- Optional adec `decision` routes (#554) default off and support shadow only; reject enforce at load with the #554 follow-up and do not keep an application or gateway effort path. Shadow must preserve the recorded stage model, effort and conversation. Send only bounded host-screened evidence after positive campus admission for sensitive turns; sensitive evidence is dimensions only, and remote/review-observer-excluded turns skip the service. Re-read current board/turn privacy and observer facts across awaits and before service HTTP effects and route metadata emission; changed or unavailable authority refuses the stale turn, never a model fallback around that refusal. Keep one opaque user-turn id through the tool loop; let adec recheck its pins and hard rules, rather than caching valid picks in the hub. Validate returned pairs against the route list, retain the heuristic on client failures/no_candidate, and keep evidence, arguments, secrets and service error prose out of route events. Never route PII screening, pure peer assignment or escalation authority through this service.
|
|
@@ -49,7 +49,7 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
|
|
|
49
49
|
|
|
50
50
|
- Memory read tools (`hub_memory_search`, `hub_memory_show`, `hub_memory_graph`, #412) take their project only from the daemon. Validate UTF-8 input caps before logs or storage, require `memory_read`, refuse PII turns and exclude arguments/results from capture. Screen complete rows and graph metadata before counting or truncating; keep whole-result byte caps, complete id/provenance/kind headers and one neutral unavailable-id refusal.
|
|
51
51
|
|
|
52
|
-
- Compose structured `hub_remember` fail fields through `composeRememberText` before screening or writes; reject over 4000 UTF-8 bytes without truncation. Treat plan-fail matches as screened advisory priority only, never a state-transition guard; skip all plan-memory work for PII tasks. A long-term note (`horizon: "long"`, or an approved task's promoted kind) passes `memorySyncPolicy().screen` (`src/hub/memory-sync-policy.ts` plus the hub's PII patterns and private paths) before `store.note`. A decline, or a policy that is unavailable, writes nothing: `hub_remember` refuses pre-effect and names horizon `short`, and a hub-written note becomes a console notice. Never fall back to writing it long-term (#1189).
|
|
52
|
+
- Compose structured `hub_remember` fail fields through `composeRememberText` before screening or writes; reject over 4000 UTF-8 bytes without truncation. Treat plan-fail matches as screened advisory priority only, never a state-transition guard; skip all plan-memory work for PII tasks. A long-term note (`horizon: "long"`, or an approved task's promoted kind) passes `memorySyncPolicy().screen` (`src/hub/memory-sync-policy.ts` plus the hub's PII patterns and private paths) before `store.note`. A decline, or a policy that is unavailable, writes nothing: `hub_remember` refuses pre-effect and names horizon `short`, and a hub-written note becomes a console notice. Never fall back to writing it long-term (#1189). Decide whether `hub_remember` stored anything from `Tasks.rememberOutcome(...).outcome` (`saved`, `candidate`, `not_saved`; #802), never from the reply wording. Store observers (`onNote`, `onGeneratedNote`) run after the note is committed, inside `try`; an observer failure never fails the save or the preceding board write.
|
|
53
53
|
|
|
54
54
|
- Keep candidate production in Tasks: screen review/correction payloads and current task/turn privacy before CandidateStore writes; recheck after asynchronous screening. Never make candidate failures roll back a saved review, or let a peer accept a candidate. Keep candidate tool arguments/results out of capture.
|
|
55
55
|
|
|
@@ -20,10 +20,15 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
|
|
|
20
20
|
- 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).
|
|
21
21
|
- `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.
|
|
22
22
|
- 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.
|
|
23
|
+
- Companion tests (`integrations/orca/`, `integrations/herdr/`) hold awaited navigation and CLI replies, change the selected id, host, kind, root or request generation, then release the reply. Assert that stale navigation creates no terminal and that stale panel success, error, timer and finally callbacks change no current link, status or controls. Run the real panel code against fake host and DOM ports.
|
|
23
24
|
- A bounded polling wait must assert its condition after the last attempt, or document why giving up is intentional. Bound it by time (a deadline with short sleeps), never by a count of event-loop turns (`setImmediate`), which a loaded runner spends before a socket frame or child output arrives (#1163, `untilSeen` in `test/control-client.test.ts`). Append-only event histories wait for a correlated closing event, not removal of the opening event. Before changing a slow wait, identify the unreached state and repair its cause (#296).
|
|
24
25
|
- A test of turn or notice behavior that is not about the snapshot budget yields an I/O turn (`await new Promise(r => setImmediate(r))`) between captures: captures in one synchronous burst share the 250 ms budget and may correctly record unknown evidence. A daemon test whose assertion needs a turn's snapshot or end evidence passes `snapshotClock: () => 0` (`DaemonOptions.snapshotClock`, #661) so a slow runner cannot spend that budget; never lengthen the budget instead (#550). A wait on concurrent-edit evidence correlates the expected turn ids and paths (#617).
|
|
25
26
|
- Holding the snapshot budget clock (`snapshotClock`, #661) holds the deadline and, since #554, the git child's wall-clock timeout (the 10 s ceiling instead of the remaining budget). The rule lives in the pure `childTimeoutMs` (`src/hub/snapshots.ts`: production keeps min(10000, remaining), only a budget carrying `now` gets the ceiling). Reproduce a slow git with a deliberately slow `git` first on PATH (`slowGit` in `test/snapshots.test.ts`), never with host-wide load, and never run a high-parallel loop while another gate shares the dgx02 host.
|
|
26
27
|
- Both OS CI legs use four shards (#548): `check (ubuntu-latest) shard i/4` and `check (macos-latest) shard i/4` runs `AHUB_CHECK_SHARD=i/4 scripts/check.sh` (`bun test --shard --timings=scripts/test-timings.json`), and each stable job name `check (ubuntu-latest)` / `check (macos-latest)` is the aggregator over its OS shards; `scripts/ci-gates.mjs`, branch protection and the reuse proof read that name, so never rename it or turn it back into a matrix job without reading them. `gateJob()` in `scripts/ci-gates.mjs` counts the shard names (`check (ubuntu-latest) shard i/N` and `check (macos-latest) shard i/N`) as gate jobs, so a run with either OS and the seeded shards done and only the other OS shards running reads as a launched full gate and not as unknown scope (`test/ci-reuse.test.ts` pins every mid-run window). A new test file needs no timing entry (it gets the median), but a shard that grows past about 15 minutes means the timings are stale: regenerate them with `scripts/make-test-timings.ts`. A shard starts the suite cold, so `scripts/check.sh` execs the system git once (`AHUB_CHECK_WARM_GIT`, default `/usr/bin/git`) before the tests, because tests that give it `PATH=/usr/bin:/bin` and a 1000 ms timeout (`read-only-shell-proof`, `task-shell-default`) are killed by a cold first exec; a failing or absent warm-up never fails the shard. `check.sh` unsets `AHUB_CHECK_SHARD` after building the test arguments, so the runner and every scratch `check.sh` a test spawns (`test/check-lock.test.ts` copies `process.env`) see an unsharded environment; keep that unset. A test must not depend on running in the same process as another file (shards split files across machines), and anything that has to happen once per gate belongs behind the shard-1 condition in `scripts/check.sh`, which `test/macos-shard.test.ts` pins.
|
|
28
|
+
- CI rerun policy (#1257): a red `check` job gets **one** rerun of the failed jobs on the same head SHA (`gh run rerun <run id> --failed`), never a new commit or an empty push to retry. If the rerun passes, the first failure is a flake: it is not "fixed by the rerun", it goes into the flake ledger below with the failing test named. If the rerun fails too, or the same test failed on an earlier head, treat it as a defect and investigate before any third attempt; a hang (the watchdog stop, a shard at its timeout) is investigated even when the rerun passes. Never edit or skip a test to get green (AGENTS.md). The aggregator names (`check (ubuntu-latest)`, `check (macos-latest)`, `seeded guards`) and the identical-tree reuse proof (`scripts/ci-reuse.mjs`, `scripts/ci-gates.mjs`) are unchanged by a rerun: a rerun is another attempt of the same run, and the proof reads the final state of its jobs.
|
|
29
|
+
- Flake ledger (#1257): the weekly `flake-ledger` workflow (`scripts/flake-ledger.ts`, `permissions`: `contents: read`, `actions: read`, `issues: write`) reads the Actions runs for `check.yml` of the last 28 days and lists the gate shard jobs that failed and then passed on the same head SHA, per test file and failing test name (the `(fail)` lines of the failed job's log, under the `##[group]test/x.test.ts:` header they ran in), split into macOS-only, Ubuntu-only and both-OS classes. It edits one tracking issue titled `CI flake ledger (check.yml)` (found by that title and a body marker, created once, never one issue per run). It is bounded: five run pages, 60 runs, 40 logs, 8 MiB per log, and it says when a bound cut the window. A job that failed with no `(fail)` line (a hang, a crash) is listed under its job name, never dropped. The top entries (the issue ranks by test file, since one broken file fails many tests at once) each need an owner and a deadline: a fix, or a quarantine with its own issue. Read it before calling a failure "just a flake". Run it by hand with `GITHUB_REPOSITORY=<owner/name> bun scripts/flake-ledger.ts --dry-run`; the aggregation (`parseFailures`, `flakyJobs`, `aggregateFlakes`, `pickTrackingIssue`) is pure and pinned by `test/flake-ledger.test.ts`.
|
|
30
|
+
- Shard weights (#1257): `scripts/test-timings.json` is regenerated monthly, or when a shard exceeds its budget by 25 % (budget 15 min, so a shard past 18.75 min; `scripts/lib/ci-shards.ts`). `bun scripts/refresh-test-timings.ts --check` prints the shard times of the newest green `main` run, the 25 % rule and the age of the weights file (exit 1 when a shard is over budget or the file is older than 35 days). `bun scripts/refresh-test-timings.ts [run id]` downloads the four macOS shard logs of that run and rewrites the file through `scripts/make-test-timings.ts` (it takes several logs, a shard log holding only its own files); review the diff and commit it in its own PR, never inside a PR that is squash-merged with unrelated changes. The format is unchanged (`{ "version": 1, "files": { "test/x.test.ts": ms } }`, slowest first), so `bun test --shard --timings` and `scripts/check.sh` read it as before. A file missing from it still runs with the median time, so stale weights are slow, never wrong.
|
|
31
|
+
- A fake-ripwire CodeFacts fixture (`test/memory-code-overlay-684.test.ts`, `test/memory-stale-followups-684.test.ts`, #723, #1261) installs the fake through `installFakeRipwire` (`test/helpers/ripwire-probe.ts`) and builds `CodeFacts` with `trace: probe.trace`. A failure then carries, beside the fake's shim, script and output timings, the product's own time for every git step of `tree()` and every ripwire launch (`CodeFacts` option `trace`, absent in production, read-only, no effect on an answer, bound or cap). Read that report before changing a bound: the two files failed on macOS shard 2/4 five times before the shim warm-up of #1197 and never in 120 attempts after it, and no cause was observed (a 40-way local stress did not reproduce it), so a recurrence is the evidence to read. Never lengthen the 5 s product cap or the 20 s test timeout for it, and never skip the test.
|
|
27
32
|
- 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.
|
|
28
33
|
- 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.
|
|
29
34
|
- 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.
|