@staix/agent-hub 0.20.0 → 0.21.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 +59 -3
- package/README.md +2 -2
- package/docs/agent-notes/README.md +11 -0
- package/docs/agent-notes/adapters.md +7 -3
- package/docs/agent-notes/benchmarks.md +27 -0
- package/docs/agent-notes/budget.md +2 -1
- package/docs/agent-notes/bus.md +2 -2
- package/docs/agent-notes/daemon.md +15 -6
- package/docs/agent-notes/local-worker.md +4 -0
- package/docs/agent-notes/models.md +2 -0
- package/docs/agent-notes/tasks.md +17 -9
- package/docs/agent-notes/tests.md +9 -2
- package/docs/bench.md +1 -1
- package/docs/data-policy.md +2 -0
- package/docs/dogfood.md +37 -4
- package/docs/events.md +2 -1
- package/docs/memory.md +9 -3
- package/docs/operations.md +565 -62
- package/docs/profiles.md +13 -1
- package/docs/quickstart.md +14 -9
- package/docs/research.md +7 -4
- package/docs/sdlc.md +54 -10
- package/docs/security.md +396 -34
- package/docs/smoke.md +127 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +1070 -22
- package/docs/verification/2026-10-27-0.19.0.md +5 -5
- package/docs/verification/2026-10-28-claude-2.1.311-2.1.312-bash-permission.json +635 -0
- package/docs/verification/2026-10-28-claude-2.1.311-2.1.312-permission-transport.json +403 -0
- package/docs/verification/2026-10-28-claude-implementer-permission-triage.md +123 -0
- package/docs/verification/2026-10-28-jcode-0.99.1-usage.md +41 -0
- package/docs/verification/2026-10-28-realpath-name-audit.md +222 -0
- package/docs/verification/2026-10-28-routing-f0bb8ad0.md +1000 -0
- package/docs/verification/2026-10-29-0.21.0.md +61 -0
- package/docs/verification/2026-10-29-model-effort-capabilities.md +123 -0
- package/docs/verification/2026-10-29-modelrouting-b152ddb9.md +953 -0
- package/docs/verification/claude-implementer-permission-triage.ts +160 -0
- package/docs/verification/claude-permission-transport-native.py +71 -14
- package/docs/verification/profiles/YYYY-MM-DD-research.md +1 -1
- package/docs/verified.json +15 -15
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +1016 -280
- package/src/adapters/acp.ts +79 -3
- package/src/adapters/channel-peer.ts +4 -0
- package/src/adapters/claude-channel.ts +13 -6
- package/src/adapters/claude-implementer.ts +60 -7
- package/src/adapters/codex-appserver.ts +77 -4
- package/src/adapters/conformance/fakes/jcode-agent.ts +6 -2
- package/src/adapters/conformance/kit.ts +3 -9
- package/src/adapters/definitions/builtin-metadata.ts +3 -114
- package/src/adapters/definitions/native-config-defaults.ts +1 -1
- package/src/adapters/definitions/native-config-policy.ts +3 -2
- package/src/adapters/definitions/native-context.ts +12 -0
- package/src/adapters/external-metadata.ts +48 -0
- package/src/adapters/external-modules.ts +63 -3
- package/src/adapters/jcode.ts +6 -2
- package/src/adapters/local-worker.ts +40 -25
- package/src/adapters/native-proof-ports.ts +10 -0
- package/src/adapters/registry-types.ts +4 -0
- package/src/broker/protocol.ts +203 -0
- package/src/broker/server.ts +314 -0
- package/src/broker/store.ts +548 -0
- package/src/cli/bench.ts +3 -2
- package/src/cli/broker.ts +58 -0
- package/src/cli/conductor-recovery.ts +142 -0
- package/src/cli/configured-acp-entry.ts +26 -6
- package/src/cli/console-state.ts +59 -17
- package/src/cli/console.ts +17 -2
- package/src/cli/facts-hook.ts +3 -1
- package/src/cli/help.ts +28 -16
- package/src/cli/identity.ts +23 -10
- package/src/cli/init.ts +34 -7
- package/src/cli/intake-output.ts +33 -0
- package/src/cli/main.ts +314 -71
- package/src/cli/output.ts +41 -1
- package/src/cli/recovery-package.ts +18 -1
- package/src/cli/role-holders.ts +35 -0
- package/src/cli/room.ts +304 -0
- package/src/cli/say-input.ts +16 -0
- package/src/cli/schedule-output.ts +49 -0
- package/src/cli/sdlc-output.ts +44 -6
- package/src/cli/smoke-pty-bridge.ts +33 -0
- package/src/cli/smoke-pty.ts +145 -0
- package/src/cli/smoke-run.ts +139 -0
- package/src/cli/smoke-target.ts +130 -0
- package/src/cli/source-output.ts +29 -0
- package/src/cli/task-output.ts +3 -2
- package/src/cli/upgrade-runtime.ts +98 -16
- package/src/cli/upgrade.ts +66 -7
- package/src/hub/ask.ts +76 -12
- package/src/hub/board.ts +132 -22
- package/src/hub/bus.ts +399 -56
- package/src/hub/child-process.ts +2 -0
- package/src/hub/ci-watch.ts +42 -20
- package/src/hub/claude-failure-stage.ts +85 -0
- package/src/hub/claude-shell-wrapper.ts +11 -5
- package/src/hub/conductor.ts +17 -4
- package/src/hub/config-apply.ts +32 -5
- package/src/hub/config-reload.ts +7 -5
- package/src/hub/config-trust.ts +7 -1
- package/src/hub/context-window.ts +33 -3
- package/src/hub/control-client.ts +14 -2
- package/src/hub/corpus-config.ts +34 -0
- package/src/hub/corpus-evidence.ts +184 -0
- package/src/hub/corpus.ts +373 -0
- package/src/hub/cost-governance.ts +26 -0
- package/src/hub/daemon.ts +1150 -171
- package/src/hub/deliverable-files.ts +8 -2
- package/src/hub/delivery-journal.ts +123 -17
- package/src/hub/dogfood-profile.ts +32 -5
- package/src/hub/dogfood-read.ts +6 -2
- package/src/hub/envelope.ts +11 -0
- package/src/hub/eval-gate.ts +131 -0
- package/src/hub/events.ts +1 -1
- package/src/hub/external-instance.ts +69 -0
- package/src/hub/facts.ts +15 -5
- package/src/hub/folder-trigger.ts +223 -0
- package/src/hub/hub-tools.ts +2 -1
- package/src/hub/inference.ts +20 -5
- package/src/hub/instance-config.ts +69 -3
- package/src/hub/instance-execution.ts +6 -2
- package/src/hub/instances.ts +32 -4
- package/src/hub/intake.ts +125 -0
- package/src/hub/learning.ts +85 -0
- package/src/hub/lifecycle.ts +7 -3
- package/src/hub/limits.ts +12 -5
- package/src/hub/native-permission-request.ts +77 -0
- package/src/hub/operational-authority.ts +63 -5
- package/src/hub/operational-settings.ts +76 -0
- package/src/hub/operational-view.ts +1 -1
- package/src/hub/packs.ts +132 -0
- package/src/hub/path-policy.ts +7 -0
- package/src/hub/peer-doing.ts +130 -0
- package/src/hub/peers.ts +12 -0
- package/src/hub/queue-backpressure.ts +58 -0
- package/src/hub/read-tools.ts +2 -0
- package/src/hub/recovery-authority.ts +151 -0
- package/src/hub/recovery-policy.ts +220 -0
- package/src/hub/research.ts +24 -14
- package/src/hub/restart.ts +7 -0
- package/src/hub/routing.ts +54 -11
- package/src/hub/schedules.ts +266 -0
- package/src/hub/sdlc-merge.ts +38 -14
- package/src/hub/sdlc-metrics.ts +32 -10
- package/src/hub/sdlc-queue.ts +7 -1
- package/src/hub/sdlc-release.ts +1 -1
- package/src/hub/sdlc.ts +37 -7
- package/src/hub/security-baseline.ts +38 -0
- package/src/hub/settings.ts +6 -1
- package/src/hub/shipped-packs.ts +8 -0
- package/src/hub/snapshots.ts +72 -6
- package/src/hub/source-read.ts +160 -0
- package/src/hub/sources.ts +311 -0
- package/src/hub/tasks.ts +910 -110
- package/src/hub/workflow.ts +1 -1
- package/src/local/doc-read.ts +69 -0
- package/src/local/proxy.ts +4 -13
- package/src/local/tools.ts +37 -3
- package/src/local/web-fetch-config.ts +43 -0
- package/src/local/web-fetch.ts +183 -0
- package/src/memory/brief.ts +85 -13
- package/src/memory/capture.ts +1 -1
- package/src/rooms/application.ts +114 -0
- package/src/rooms/boundary.ts +103 -0
- package/src/rooms/commands.ts +294 -0
- package/src/rooms/crypto.ts +371 -0
- package/src/rooms/link.ts +584 -0
- package/src/rooms/policy.ts +82 -0
- package/src/rooms/remote-peer.ts +80 -0
- package/src/rooms/service.ts +426 -0
- package/src/rooms/status.ts +146 -0
- package/src/rooms/store.ts +1210 -0
- package/src/rooms/wire.ts +465 -0
- package/src/ui/index.html +91 -4
- package/templates/packs/example.pack.json +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,65 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
Issue and pull request numbers in the entries for 0.7.7 and earlier refer to the previous repository, archived on 2026-09-30 when this repository's history was rewritten; the one exception is the open smoke-check issue, formerly #12, which moved here as #1. Numbers in newer entries refer to this repository.
|
|
3
|
+
Issue and pull request numbers in the entries for 0.7.7 and earlier refer to the previous repository, archived on 2026-09-30 when this repository's history was rewritten; the one exception is the open smoke-check issue, formerly #12, which moved here as #1. Numbers in newer entries refer to this repository. In newer entries and the docs, `#N` is only an issue or pull request of this repository; a hub board task is written `task N`, without `#` (#879).
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.21.0
|
|
8
|
+
|
|
9
|
+
- Tasks and routing operational settings (#489 H2, task 441): six more numbers use the H1 mechanism (fixed rows in `src/hub/operational-settings.ts`, the closed `config-apply.ts` schema, the settings table), all live and with the default equal to the constant they replace: `tasks.demote_at` (1.5), `tasks.demotion_half_life_h` (24), `tasks.contradiction_window_days` (7), `tasks.rescreen_max` (10), `tasks.split_min_observations` (5) and `tasks.split_max_failure_share` (0.3). Each is read where its constant was used, so a change applies at the next demotion read, verdict, re-screen or prediction without a restart, and unset behaves exactly as before. The split thresholds only feed the shadow prediction, which never changes assignment. Nothing here is a safety floor or a contract. `docs/operations.md` lists the other 35 H0 operational items of `tasks.ts`, `routing.ts` and `task-sweep.ts` that stay as they are, and why.
|
|
10
|
+
- Operational numeric settings (#489 H1, #966): one fixed descriptor per setting supplies the bounds, the apply class, the console and dashboard row and the default, through the existing settings path. The worked example is `tasks.groom_fyi_window_min` (1 to 1440 minutes, default 10, live at the next planner notice); unset is the old constant. No new config file, env knob or per-call override, and no conductor authority.
|
|
11
|
+
- `hub_send` recipients (#963, #967): a single peer id given as a string reaches only that peer instead of broadcasting. Any other malformed `to` (null, an empty string, a number, a mixed array) is refused with `not sent: to must be an array of peer ids` before anything is published. An omitted `to` or an empty array still broadcasts, and a reply still inherits its sender.
|
|
12
|
+
- The agent-compat workflow selects its tracking issue by its exact title, never by the label alone (#964, #969), so it can no longer overwrite or close another issue that carries the `agent-compat` label. Two matching issues, malformed input or a truncated issue list fail the step without writing.
|
|
13
|
+
- Status no longer runs git on every write while a terminal Claude instance is bound (#525-5, task 400, #971): the status and stall views read the cached execution binding, and every effect (routing admission, the facts reply, recovery, starts) still revalidates it freshly.
|
|
14
|
+
- Model-routing promotion gate (#637 part 2, task 324): `scripts/benchmarks/model-routing-corpus.json` (44 cases, two splits) with `model-routing-gate.ts` (`--receipt`, `--freeze`, and a by-hand `--live`) and `scripts/check-model-routing-receipt.ts`, run as a second step of the `routing-receipt` workflow. A PR that changes the model-routing keys of `templates/routing.toml` (`hub_routes`, `local`, `fixed_model`, `stay_switch`, the `[pi]` windows) needs a `docs/verification/*-modelrouting-*.md` receipt that CI rebuilds from the corpus and the two files. The corpus drives the real `HubRouteRuntime` with a scripted host, so no model runs in CI; the judge cases and the served model ids come only from the live receipt, which CI checks for shape and consistency (it cannot check that a model said it). No live gateway call has been made yet: the baseline receipt says `live-not-run`.
|
|
15
|
+
- Test timing (#559, follow-up to #548): three tests that tipped over on hosted macOS shards no longer depend on runner speed. The #541 hardlink loops in `test/path-policy.test.ts` are five parts of 1000 iterations per layout (5000 per layout, as before) instead of one test of 15000; the code-facts deadline test in `test/code-facts.test.ts` holds the deadline timers and fires the analysis's only after its child marker exists (a barrier, with the real deadline code), so the descendant exists first whatever the runner's speed; the PTY tests in `test/smoke-pty.test.ts` whose property is not the timeout run once more when the macOS bridge timed out with no output at all and its first attempt's cleanup was confirmed (a measured hang of the bridge's `Bun.spawn` with a terminal under load, local macOS only, not fixed here, so a hosted run can still fail twice in a row). No product change.
|
|
16
|
+
- Conductor-bound recovery continuation and task unassign (task 535, #900): `restart --yes`, `upgrade --yes --to <exact-version>` and `recovery resume|abort` are limited to a source-issued, same-actor, project/target/policy-bound continuation; unsupported coordinator handoff remains person-only. The proof is separate from public recovery JSON, rechecked before effects and does not grant general console commands. `task unassign` retains the conductor actor and manual-offer suppression. Fixture coverage does not establish current installed or real-account behavior.
|
|
17
|
+
- Pin `@staix/agent-mem` 0.13.0 (was 0.12.0). The hub uses the library API; amem 0.13.0's CLI behaviour change (attributed agent and non-TTY sessions, readable default output with `--json` byte-identical) does not change hub recall, capture or briefs.
|
|
18
|
+
- `ahub down` stops this project's hub as the counterpart of `ahub up`. It is the same verified graceful stop as `ahub kill` (owned peers stopped, manifest and registry claim cleared) and stays person-only from an agent shell.
|
|
19
|
+
- The person keeps only trust, money, attribution and interactive terminals in an agent shell (#933, a minor-version change): `permission`, `agents` beyond `--json`, `role assign`, `budget` changes, every `--as-user` form, a launcher's person-only flags, `pet --window`, `watch`, `recovery reattach` and unknown commands are still refused with the same cause line. The configured conductor may now run `board import`, `intake approve`, `schedule run|pause|resume`, `research backfill`, `bench run` (its tasks are proposed as the conductor), `source add`, `goal link`, start the Jcode and Antigravity (`ahub agy`) launchers as `ahub up` already could, and set `tasks.escalate_after`; the hub rechecks the role and records one `agent_cli` row for each (a usage error is refused before that row), and refuses another agent with the operational refusal. Memory changes and the other goal changes stay the person's, because the pinned amem records their actor as the user; `upgrade --yes` keeps the prior person-only rule except for #900's exact source-issued, same-conductor continuation when supported. `tasks.wip_limits.*`, `tasks.auto_pull`, `tasks.queue_backpressure.*` and the routing keys stay the person's. Any agent may `task cancel` its own task as `hub_task_cancel` allows, `task role` (not while it is in review) and `task label` a task it owns (its research label names it, and a person's label for the same task always wins), each refused by the hub for another peer's task, and list `ahub deliverables` for the tasks `hub_task_show` would show it. `route` has no form beyond `explain`, so nothing changed there; restart and `task unassign` are #900.
|
|
20
|
+
- Test fix (#554): a daemon test that holds the snapshot budget clock (`snapshotClock`, #661) still had every git child killed at the remaining budget (250 ms) by `spawnSync`, so a loaded runner timed git out and the owner-write evidence came back unknown (about 3 of 100 runs at 100 parallel in `owner-write-evidence-daemon`). With a host-held clock the child now gets the 10 s ceiling; production, which never sets that clock, keeps the budget-bound timeout and unknown stays unknown.
|
|
21
|
+
- CI (#548): the macOS check leg runs as four shards of the full suite (`AHUB_CHECK_SHARD=i/n` in `scripts/check.sh`, balanced by `scripts/test-timings.json` through `bun test --shard --timings`), with the docs, typecheck, role, bundle and package steps on shard 1 only, so no shard approaches the hang backstop; the macOS backstop returns from 2400 s to the Linux 1500 s. The required name `check (macos-latest)` is now an aggregator over the shards, like `seeded guards`. `bun scripts/make-test-timings.ts <log>` regenerates the timings from a leg log. A shard execs the system git once before its tests (`AHUB_CHECK_WARM_GIT`), because a cold hosted macOS runner's first `/usr/bin/git` execs outlast the 1000 ms some tests allow it.
|
|
22
|
+
- Realpath-name audit (#552, the #541 class): Bun's realpath can name another hardlink of the same file on macOS APFS, so three more callers that decided from the resolved name alone now judge the typed name as well, adding refusals only: `Facts.rel` (secrets denylist, `.git` and hub-owned names), the deliverable file check (hub-owned names, `local.deny`) and `ahub source add` (`pii.paths`, through `screenRegistration`'s new `typedPath`). The typed name is checked when it lies inside the project, by a segment test (`insideRoot` in `src/hub/path-policy.ts`) rather than a string prefix, so a directory called `..x` is judged too. In a shadow repository `Facts.rel` also asks the ignore policy about the typed name, so a path through a symlinked directory is no longer named by that spelling (git cannot judge it); its real path still is. The audit of every `realPath` call site (170 rows) is `docs/verification/2026-10-28-realpath-name-audit.md`.
|
|
23
|
+
- Test fix (#553): the daemon web_fetch test made a real round trip to iana.org through the real proxy and waited 4 s for it, so it failed about 3 of 100 runs at 30 parallel. It now runs against a fixture transport (`DaemonOptions.webFetchTransport`, a host-only test seam like `configWatch`), so it needs no network and no wait. No change to the tool or the proxy.
|
|
24
|
+
- Domain pack format (#513a): `.agenthub/roles/<name>.pack.json` bundles a base profile, the catalog roles it expects, a review rubric whose ids are #507 criterion ids and display-only example checks. `ahub init --profile <profile|pack>` previews what a pack adds (ids only, never text) and applies it as `profile_overrides.review_checklist` and `example_checks`; the same pack again is a no-op, and a different profile or different existing overrides are refused. A pack is data: no field selects or runs a command. A shipped layer (`templates/packs/`, only the format example `example` until #513b adds the writing, analysis and ops packs) loads under the project layer, and a project pack of the same name replaces it. Existing `profile_overrides` that hold the same data in another key order count as already applied. Init does not copy role files and a check is a note, not a command; the spec amendment "domain packs (#513a)" records why.
|
|
25
|
+
- Recurring unmet criteria become a candidate (#514a): reviews that name the same acceptance-criterion id unmet on at least 3 tasks of one profile and class in 30 days that make up at least 2 separate pieces of work (tasks sharing a PR or branch count once) file one backlog card for a person, with the `profile_overrides.review_checklist` line as text (`src/hub/learning.ts`, reusing the #481 S3 recurrence path). The hub never applies it; `profile_overrides` is not machine-local, so the card names both config layers. PII tasks never count.
|
|
26
|
+
- Claim verdicts and fact_check provenance for the research scorecard (#901, follow-up to #799): `ahub task claim <id> <slot> <supported|unsupported|unclear> [--model <label>] [--model-said <...>]` (console only) records a person's verdict on one declared plan claim by its slot, never its text; the hub stamps the `checks` key that ran (and the example citation check's missing count, from its exact last line) on its own `check passed` and `check failed` entries. The research rows for claims reaching a verdict, model label, person verdict, agreement and `fact_check` selection, pass and fail and missing keys per run now fill from those records, and stay unknown, never zero, without them (an older keyless check entry makes the check rows unknown for its group). A PII task counts its verdicts and names no label. Each verdict is bound to a hash of the claim list it evaluated, so replacing or reordering the claims leaves it naming no claim of the new list, and recording one never discards a running check.
|
|
27
|
+
- Local worker path guard (#541): `guardPath` now judges the secrets denylist and the hub-owned names (`.git`, `.agenthub`) on the name the caller typed as well as the resolved one, because Bun's realpath can name another hardlink of the same file on macOS APFS. It only adds refusals, and only for a typed name inside the project (`insideRoot`, #555; a name outside it that links in is judged by its resolved name, as before #541); the flaky `hardlink aliases (#259)` assertion now compares file identity.
|
|
28
|
+
- Dirty submodules are named, not hidden (#798 steps 2 to 5): a turn that leaves a git submodule dirty lists it as `changed (contents unobserved)` in `ahub turns` (`unobserved` in `--json`), a task that declares a path inside a submodule gets a notice at propose and accept (never a refusal), and `ahub undo` says the submodule's contents cannot be restored. A project without `.gitmodules` makes no extra git call; with one, a turn end adds one bounded `git status` probe. A probe that cannot answer records nothing: unknown, never none.
|
|
29
|
+
- Jcode usage and context readings (#801, 0.99.1 only): a Jcode peer's ACP `usage_update` `{used, size}` is now a source-labelled context reading (`jcode_usage_update`) in `ahub status`, the console and the dashboard, and each prompt's result `usage` is recorded as one `jcode_acp` usage event with the provider's own input, output and cache buckets (never Jcode's `totalTokens`, which counts the cached read twice for OpenAI-style providers). Neither feeds a token window or a quota window; a malformed, wrong-session or stale value stays unknown. Jcode still has no native quota reader (`ahub budget` says `no reading`): its `usage --json` gives no window length. Receipt: `docs/verification/2026-10-28-jcode-0.99.1-usage.md`.
|
|
30
|
+
- Pin @staix/agent-mem 0.12.0 (readable `amem doctor`, `status` and `setup` output with next actions; Codex and Qwen setup fixes). No storage or API change for the hub.
|
|
31
|
+
- Eval gate for agent-configuration PRs (#481 S5, mechanism only): `eval_check` and `config_paths` in `.agenthub/sdlc.toml`. A PR touching a configured path (default AGENTS.md, `docs/agent-notes/**`, `templates/**`, role texts, routing.toml, hub tool definitions) requires the named check on every sdlc readback and the guarded merge, and the approval note records its pass rate and suite version. No suite is chosen or defaulted (suite choice pending the person); with no `eval_check` the gate only reports it (a note, never a blocker); code-only PRs and off mode are unchanged.
|
|
32
|
+
- SDLC metrics remainder (#442, #481 S7 and AC12): the guarded merge records the merged PR's review and CI figures once (`cost` on the merge record: CI runs, same-head reruns, GitHub reviews, merge time), `ahub sdlc metrics` shows them with an honest unknown for anything not read, `ahub sdlc release` shows release interval, approval-to-release lead time and hotfix rate, and two new bands, `hotfix_rate` and `security_open_alerts`, use the existing one-card-per-episode path. An opt-in `[security_scan]` table records GitHub's open-alert counts on a schedule (counts only, a source it cannot read is unknown, never zero). The per-release gates-run record stays unbuilt.
|
|
33
|
+
- `ahub source add <path|url>` and `ahub source list` (#508, person-only): register an input as a content-hashed source. The id is `src:` plus the first 16 hex digits of the SHA-256, so the same bytes are one source however often they are added; the snapshot (`meta.json`, `raw`, `text`) lives under the hub state dir `sources/`. A URL is fetched once by the CLI, https only, at most 5 redirects, up to 16 MiB; nothing is truncated. A path under `pii.paths` or text matching `signals.pii_patterns` registers private, and privacy only ever goes up. The model PII screen, `hub_source_read`, plan validation and the dashboard list are later slices.
|
|
34
|
+
- Machine-local schedules (#512 AC1 and AC3): `schedules` in `.agenthub/config.local.json` (entries `{id, every, class, title, detail?, sources?, owner?, queue?}`, `every` as `<n>m|h|d` with a 15 minute minimum, `cron` refused with "cron not supported yet; use every"). Due runs are checked on their own timer, which runs with `task_sweep` off, and propose through the normal task flow: at most one task per schedule per tick, none while its previous task is open, and a held hub skips and coalesces. Last and next run times live in `hub.db` and show in `ahub status`. A committed config cannot set it, and a settings write cannot either.
|
|
35
|
+
- `ahub schedule list|run <id>|pause <id>|resume <id>` and each schedule's next run on the dashboard (#512 AC4 and AC3 UI, person-only): `list` shows every schedule with its next run, state, last run and task (never a title or detail); `run` proposes one now through the same hold, open-task and PII path as the timer, under the timer's single pass guard, and restarts its period; `pause` stops the timer proposing it without counting skipped ticks and `resume` starts a fresh period so a pause is never replayed as a run (a `paused` column on `schedule_runs`, older rows unpaused). `ahub status` shows a paused schedule with the command to resume it, and the Agents view of the dashboard has a read-only Schedules table with a next-run column. Folder triggers are a later slice.
|
|
36
|
+
- `ahub intake "<text>" [--source <id>...]`, `approve <id>` and `discard <id>` (#511 AC1 CLI and AC2 to AC4, person-only): a plain request becomes a draft backlog card (`draft: true`) that nothing offers or pulls. The configured planner is asked to split it with `hub_backlog_split`, or the hub model proposes the split, validated against the same schema and the #507 criteria format; invalid output leaves the draft whole with the reason. Children inherit the mark, a PII request never goes to a planner and is split only by an on-prem model, approval clears the mark on the draft and its parts in one transaction, and discard cancels them through `task cancel`. The dashboard text box is a later slice.
|
|
37
|
+
- Folder triggers (#512 AC2, machine-local): `folder_triggers` in `.agenthub/config.local.json` (entries `{id, folder, glob?, class, title, detail?, owner?, queue?}`) poll a folder on the schedules timer, never watch it. The folder must lie inside the project or a declared `data.readonly` folder (by real path), and a vault, a denylisted or a `pii.paths` folder is refused with a reason. A trigger's baseline (what is there when it starts, over several polls for a big folder) only records; after that a file whose SHA-256 the trigger has not seen is screened like `ahub source add`, registered as a source (private when flagged) and proposes one task that names the source id and never the file's name. Renames and touches propose nothing, symlinks are never read, at most 20 files per poll, and nothing is read while the hub is held. Never editable from settings and ignored in a committed config.
|
|
38
|
+
- `web_fetch` for the local worker (#515 AC1 and AC3, off by default): `local.web_fetch: { "allow": ["docs.example.org"] }` in `.agenthub/config.local.json` lets the on-prem worker read web pages as plain text. It is refused for the whole turn of a PII task, whatever the host, because the URL or a query could carry PII to the web. Otherwise it runs only through its own egress proxy (never the shell's), to the listed hosts over https, as a bare GET with no header or body from the model, re-checking every redirect, and returns bounded text through `sanitize()`; the result and the URL are never captured to memory. A committed config cannot set it and settings cannot change it.
|
|
39
|
+
- `DOING` per peer (#878 P1+P2): `ahub status`, the console Peers table, the dashboard and `hub_status` show what a peer is working on, derived from signals the hub already receives: a Claude session's `ai-title` (read from its transcript where the last read stopped, at most 1 MiB per read) and the in-progress `TodoWrite` or `TaskUpdate` item from its PreToolUse hook; a Codex thread's name and its plan's in-progress step; an ACP plan's in-progress entry. The current item wins over the title, cut to 120 characters. A peer with no source shows nothing. The value passes the public metadata screen and is a `[private]` stub while the peer owns or reviews a PII task, in a private turn, when it was observed in either, or when the screen refuses or fails. It lives in daemon memory only: not in `status.json`, logs, events or memory capture. No agent tool or reporting is added.
|
|
40
|
+
- Jcode peers are told their hub tools are deferred MCP tools (#912): a Jcode session's first prompt now starts with guidance to search for and load `hub_send`, `hub_task_accept`, `hub_task_done`, `hub_review` and the rest from the `agent-hub-jcode` server before deciding a tool is missing, and never to report the hub unavailable unsearched. It is written in one place (`JCODE_DEFERRED_TOOLS_INSTRUCTION`) and sent once per session to the base peer and every extra instance; other peers' prompts, the AGENTS block and the profile templates are unchanged, and Jcode's own deferral and cache settings are not touched.
|
|
41
|
+
- Knowledge corpus config and index (#510 AC1, library only): `corpus.paths` in `.agenthub/config.local.json` names read-only folders of plain notes. A folder that is, lies in or contains an Obsidian vault (a `.obsidian/` entry, checked on the real path, in every ancestor and anywhere below) is refused with a reason naming the Obsidian MCP tools; `pii.paths`, the deny rules and `signals.pii_patterns` apply first, and a root that holds a protected folder (never listed, so not provably vault-free) or any unreadable evidence is refused. The index parses frontmatter, headings and wikilinks, is bounded and incremental, stores no note text and writes nothing. Nothing reads it yet: ask and brief citations are the next slice.
|
|
42
|
+
- Task-routing promotion gate (#637 part 1): a frozen corpus of 47 board-shaped cases with a train/test split and a content hash (`scripts/benchmarks/routing-corpus.json`), a gate script that runs the pure `assign()` and writes a receipt to `docs/verification/` (`bun scripts/benchmarks/routing-gate.ts`), and a CI check (`routing-receipt`) that fails a PR changing `templates/routing.toml` without a receipt that names the file, the current default and the corpus hash, which it rebuilds whole from the corpus and the two files (configurations, changed keys, every metric, every case and the rendered text), so no field of a receipt can be edited. The promotion rule (not worse on owner top-1 accuracy, no PII or refusal regression) is in `docs/agent-notes/benchmarks.md`. Model routing is part 2. The same job publishes a `routing-eval` check run carrying `eval_pass_rate=` and `eval_suite=` markers, so the person can name it as `eval_check` in `.agenthub/sdlc.toml` (#481 S5).
|
|
43
|
+
- `doc_read` for the local worker (#515 AC2 and AC3, off by default): `local.doc_read: true` in `.agenthub/config.local.json` lets the on-prem worker read a PDF, DOCX or HWPX document through the project's `text_projection` converters, inside the projection sandbox and bounded. The shared path guard, `pii.paths` (by path and by text patterns: an ordinary turn is refused, a PII task turn may read), and `sanitize()` apply, and the text is never captured to memory. A committed config cannot set it and settings cannot change it. `web_fetch` is a separate task.
|
|
44
|
+
- Hub board task ids in the 0.19.0 entries, the 0.19.0 verification report and the user documents are written `task N` instead of `#N`, so GitHub no longer links them to unrelated issues (#879).
|
|
45
|
+
- `ahub sdlc merge <task> [--dry-run]` and the conductor's `ahub doctor --orphans --kill` print for a person by default (#899, the last two sites of #814): the merge shows the mode, the decision, every gate that refused it, the `gh` command and GitHub's readback after a run; the orphan cleanup shows the pids the hub stopped and whether any candidate stayed unconfirmed. `--json` prints exactly what each printed before, and the exit codes are unchanged. The print guard's not-yet-converted list is empty.
|
|
46
|
+
- Rooms across machines (#446 X1 to X5b), all person-run and off until a room exists:
|
|
47
|
+
- authenticated room crypto and wire primitives (#908);
|
|
48
|
+
- a blind broker, `ahub broker serve`, that relays room ciphertext with bounded queues, plus `ahub broker room drop` (#910);
|
|
49
|
+
- outbound membership with out-of-band enrollment and signed presence, through `ahub room create|invite|join|leave|revoke|expose` (#922);
|
|
50
|
+
- screened chat between native peers across rooms, with durable signed receipts (#941);
|
|
51
|
+
- room metadata in the console (panel `8`) and the dashboard (#956);
|
|
52
|
+
- a durable person-only inbox for remote replies to `user@label`, through `ahub room inbox` (#959).
|
|
53
|
+
- Opt-in queue backpressure for automatic task offers (#800): `tasks.queue_backpressure.max_queued` and `.max_oldest_s`, both machine-local and 0 (off) by default (#927). Held offers resume after an admission disconnect (#951).
|
|
54
|
+
- Opt-in context-headroom admission (#802): `context.max_assignment_used`, machine-local and 0 (off) by default. New automatic owner offers skip a peer whose fresh current-session context occupancy is at or above it; unknown occupancy never blocks (#961).
|
|
55
|
+
- Codex planner-only instances must confirm a read-only sandbox, and task show, snapshots, the console and the dashboard name the stage holders with their reasons (#905).
|
|
56
|
+
- External module-kind instances are bound to host proof: the module pin, the catalog, the actual owned spawn, the worktree and the MCP identity (#754, #894).
|
|
57
|
+
- Failed owned Claude turns record a bounded host-observed failure stage, and PII withholds it (#950).
|
|
58
|
+
- Merge queue: a narrower input fence, a recovery recheck at dispatch, and the draft blocker pattern now matches (#786, #928).
|
|
59
|
+
- The legacy builtin adapter catalog is retired, with no behaviour change (#625, #903).
|
|
60
|
+
- Docs: native model and effort control capabilities verified per vendor (#266 AC1, #952); headless Claude permission transport characterized (task 544, #942).
|
|
61
|
+
- Test fixes for flakes under load: #896, #898, #939, #940, #946, #947.
|
|
62
|
+
|
|
7
63
|
## 0.20.0
|
|
8
64
|
|
|
9
65
|
- Pin @staix/agent-mem 0.11.0 (ranked search explanations, dated eval sets, and import and consolidation boundaries: Claude Code project-directory encoding, accepted-only relation targets, consolidation review receipts that survive pruning). Every amem that shares a memory home must run 0.11.0 or later to keep those receipts, so upgrade a global `amem` CLI too. Refs agent-mem#13.
|
|
@@ -29,10 +85,10 @@ Issue and pull request numbers in the entries for 0.7.7 and earlier refer to the
|
|
|
29
85
|
- Qwen dual output: Qwen's own background-task notice is not the person's prompt and ends no hub turn (#861).
|
|
30
86
|
- Conductor operations keep their own error and are audited as refused only on an authority refusal; safe history is cached (#812, #839).
|
|
31
87
|
- CI admission retries a transient GitHub 5xx or 429 twice inside its deadline and stays fail-closed otherwise (#872, #873); check-runner receipt saves are serialized per run (#867, #868).
|
|
32
|
-
- The suite watchdog stops a run on lost progress, not elapsed time (
|
|
88
|
+
- The suite watchdog stops a run on lost progress, not elapsed time (task 516): `scripts/progress-tee.sh` forwards bun's output and appends one byte per test result line or file header to a progress file, and `scripts/hang-watch.sh` kills a suite whose progress file has not grown for `AHUB_CHECK_QUIET_S` seconds (300 by default), printing the process table below it first. `AHUB_CHECK_HANG_S` stays as a backstop (1500 s on Linux and 1800 s on macOS in CI, with 35 and 40 minute job limits). A slow suite that keeps passing tests is no longer cut at the total bound.
|
|
33
89
|
- Pin @staix/agent-mem 0.10.0 (standalone MCP screening/ranking and capture-worker prefetch improvements; unchanged hub policy and storage contracts). Refs agent-mem#13.
|
|
34
90
|
- Pin @staix/agent-mem 0.9.1 (split-home sync holds a verified foreign-generation prune as "held, not applied" instead of refusing the sync, cached MCP policy screening, `--capture-days` claude-mem capture import; 0.9.0 was never published). Refs agent-mem#13.
|
|
35
|
-
- Tests and docs: the snapshot clock is held in the projection-surface daemons (
|
|
91
|
+
- Tests and docs: the snapshot clock is held in the projection-surface daemons (task 515, #854), benchmark Python bytecode is ignored (#840), and the cut-payload guard is stated as refusing session-wide grants (#843, #860).
|
|
36
92
|
|
|
37
93
|
## 0.18.0
|
|
38
94
|
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Code, Codex, Kimi Code, Pi and a hub-owned local worker (plus Qwen Code, Jcode a
|
|
|
5
5
|
Antigravity if you want them) share a message bus, a task board and a memory, each
|
|
6
6
|
connected through its own native interface.
|
|
7
7
|
|
|
8
|
-
Status: 0.
|
|
8
|
+
Status: 0.21.0, control protocol 16. Bun + TypeScript, one loopback daemon per
|
|
9
9
|
project. Site: [agent-hub.staix.net](https://agent-hub.staix.net).
|
|
10
10
|
|
|
11
11
|
## Quick start
|
|
@@ -94,7 +94,7 @@ uses the home must run a version that understands the new layout.
|
|
|
94
94
|
[#258](https://github.com/STAIxBWLB/agent-hub/issues/258)).
|
|
95
95
|
|
|
96
96
|
Live checks that still need a real account or a person are listed in the
|
|
97
|
-
[release verification report](docs/verification/2026-10-
|
|
97
|
+
[release verification report](docs/verification/2026-10-29-0.21.0.md) and the
|
|
98
98
|
[smoke checklist](docs/smoke.md).
|
|
99
99
|
|
|
100
100
|
## Documentation
|
|
@@ -54,3 +54,14 @@ Background behind the 0.19.0 rules:
|
|
|
54
54
|
- Code-scope verdicts (#852, tasks.md): two review rounds showed that any classifier reading a `code` reviewer's unmet text files real defects, such as "the CI workflow is missing the trigger", as verification-only, so the evidence-only rule reads only the structured findings.
|
|
55
55
|
- Policy-table fixtures (#843, tests.md): a fixture with a fixed list of keys or versions, which a planned `COMPAT` entry was not in, was reported twice in #843.
|
|
56
56
|
- Suite watchdog (#516, tests.md): a total-time kill cut suites that were still passing tests line by line (hosted ubuntu runs at 1080 s, the macOS leg of PR #857 at 1440 s), so a lost-progress bound became the hang detector and the total bound only its backstop.
|
|
57
|
+
|
|
58
|
+
Background moved out of the area notes for 0.21.0:
|
|
59
|
+
|
|
60
|
+
- Cold shim exec (#519, tests.md): the first exec of a newly written executable cost about 250 ms on macOS against 6 to 8 ms afterwards, the whole of a 250 ms product bound; `test/memory-code-head-684.test.ts` failed 24 of 30 isolated runs until it warmed the shim. The real probe with a plain `git` measured 10 ms at worst over 600 calls under CPU load.
|
|
61
|
+
- Held snapshot clock (#550, #554, tests.md): `test/settings-reload-daemon.test.ts` without `snapshotClock` failed 138 of 150 single-test runs at 50 parallel, 0 of 150 with it. Before #554 a held clock still left each git child 250 ms, so a loaded runner turned a slow git into unknown evidence (3 of 100 at 100 parallel in `owner-write-evidence-daemon`); the fix landed in `src/hub/snapshots.ts`, outside the planned paths, with the planner's approval.
|
|
62
|
+
- macOS shards (#548, tests.md): counting the shard names in `gateJob()` was planner-approved outside the card's paths. In the first hosted run of PR #954 a cold `/usr/bin/git` took over a second, and 8 tests that run it with `PATH=/usr/bin:/bin` and a 1000 ms timeout were killed at about 1000 ms, against about 140 ms in an unsharded leg that had run git earlier. Without the `AHUB_CHECK_SHARD` unset, any shard but 1 skipped the docs step `test/check-lock.test.ts` expects.
|
|
63
|
+
- Status ordering (#531, tests.md): the slow-snapshot status test took 0.52 s idle and failed 17 of 20 runs with 64 CPU spinners on 4 cores at a 1 s bound.
|
|
64
|
+
- Config watcher off (#546, tests.md): the pet reload test failed 8 of 200 runs at 40 parallel on 18 cores until the watcher was off, and 0 of 200 after.
|
|
65
|
+
- Web fetch transport (#553, tests.md): the daemon test's answer waited on iana.org for 99 percent of its time and failed about 3 of 100 runs at 30 parallel on a 4 s bound; a longer wait would only have measured the network.
|
|
66
|
+
- Jcode usage (#801, budget.md): Jcode's `totalTokens` adds the cached read on top of an input that already holds it for OpenAI-style providers, and `jcode usage --json` names its limit with free text and no window length.
|
|
67
|
+
- Claim verdicts (#901, tasks.md): review of #937 found `factCheck.agreement` counting person-verdict frequencies, which is why a person verdict alone is never an agreement.
|
|
@@ -7,13 +7,13 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
7
7
|
## Adapter definitions, the registry and external modules (#451)
|
|
8
8
|
|
|
9
9
|
- Keep per-kind native policy in `src/adapters/definitions/` and consume it through the registry. Private CLI implementations accept only fixed host-bound ports; keep identity, audit, lifecycle admission and effect ordering in the host. Except for the verified person-owned A4 startup-module contract, do not add config-selected callbacks or runtime imports from registry leaves into eagerly initialized hub/CLI modules; use type-only ports for existing host planners.
|
|
10
|
-
- 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
|
|
10
|
+
- Adapter registry (#451): kind-specific policy lives in that peer's `AdapterDefinition` under `src/adapters/definitions/` (runtime definitions `NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN` in `builtin-metadata.ts` plus `qwen.ts`; the daemon constructs every native driver through `start.create` and `DAEMON_PORTS`). 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
11
|
- Registry load runs `assertDaemonEngine` and `assertNativeCapabilityScope` before publication. A definition that is not a fixed builtin (object identity in `fixedBuiltinDefinitions`, never a kind name) may not declare `start.acpCommand`, `attach.admission`, `budget.resetSoftTokensOnResume`, `session.crashIdentity`, `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`).
|
|
12
12
|
- Native branch ratchet (#625): `test/daemon-native-branches.test.ts` counts every call of a native class predicate (an `is*Peer` imported from an adapter module other than `registry.ts`) per enclosing function in `src/hub/daemon.ts` and `src/hub/claude-launch-proof.ts`, and the counts must equal `test/daemon-native-branches.json` (`predicate`, `function`, `count`, `reason`). A PR that adds, moves or removes such a call edits its row by hand with a concrete reason; never regenerate the table from a test. Bind a predicate alias with `const` only and never call one through a computed namespace key: the scan refuses both.
|
|
13
13
|
- Native proof state (`frontendProxy`, builtin `statusNotice`, `revertTurn` with `session.nativeRevert`, the Claude launch proof and terminal execution root) is read in the daemon only after `isFixedBuiltinDefinition(definitionOf(peer))`: a same-name foreign definition gets none of it. A product of a definition that is not a fixed builtin may not have a `frontendProxy`, `statusNotice` or `revertTurn` member at all, even undefined (`assertNativeProofPorts`, run by `createFromDefinition` and the conformance kit). Use `frontendProxyState` for the daemon proxy reads: absence keeps the generic path, present undefined refuses with `frontend proxy state unknown`. Keep approval policies and restoration debt inside CodexPeer. The remaining Codex `actionEnforcement`/`actionGated` predicate is intentional until a separately specified action-policy port; never add it to `FrontendProxyPort`.
|
|
14
14
|
- Adding a native peer: declare its `AdapterDefinition` under `src/adapters/definitions/` (or its own directory, as agy) and list it in `ADAPTERS` (`src/adapters/registry.ts`). `KNOWN_PEERS`, `OPTIONAL_PEERS`, `SUPPORTED_PEERS`, `START_MODE_PEERS` (a kind with both a TUI and a headless form), `ROLE_ORDER`, `HELP_ORDER` and `PLANNER_PEERS` are derived from it by `deriveAdapterLists`: set `optional`, `projection` (`roleRank`, `startRank`, `helpRank`, `plannerEligible`; a configured or external definition cannot) and, for an `ahub <kind>` launcher, `cli.entry`, `cli.usage`, `cli.preview` and `cli.startWords`, never a list edit (`builtinCliEntries(definitions)` projects the entries). The literal cross-release copies `KNOWN_PEERS` and `OPTIONAL_PEERS` in `src/hub/peer-catalog.ts` and `START_MODE_PEERS` in `src/hub/start-mode.ts` stay hand-kept for older coordinators: `test/registry-guard.test.ts` fails when one differs from the derived list. Adding a peer is the one change that updates their probe counts and `VENDORS` in `test/helpers/registry-inventory.ts` (`REGISTRY_COMPATIBILITY_PROBES`), the probe literal in `test/adapter-registry-a2.test.ts`, the `ADAPTERS` order in `test/adapter-registry.test.ts` and the literal in `test/adapter-catalog-a2.test.ts`; say so in the PR as an expected test change. `isSupportedPeer` answers from `ADAPTERS`, and for a declared instance from its kind's runtime definition unless the instance resolves to an unavailable reviewer variant; `isKnownPeer` and `defaultPeerRoles` answer from `definitionOf`; `STARTABLE_PEERS` reuses `SUPPORTED_PEERS`.
|
|
15
15
|
- When an adapter lands, add its project config directories to `AGENT_CONFIG_SEGMENTS` (`src/hub/permission-mode.ts`); scoped automatic writes must ask there, while explicit approval and `never-ask` keep their existing behavior.
|
|
16
|
-
- Load external modules only at daemon startup from trusted untracked local declarations: verify the self-contained bundle and content pin before importing captured bytes. Keep CLI/reload paths metadata-only, modules restart-bound and never settings-editable, builtin capabilities restricted, and external-kind instances
|
|
16
|
+
- Load external modules only at daemon startup from trusted untracked local declarations: verify the self-contained bundle and content pin before importing captured bytes. Keep CLI/reload paths metadata-only, modules restart-bound and never settings-editable, builtin capabilities restricted, and external-kind instances admitted only under the accepted #754 headless opt-in, live captured catalog, separate worktree and host-managed current-owner proof; preserve the live-module controlled-recovery refusal. Export conformance registrations and fake assets without starting them on import.
|
|
17
17
|
- Bind scanned external runtime SDK imports to this daemon's own `/adapter` entry; never resolve them against project packages. Refuse `/conformance`, `bun:ffi`, `bun:test` and CommonJS loading at runtime. Isolate module refusals from hub startup and retain unknown module roster rows as inert when metadata is unavailable.
|
|
18
18
|
- Require the immutable captured project catalog at instance, roster, execution and permission resolver boundaries; choose a builtin catalog explicitly only for builtin-only callers; accept `adapters` only as wholly machine-local trusted data and require restart for changes. Refuse missing/mismatched pins, non-echoed load sessions, leader/attach argv and unknown owned-group evidence; require an explicit standalone `--no-leader` before argv termination for Grok (kind, executable or pinned native name); signal only the original verified group. Keep unmapped policy unmanaged, absent budgets exempt and read-only instances non-raisable through native acknowledgement plus host/MCP mutation gates. Claim native support only with its isolated T0 receipt.
|
|
19
19
|
- Consume quota/context through the resolved definition's declared readers; absence stays unknown. Use explicit `attach.externalHello` and `instances` policy for admission, never `identity.mcp`; keep runtime owner/root and reviewer isolation proof in the host. Never let configured `roles` include `conductor`. Keep native stderr, prompt-failure, permission-mode and transport error text out of configured-kind logs and turn-failure reasons. A configured CLI start (`src/cli/configured-acp-entry.ts`) accepts only `--mode headless`, `--as`, `--session-id` and one preview flag, and its preview never shows argv or session values.
|
|
@@ -26,11 +26,12 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
26
26
|
- Auto-approval covers the hub's own tools by exact `mcp__agent-hub__<name>` title and only ever picks `allow_once`. Identity comes from the permission request title or, when an agent titles the request with the argument JSON (Qwen), from the announced `tool_call` title bound to the call id and resolved against the session's configured MCP servers, never from payload text, a prefix or an unrelated display title. Never widen it to a prefix or replace it with an agent-selected session mode. Configured ACP kinds (`nativePolicy` or `requireOwnedGroup` set) never auto-approve from a request or tool-call title, even an exact hub tool name; under a locked read-only mode, reject any request whose ACP `kind` is not read, search or think, and never offer `allow_always`. Set Kimi `yolo`/`auto` only for its verified native identity and an operator mode; config never-ask waits for a console y, and other ACP agents stay unmanaged on fresh/resumed starts without setting/resetting modes or failing over their reported mode; refuse runtime changes until their map exists; await the advertised mode change after session new/load and before a prompt, and fail startup on refusal. Use the shared `KIMI_MODE_IDS` mapping (`src/adapters/definitions/kimi-native.ts`, re-exported by `src/hub/permission-mode.ts`).
|
|
27
27
|
- Permission modes (#242) are a person's and live in the daemon's memory only: `permission` and `permission_default` are console-role control messages, `ahub permission` is `console` in `classifyPeerCommand`, and no hub tool or conductor tool sets one. The dashboard and `ahub settings` set one only through `settingsSet` (`src/hub/daemon.ts`, #269), which calls `changePermission` inside `permissionChange` as the console does: never add another path that writes a runtime choice to `permissionModes`; a reloaded project default (#439) is applied only by `applyPendingPermissionDefault`, and only while that peer is absent or offline. Never write a runtime choice to a restart snapshot.
|
|
28
28
|
- Proven read-only shell grants (#816, `src/hub/read-only-shell.ts`, `read-only-shell-proof.ts`) apply to Pi/local and the #845 headless Claude host wrapper under `ask-when-needed`, at the host permit boundary: parsing grants nothing, and a grant needs the closed program and flag table, trusted executable lookup and the path guard. For Pi/local, between the grant and the spawn the host calls `shellExecutionCheck` synchronously; `ask` re-asks once with `forceAsk` and a second unstable proof refuses without running, and an aborted or superseded turn (`executionGeneration`) never spawns. Never widen the table or skip the recheck to make a command pass.
|
|
29
|
-
- Agent shell authority (#812, `classifyPeerCommand` in `src/cli/identity.ts`, `src/hub/operational-authority.ts`): the Allowed read forms live in `peerReadForms` and are admitted only when the daemon also admits the same form for an ordinary tools connection; the conductor family is the closed `conductorOperation` list, each admitted by `admitOperational` (role recheck, closed audit label, no argument text; `queue show`, a settings preview and the authorize handshake recheck through `operationalCurrent` without an audit record). Throw `OperationalAuthorityError` only from an authority fence: only it is audited as refused and gets the conductor or person guidance, and an ordinary operation failure keeps its own error (#839). A new CLI form is `console` until it is added to one of those tables with a test in `test/identity.test.ts`.
|
|
29
|
+
- Agent shell authority (#812, `classifyPeerCommand` in `src/cli/identity.ts`, `src/hub/operational-authority.ts`): the Allowed read forms live in `peerReadForms` and are admitted only when the daemon also admits the same form for an ordinary tools connection; the conductor family is the closed `conductorOperation` list, each admitted by `admitOperational` (role recheck, closed audit label, no argument text; `queue show`, a settings preview and the authorize handshake recheck through `operationalCurrent` without an audit record). Throw `OperationalAuthorityError` only from an authority fence: only it is audited as refused and gets the conductor or person guidance, and an ordinary operation failure keeps its own error (#839). A new CLI form is `console` until it is added to one of those tables with a test in `test/identity.test.ts`. #933: `console` is only trust, money, attribution and interactive terminals; a conductor form joins `CONDUCTOR_DAEMON_OPERATIONS` (its own daemon request calls `admitOperational`) or `CONDUCTOR_LOCAL_OPERATIONS` (its effect runs in the CLI, so the `admit` handshake is its one audit row, sent only after its `LOCAL_OPERATION_ARGS` check in `src/cli/main.ts` passed), never both; a form whose write records the person as actor (amem's `user`) or whose runner inherits the agent markers stays `console`, except the recovery-bearing daemon forms (`restart --yes`, `upgrade --yes --to <version>`, `recovery resume|abort`, #900), which also require their source-issued continuation proof (rule: `daemon.md`), and every reclassified form changes its daemon check in the same PR.
|
|
30
30
|
- A headless ACP Kimi or Codex whose mode lives in the native agent joins the bus only through `reconcileNativeStart` (`src/hub/daemon.ts`): after `start()` the daemon's current mode is applied, then `bus.add` (it can drain at once), then the state `start()` reached is replayed. Every mode change runs inside `permissionChange`, one per peer at a time.
|
|
31
31
|
- Generic permission and generation dispatch in the daemon reads the optional `PeerAdapter` ports (`src/hub/peers.ts`, #667): `permissionModeState`, `setPermissionMode` (resolves only after native acknowledgement; changes approval frequency, never the sandbox), `sessionGeneration` and `checkpointUsesSessionGeneration`; never add a native-class predicate (`isAcpPeer`, `isCodexPeer`) for it. An absent `sessionGeneration` keeps the recovery-session/owner-token fallback; a present but undefined one is unknown and refuses. `BasePeer` installs no default generation, and only an explicit `checkpointUsesSessionGeneration = false` (Pi) skips the checkpoint generation fence. A `permissionModeState` of `unknown` or `unmanaged` refuses a change.
|
|
32
32
|
- Delivery, lifecycle and session dispatch in the daemon reads the same optional ports (#625 slices 2b-2c): `deliveryOnly` (absent or `false` permits `hub_delivery_retry`; `true` or a present `undefined` refuses), `hubOwned` (only `true` lets `peer_stop` stop the runtime), `pendingOwnedProcess`, `form` (a present `undefined` is unknown: `peer_stop` refuses, and a definition with `permission.unmanagedForms` treats it as a TUI), `captureResume`, `detachForRoster`/`resumeFromRoster`, `pullOnly`, `recoveryReady` and `nativeSession` (rules: `daemon.md`). An adapter the hub owns and may stop sets `hubOwned = true`, a delivery-only transport sets `deliveryOnly = true`; never add an `is*Peer` call for any of them (ratchet above).
|
|
33
33
|
- Codex takes a mode as an `approvalPolicy` override on `turn/start`, and the override persists on the thread: pass the hub's and the TUI's `turn/start` alike through `applyPermissionPolicy`, and return to `ask` only by sending the captured native policy once. With none captured, refuse while the TUI is attached; never just stop sending the override. Keep the captured native policy and the restoration debt per thread (`nativeApprovalPolicies`, `overriddenApprovals`) across detach and `thread/start` or `thread/resume`; clear a debt only on a successful response to its restoring `turn/start` while the mode is `ask` (`settlePermissionRestore`), so a rejected turn keeps it. While a thread owes a restoration with a known baseline, in any mode, never replace that baseline: a `thread/resume` response and the TUI's own `turn/start` can both echo the sticky override, and the restoring turn sends the kept baseline whatever the TUI supplied. That protection is one turn: after the restoration succeeded, a policy the TUI sends is taken as the person's choice, captured and passed through, because the proxy cannot tell a repeated echo from one. A detached proxy's `clearPermissionMode` is never refused; if the native policy is unknown, return a CLI-visible note that the thread keeps its last policy. Attached refusal directs the person to close the TUI, then run `ahub permission codex ask`.
|
|
34
|
+
- Codex planner threads (#525-6, `threadSandbox` in `codex-appserver.ts`, live from `isPlannerOnly` for a declared instance): every `thread/start`/`thread/resume` goes up with `sandbox: "read-only"` and is adopted only when its response positively confirms the `readOnly` policy (`confirmsReadOnlySandbox`). An unconfirmed or malformed reply, a reused RPC id, an `unavailable` policy (a planner with a declared worktree) or a throwing reader refuses that RPC and closes the TUI link before adoption or delivery, and `deliver`, `steer` and `steerText` recheck `threadSandboxReady` right before they send. Never count missing evidence as read-only, and never let a role change promote an unconfirmed thread.
|
|
34
35
|
- An unanswered `session/set_mode` leaves the native mode unknown, not unchanged: `AcpPeer` invalidates the turn, goes offline, stops its process group and refuses further changes until restarted. Never report the old or the requested mode after a timeout.
|
|
35
36
|
- A mode grant in `piPermit` or the local worker's `permit` (`src/hub/daemon.ts`) answers as the prompt path does: allowed only while that owner is still the attached one and the call was not aborted. `ask-when-needed` covers `PI_EDIT_TOOLS` on a `grantablePath` only (path rule: `local-worker.md`).
|
|
36
37
|
- Pi and local approvals (#253): `permit` answers a `ToolApproval` (contract: `local-worker.md`) and `requestPermission` (`src/hub/daemon.ts`) tells the adapter's `ApprovalObserver` what settled it. An expiry counts toward the stop only as `source: "expired"` with `eligibleExpiry` not false (no person answered a request of that peer while it was pending); `person` resets the count, `automatic` (a mode grant, Pi's `always`) never does, and a new turn starts it over. The second one ends the turn, never the peer: local throws `ApprovalWaitStop`, Pi sends `abort_budget` with `cause: "approval"`, and both settle the delivery `completed` with the reason; a Pi whose extension did not announce `approvalTurnAbort` goes offline with the delivery held. `requestPermission` deletes a request from `permissions` before it runs an observer, resolves or reports, and an aborted signal closes it as `cancelled`: a late answer must find nothing to grant, and a failed event, notice or console push never strands the caller.
|
|
@@ -48,6 +49,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
48
49
|
- Codex: the adapter never sends its own `initialize`. It is a proxy; hub requests use negative ids and their responses must not reach the TUI.
|
|
49
50
|
- A durable Codex steer stays pending while its native turn runs. Its RPC may answer after `turn/completed`: bound unanswered replies from that turn's end, requeue a definitive late refusal, and hold a late acceptance as uncertain without attaching it to a later turn's audience (#296).
|
|
50
51
|
- 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.
|
|
52
|
+
- 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.
|
|
51
53
|
- Codex 0.154.0 `agentMessage` items carry `text` and `phase` (not `content[]`); only the last non-`commentary` message of a turn is shared.
|
|
52
54
|
- 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.
|
|
53
55
|
- 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.
|
|
@@ -100,6 +102,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
100
102
|
- Jcode steer (#572): reach a busy owned jcode only through `nativeSteer` on its private server socket, never by cancelling its turn: `ping` must list `soft_interrupt_ids`, `subscribe` uses `target_session_id` (the ACP session id) and `continue_on_disconnect`, `state` must report `is_processing` (a soft interrupt sent while idle starts a turn by itself), then `soft_interrupt {content, urgent: false, client_id}`. `canSend` rechecks the captured turn, busy state, owned server process, socket identity and session immediately before the write. Every refusal before the send is `failed_safe` with nothing sent; a sent message that is not acknowledged, or whose turn cannot be confirmed, is `needs_review`, never `failed_safe` or `completed`. Read `currentTurn()` before sending and adopt only through `AcpPeer.adoptIntoTurn(turn, ...)`, which refuses a different or ended turn; adopted senders join the turn answer's audience (`replyParent` and `replyAudience` over the turn envelopes, so the hop cap holds), and each steer receipt is `accepted` at once and settles with the turn (`completed` on a normal end, `needs_review` on a cancel, failure, stop or watchdog). Headless Kimi and Qwen have no steer: status reports their waiting important messages only through `importantWaitingStatus` (`src/hub/daemon.ts`) past `budget.stale_min`. A turn that ends and restarts between jcode's `state` reply and the write still receives the message, held `needs_review` and adopted into nobody: keep that ceiling until upstream offers an atomic per-turn steer, and never add a cancel-to-deliver path.
|
|
101
103
|
- 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.
|
|
102
104
|
- 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.
|
|
105
|
+
- 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`.
|
|
103
106
|
- 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` that compat `agy` refuses (1.4.2 and 1.4.7 are T0-verified; other 1.4.x patches launch with a stderr warning, #589; another version needs the person's trust, #691), and a hub without the `native_hook_launch` feature.
|
|
104
107
|
- 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.
|
|
105
108
|
- 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`, normal termination and a fresh validated completion row; a missing readback keeps the peer busy. Native fact acknowledgement accepts only SDK ephemeral-message rows in the exact session transcript path. `decodeAgyHook` classifies as `Read` only the observed native read (`view_file` `AbsolutePath`, or 1.4.3 `read_file` `Path`); a `StartLine`/`EndLine` range becomes `offset`/`limit`, so a partial read never counts as a whole-file view. Unobserved edits get no scoped grant or effect attribution. The daemon records Pre/PostToolUse through `Facts.preTool`/`postTool` and offers nothing on them (#555).
|
|
@@ -124,6 +127,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
124
127
|
- 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.
|
|
125
128
|
- 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`.
|
|
126
129
|
- 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`.
|
|
130
|
+
- Failure stages (#950): a failed turn's reason comes only from the launch-bound `ClaudeFailureTrace` (`src/hub/claude-failure-stage.ts`), sealed at leader exit and rendered by `formatClaudeFailure`; it is `details withheld` unless `failurePrivate` answers `false` (absent, unknown or a throw is private), and a trace error never changes cleanup or settlement. The tools-mode server denies an invalid `hub_permission_request` locally whatever the hub answers: `acceptsNativePermissionRequest` keeps the approval path's checks, and the strict `validateNativePermissionRequest` only classifies a diagnostic probe (at most `NATIVE_PERMISSION_PROBE_MAX_PER_LAUNCH` per launch); never use it to narrow or widen approval.
|
|
127
131
|
|
|
128
132
|
## Always-ask (#636)
|
|
129
133
|
|
|
@@ -14,3 +14,30 @@ Scope: the CooperBench runners, their fixtures, teardown and recovery. Read befo
|
|
|
14
14
|
- Treat an earlier journal-less ACP attempt as possibly affected by cancellation overlap (#304) when its recorded peer `state` goes idle without an answer near `watchdogMs` after the last recorded activity, followed by rapid `failure` entries and `qwen_log` prompt-failed lines, then `active_end` as `peer-failure` with that reason. The driver's tap does not record `undeliverable`, main logged no watchdog cancel, and Qwen's busy-refusal wording is still unmeasured: these are candidate signs, not proof, and absence does not clear an attempt. Keep historical results unchanged and the native measurement owed in `docs/smoke.md`.
|
|
15
15
|
- After #304, an unreported cancelled prompt leaves a cancellation-deadline line in `qwen_log`, a recorded offline state about 30 s after the watchdog, and `active_end` as `native-failure` on the next driver tick. The driver starts each peer once and cannot restart it within the attempt; its in-memory queue ends with the process. The end cause of affected attempts is not comparable across this fix (formerly `peer-failure` or the wall limit); keep that distinction in later collections, per issue #304's Decision, without changing historical scores or results.
|
|
16
16
|
- The benchmark local-worker tool surface is `src/local/tools.ts`: its `bash` permit now carries a host shell recheck (#816, see local-worker.md). A benchmark driver that stubs `ctx.permit` needs no change, since an absent `shellExecutionCheck` means `valid`.
|
|
17
|
+
|
|
18
|
+
## Routing promotion rule (#637 part 1)
|
|
19
|
+
|
|
20
|
+
A default that decides who takes a task is changed with a receipt, not by hand. Scope: `templates/routing.toml` (task routing: class peer lists, `local_allowed`, `[signals]`, `[constraints]`, `[pi]` windows) and what `assign()` reads of it. Model routing (`hub_routes`, `local.route`, `fixed_model`, the gateway aliases) is part 2 of #637 and has its own corpus, receipt and check below; this rule's receipt still says `served_models: not applicable`.
|
|
21
|
+
|
|
22
|
+
- **The frozen corpus** is `scripts/benchmarks/routing-corpus.json`: board-shaped cases (class, signals, peer states, reservations, exclusions, dependencies), each with the owner it must get (a peer or nobody), optionally the reviewer and the refusals that must appear. Every case has a `train` or `test` split, and the file carries a content hash over everything but the hash itself, the split included. The cases freeze the roles they route with (`defaults.roles`), so a change to the shipped roles cannot move them. Each split must hold at least one PII case and one refusal case, and a PII case that must reach `local` as well as one that must reach nobody.
|
|
23
|
+
- **Expectations are written from the policy's documented rules** (the `note` of each case names the rule), never copied from a run. A case a plausible default change would flip is the point; a corpus every edit passes measures nothing. `test/benchmarks/routing-gate.test.ts` edits the template eleven ways and requires each to be blocked.
|
|
24
|
+
- **The gate** is `bun scripts/benchmarks/routing-gate.ts`: no flag prints the current default's numbers (it must be 100% on both splits); `--receipt [--base <ref>]` compares the work tree's `templates/routing.toml` with the default at the base (the merge base with `origin/main` by default) on the test split and writes `docs/verification/<date>-routing-<sha8>.md`; `--freeze` rewrites the hash after an intended corpus change. It runs the pure `assign()`: no clock, no network, no model, so CI can run it and `ahub route explain` cannot drift from it.
|
|
25
|
+
- **The rule.** A PR that changes `templates/routing.toml` links a receipt whose candidate is not worse on the primary metric (owner top-1 accuracy on the test split) and regresses no PII or refusal case. Any regression on a PII case blocks, even when the primary metric rises. A case counts as a regression when the base passed it (owner, the stated reviewer and the stated refusals) and the candidate fails it.
|
|
26
|
+
- **CI** is `.github/workflows/routing-receipt.yml` running `scripts/check-routing-receipt.ts`. It fails a PR that changes the template unless a receipt under `docs/verification/` names that exact file (sha256), was made for the current corpus hash and for the default at the merge base, is on the test split, and is, field for field, what the gate would write for the corpus and the two files: the check REBUILDS the whole receipt (both policy summaries, the changed keys, every metric, every per-case outcome, the served-model note) and compares it canonically with the machine block, then compares the text outside the block (the summary and the case table) with what the gate renders from that data. The receipt chooses only its date (`YYYY-MM-DD`, also in the file name) and a short label for the base ref; a receipt edited by hand in any field, or a block with an extra field, cannot pass, and a receipt whose verdict is `blocked` does not pass either. The corpus never changes in the same PR as the default (land the corpus change and its refreeze first). A PR that does not touch the template needs nothing.
|
|
27
|
+
- **A corpus change voids every earlier receipt** (their corpus hash no longer matches). That is intended: the receipt of an old default is evidence about an old measuring stick.
|
|
28
|
+
- **Do not**: edit a case's `expect` to make a candidate pass, relax a guard, move a case between splits to flatter a candidate, or regenerate a receipt from a different base than the merge base. A candidate that fails a case the policy documents wrong is a spec question for the person, not a corpus edit in the same PR.
|
|
29
|
+
- A check that certifies a generated file recomputes ALL of it and compares it whole, never a chosen subset of fields (#321 review: the first version compared the verdict and counts only, so a receipt could carry any configuration, changed keys and per-case table and still be certified). Add a tampering test for every field the file carries, and one for the text around a machine block.
|
|
30
|
+
- **The routing gate is the repository's eval check (#481 S5, person's scope of 2026-10-28).** `scripts/check-routing-receipt.ts --publish` also creates, through the Checks API, one completed check run named `routing-eval` at the PR head, whose title carries `eval_pass_rate=<0..1> eval_suite=<corpus hash, first 12 digits>`: the owner top-1 accuracy of the head's `templates/routing.toml` on the test split, and the corpus version. Those are the two markers `readEvalResult` (`src/hub/eval-gate.ts`) parses. The person names it with `eval_check = "routing-eval"` in `.agenthub/sdlc.toml`. It is a SECOND run because an Actions job's own check run (`routing-receipt`) has a null title and summary, so the hub could read no marker from it; the job needs `checks: write`. A rerun UPDATES the run (the hub reads exactly one completed run of that name at a head). A publish that cannot be made is a `::warning` and never changes the job's exit status, which is the receipt verdict; the hub then shows the eval gate's "pass rate or suite version not reported" gap. With no readable corpus or template the report has no markers and a failed conclusion, never a made-up number. The rate is a score of the shipped default on the test split (100% while every documented rule holds), not a score of the change: the change is judged by the receipt.
|
|
31
|
+
- A reader of many generated files tests the SHAPE of each before it dereferences any field (`receiptShape` in `scripts/benchmarks/routing-corpus.ts`): a block that is only a receipt by `kind` and `version` is skipped, never an exception, so one malformed old file cannot stop the check before a valid receipt beside it is considered (#321 review nit). Test malformed and valid files together, in both orders.
|
|
32
|
+
|
|
33
|
+
## Model-routing promotion rule (#637 part 2)
|
|
34
|
+
|
|
35
|
+
A default that decides which MODEL answers is changed with a receipt too. Scope: the model-routing keys of `templates/routing.toml`, read as one summary (`modelRoutingSummary` in `scripts/benchmarks/model-routing-corpus.ts`): `[local]`, `[hub_routes.*]`, top-level `stay_switch` and `max_switch_prefill_tokens`, each class's `route`, `fixed_model` and `pi_backend`, and the `[pi]` windows. Peers, `local_allowed`, `[signals]` and `[constraints]` are part 1's. Part 1's receipt hashes and rebuilds the WHOLE template (its `policySummary` is every parsed key), so ANY change to `templates/routing.toml` still needs part 1's receipt, and a change to a model-routing key needs this one as well: two receipts, one per gate. A change that touches no model-routing key (peer order, signals, constraints) needs only part 1's, and this check says so and passes. Narrowing part 1 to task-routing keys would end the double receipt; it is left unchanged here (out of this task's paths) and is the upgrade path.
|
|
36
|
+
|
|
37
|
+
- **The frozen corpus** is `scripts/benchmarks/model-routing-corpus.json`, built from `build-model-routing-corpus.ts` (readable fixtures) and frozen with `model-routing-gate.ts --freeze`. Three case kinds. `selection` cases give the route and alias a class gets on the local worker. `route` cases drive the REAL `HubRouteRuntime` (stage, plan_execute, advisor, escalation) with a scripted host: the "model" is a fake that records which alias was asked and replays the case's judge replies, so what is scored is the routing code and the routing keys, never a model. `judge` cases are conversations whose verdict class (approve or redo, escalate or not) a sound judge returns, written from the judge prompts' own rules; they are scored ONLY by the hand-run live receipt, never in CI.
|
|
38
|
+
- **Expectations are written from the documented rules** (each `note` names one), never copied from a run. Both splits carry a deterministic PII guard and a deterministic hard-override guard, the test split carries a prompt-injection judge guard, and validation refuses a corpus without them. A PII case has an ordinary twin (the PII trace carries no prefill size, the ordinary one does), so it cannot pass on an empty trace.
|
|
39
|
+
- **The gate** is `bun scripts/benchmarks/model-routing-gate.ts`: no flag prints the shipped default's numbers (100% on both splits); `--receipt [--base <ref>] [--out <file>] [--live]` writes `docs/verification/<date>-modelrouting-<sha8>.md` (the name differs from part 1's `-routing-` so neither check reads the other's files). The sha8 is the digest of the model-routing keys, not of the whole template.
|
|
40
|
+
- **The rule.** The candidate is not worse on the primary metric (deterministic case pass rate on the test split) and regresses no PII or hard-override case; when the live half is present, the judge cases follow the same rule (fewer passes or a regressed guard judge case blocks). The receipt's `overall` verdict (its heading, and the gate's exit code) combines both; `evidence.verdict` is the deterministic part only, and the document prints both. A change that needs the live half but was written without it says `Incomplete` in its heading.
|
|
41
|
+
- **The live half is by hand.** A change to an alias, a judge or any key outside the deterministic set (`liveRequired`) moves what a model answers, so the receipt must carry a `live` block made with `--live`: it asks each policy's judge alias the judge cases through the gateway with the request THAT POLICY builds (`judgeRequestFor`: the same prompts and the advisor's route `transcript_max_chars` as `HubRouteRuntime.review` passes them; a test compares it with what the real runtime sends) and writes the model id the gateway REPORTED for each alias (OmniRoute `servedModel`). Each outcome records the sha256 of the request it was asked, and CI recomputes it, so a receipt for a changed cap that measured the default's request is refused. It needs `AGENTHUB_OMNIROUTE_URL` and `OMNIROUTE_API_KEY`, exits 2 without them before any request, and writes nothing when a call failed, the gateway named no served model, or it named the alias. A deterministic-only change may carry `served_models: live-not-run`.
|
|
42
|
+
- **CI** is the second step of `.github/workflows/routing-receipt.yml`, `scripts/check-model-routing-receipt.ts`. It runs no model and has no gateway or key. It REBUILDS everything the files determine (configurations, changed keys, every metric and per-case outcome, the rendered document) and compares it whole, as part 1 does. The live block is the one thing no file determines, so CI checks its shape and consistency only: exactly the test-split judge cases, the alias each policy asks, no failed call, a served id for every alias that is a real model id and not the alias, and scores recomputed from the recorded verdicts. It cannot check that a model said it: a person who reads the receipt owns that.
|
|
43
|
+
- **A corpus change voids every earlier receipt** and never lands in the same PR as the keys it measures. Do not edit a case's `expect` to make a candidate pass, relax a guard, or move a case between splits to flatter a candidate.
|
|
@@ -10,11 +10,12 @@ Scope: quota and native context readings, checkpoints, pause, handoff and resume
|
|
|
10
10
|
- The status line tee must never fail or slow the render: no throw, original command run with the same stdin, 5 s cap.
|
|
11
11
|
|
|
12
12
|
- Keep native context occupancy (`src/hub/context-window.ts`, readers in `src/adapters/definitions/native-context.ts`) separate from quota accounting; never use Codex's accumulated `total` as occupancy or estimate Pi counters. Host code reads quota and context only through `readNativeQuota` (`budget.ts`) and `readNativeContext` on the resolved definition's declared `budget.reader` / `budget.context`; no declared reader is unknown, never an empty reading (rule: `adapters.md`).
|
|
13
|
-
- Context checkpoint completion requires its request id and the current attached peer/native session. Invalidate its transport generation synchronously at a new peer claim, before asynchronous recall or attachment. Never pause, reassign or replace a session on context pressure. Refuse private-turn, PII-pattern and all associated open PII-task summaries (owner or reviewer, including in_review, with the current routing policy; use `isClosedTaskState` to exclude approved and cancelled work) before local persistence or cloud memory; never broadcast checkpoint text.
|
|
13
|
+
- Context checkpoint completion requires its request id and the current attached peer/native session. Invalidate its transport generation synchronously at a new peer claim, before asynchronous recall or attachment. Never pause, reassign or replace a session on context pressure; `ContextWindows.admission` (#802) only copies the reading for new-offer routing and never changes the crossing latch. Refuse private-turn, PII-pattern and all associated open PII-task summaries (owner or reviewer, including in_review, with the current routing policy; use `isClosedTaskState` to exclude approved and cancelled work) before local persistence or cloud memory; never broadcast checkpoint text.
|
|
14
14
|
- Invalid/stale context readings expose unknown and do not rearm a crossing. Latch a crossing only after its event is accepted; reconsider paused/recovery-held crossings after release with fresh current-session readings. Tee context fields contain finite numeric values or null only, bound to daemon instance, launcher and native session. Context and session go to the per-peer file `claudeStateFile(base, peer)` (the builtin keeps the unsuffixed name); `claude-usage.json` stays one shared file per account.
|
|
15
15
|
|
|
16
16
|
- A provider hard refusal ends a reading override and supersedes a soft evaluation still awaiting its checkpoint; never let the obsolete checkpoint overwrite that pause. An ACP provider quota refusal reports a hard budget reading and confirms the live pause before the adapter requeues work, so the pause fences retries and the existing relay handles assignments. Use the observed window length for an unknown reset; repeated quota refusals while paused do not consume poison-envelope retries.
|
|
17
17
|
- Kimi's ACP `{used, size}` occupancy is a context reading (`acpContext`, source `acp_usage_update`) bound to the adapter's own session id: never a `tokens` event or a budget window, and an invalid shape reports unknown, not the previous value. A gate crossing sends a checkpoint request only to definitions with `budget.checkpoint` (Claude and Codex); an ACP peer gets the event and the notice (#285).
|
|
18
|
+
- Jcode (#801): its ACP `usage_update` `{used, size}` is a context reading (`jcodeContextValue`, source `jcode_usage_update`, session-bound); its prompt result `usage` is per-turn consumption recorded as one `jcode_acp` `usage` event (`acpTurnUsage`, `onTurnUsage`) and never reaches `addTokens`, a token window or a budget window. Never read Jcode's `totalTokens`, never infer a quota window from either record, and keep a malformed, wrong-session or stale value unknown. Never read `jcode usage --json`. A later Jcode version repeats `docs/verification/2026-10-28-jcode-0.99.1-usage.md` before its readings are claimed.
|
|
18
19
|
- A quota resume notice replaces the peer's unread resume notices: carry their moves into the new one (`refs.budgetResumeMoves`, or the whole text of a notice that has none), publish it, and only then withdraw the ones it replaces, so a failed publish keeps them (#306).
|
|
19
20
|
- Quota handoff context reads `QUOTA_HANDOFF_PLATFORMS` (`src/adapters/registry.ts`, each definition's `hooks.recallPlatform`: claude, codex, kimi, qwen, jcode). It is independent of `hooks.recallInjected`: a handoff platform never joins the injected-platform exclusion in recall (`test/adapter-handoff-platforms.test.ts`).
|
|
20
21
|
- `Budget.report` resolves the reporter and its account through `hooks.definitionOf` (the daemon's startup-bound project catalog; builtin-only when absent) and skips `user`, `hub` and a registered kind, configured and external module kinds included, with no `budget` block or with `budget.quotaExempt` (only local); an unregistered id keeps quota accounting. `budget.instrumented` (execution budget) never exempts quota.
|
package/docs/agent-notes/bus.md
CHANGED
|
@@ -10,10 +10,10 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
|
|
|
10
10
|
- `WsPeer.steer` exists only for a push channel session: set at attach, cleared on detach, roster detach and a pull-only attach, and it refuses anything but important work in the current busy turn (#493).
|
|
11
11
|
- `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.
|
|
12
12
|
- 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).
|
|
13
|
-
- 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 is never condensed.
|
|
13
|
+
- 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 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.
|
|
14
14
|
- A condensed digest is sent by `digest`, not `hub`: `replyParent()` skips only the hub's `presence` item, and a reply to a digest must keep the highest hop of what it replaced. On a `failed_safe` delivery or a `retry` resolution the bus puts back the originals, never the digest.
|
|
15
15
|
- Limits admit what is sent: the envelope `newEnvelope` builds (a reply inherits its parent's sender, `digest` resolves to the originals, `capPriority` applies), never the raw `to` and priority. Admit first and build later, and implicit replies all count as broadcasts.
|
|
16
|
-
- A reply is addressed, never broadcast. An adapter that answers a delivery passes `to: replyAudience(envs)`; anything else with an `inReplyTo` inherits that envelope's sender. Leaving `to` empty fans the message out to every peer.
|
|
16
|
+
- A reply is addressed, never broadcast. An adapter that answers a delivery passes `to: replyAudience(envs)`; anything else with an `inReplyTo` inherits that envelope's sender. Leaving `to` out or empty fans the message out to every peer. The control `send` takes a nonempty string `to` as that one peer and refuses any other non-array shape or bad item before publishing (#963); never turn a malformed `to` into a broadcast.
|
|
17
17
|
- `digest` is not a peer: addressing a reply at the envelopes the peer was handed sends it nowhere once a delivery was condensed. The bus resolves `digest` back through `lastDelivery.originals`; anything else that reads a reply's `to` has to do the same.
|
|
18
18
|
- A pull-only peer (a Claude session without channel pushes, Kimi in its own TUI, or agy) is never drained by a push; `Bus.pull` hands it its queue oldest first up to `PULL_BUDGET_BYTES` (64 KiB of `frame` text, #713; `takeBudget`), the first item always going so one huge envelope cannot stall it, and a poison one still alone. A push peer, and an attached channel session that is not pull-only, keep `take`'s `DIGEST_MAX`: never raise that for a pull. The daemon's `inbox` reply carries `waiting` and the plugin says `N more waiting: call hub_inbox again before you start work`. An attached channel session may also pull its own queue through `hub_inbox` while busy (#493); the same holds and journal completion apply. Drain and pull share `held()` (recovery, an uncertain delivery, any pause, the lifecycle `deliveryHeld` fence) and `remember()` (so `reply_to` resolves), and a drain rechecks `deliverable()` after condensing because such a session can take the peer meanwhile. A pull is journaled `completed` in the same transaction as the bus snapshot, never `accepted`; a failed one puts back only that peer's queue and preface and never reloads the snapshot.
|
|
19
19
|
- Record exact task-offer reads only through `DeliveryJournal.completeDelivered` on an active-fenced adapter completion, correlated reply or actual pull, atomically with completion. Capture original offers before terminal pruning and record their ids, never condensed ids; accepted receipts, generic transitions, operator/hub dispositions and restored completed rows are not read evidence.
|
|
@@ -9,17 +9,22 @@
|
|
|
9
9
|
- For explicitly user-authorized live verification, use the per-invocation CLI delegation in `docs/security.md` §Agent CLI identity and conductor authority; keep caller markers and record native evidence separately from fixtures.
|
|
10
10
|
- Keep `export const LIVE_TEST_SUPPORTED = true;` in `src/cli/identity.ts`: delegated recovery staging reads this declaration before any source effect.
|
|
11
11
|
- A delegated non-recovery command runs only after `recordDelegatedCliAudit` committed `delegated` against a hub whose `features` list `cli_audit_delegated`, that instance's console `cli_audit_ack` confirmed retention, and `ControlClient.pinInstance` pinned the invocation to it; a refused feature, failed ack or changed consumer executes nothing (#410). Upgrade, restart and recovery record delegation only in the operation receipt (`delegatedAudits` through `createOperation` or `recordRecoveryDelegation`). The daemon counts an audit retained only after `event()` returned true.
|
|
12
|
+
- For conductor recovery (#900), keep source-issued private continuity separate from public actor/audit metadata; recheck the same initiator and every affected project at effect boundaries. Compare candidate config and routing with the running accepted policy. Verify and use the same captured startup inputs before any adapter import; never publish a later raw-file reread. Keep captured TUI restoration and unknown source/target evidence person-only. Derive target feature floors from verified target bytes, never a request or receipt claim (`docs/operations.md`, "Conductor-bound restart and recovery continuation").
|
|
12
13
|
- Conductor operational refusals (#812, #839): throw `OperationalAuthorityError` (`src/hub/operational-authority.ts`) for an authority refusal, never a plain `Error`. Only it sets `authorityRefused: true` and its scope on the reply, which the control `reply` wrapper audits once as a refused `agent_cli` event; any other failure of a labelled request keeps its own error with `authorityRefused: false` and is never audited as refused, and the CLI's `fail` audits only when `authorityRefused` is not `false`. An unreachable or dead hub is "authorization unavailable", never a missing conductor role. `publicOperationalView` is cached on the board status revision, config revision, routing and the private-path policy: a new input to that projection joins the cache key.
|
|
13
14
|
|
|
15
|
+
- Peer DOING (#878): `observeDoing` is the only writer and `doingRows` the only reader (`src/hub/daemon.ts`, helpers in `src/hub/peer-doing.ts`). Decide privacy at observation (`contextPii` or a refusing or throwing `tasks.publicMetadataText` latches a stub) and again at display (an open PII task owned or reviewed, or a private turn); reuse those screens, never add one. `status()` adds it only with `includeDoing` (the `status` reply and the dashboard snapshot), so `writeStatus` never writes it to `status.json`; the conductor's `hub_status` gets the same rows, each non-stub value framed by `publicConductorStatus` as untrusted (`sanitize`d text, `from`, `untrusted: true`, `doingNote`); never give an agent DOING text without that frame. Claude's title comes from the transcript path the facts hook reported and `claudeTranscript` validated, read through `readAiTitle` (1 MiB per read, lines over 64 KiB skipped); its current item only from PreToolUse input, never the transcript; every cached TaskUpdate subject, current or not, is screened when it is learned (`contextPii` or `tasks.publicMetadataText` refusing or throwing, via `ClaudeTodoSource`), keeps that verdict and passes it to `observeDoing` as `learnedPrivate`. A doing failure never changes a facts reply.
|
|
16
|
+
|
|
17
|
+
- Conductor console ops (#933): a tools caller's `CONDUCTOR_TASK_OPS` request (`github_import`, `intake_approve`, `schedule_run|pause|resume`, `task_unassign`) and `goal_link` are admitted by `admitOperational` in `onMessage` before Tasks sees them, and the task op runs inside `operationalContext`, which `TasksDeps.operational` reads; never let `taskOpBody` admit a non-person caller to a console op any other way. `conductor_operational` `admit` audits a `CONDUCTOR_LOCAL_OPERATIONS` form once; `authorize` only rechecks the role. `task_catalog_role` from a tools caller passes the peer to `Tasks.setRole` (never `USER`), and is refused for a child, external, read-only or PII-context caller.
|
|
18
|
+
|
|
14
19
|
- Console attention navigation selects a delivery only by an exact visible row id. A missing id or row prints read-only guidance; never select by peer or fall back to the first Queue row.
|
|
15
20
|
|
|
16
21
|
Scope: the operator console, control WS and its protocol, state files, the dashboard listener and settings, the Claude channel's reconnect, `ahub setup`, git snapshots, upgrade and recovery. Read before editing `src/adapters/definitions/native-recovery*.ts`, `src/adapters/definitions/native-policy-aux.ts`, `src/adapters/definitions/native-config-*.ts`, `src/adapters/definitions/native-cli-*.ts`, `src/cli/console.ts`, `src/cli/console-state.ts`, `src/cli/attention.ts`, `src/ui/needs.ts`, `src/hub/daemon.ts`, `src/hub/ui.ts`, `src/hub/settings.ts`, `src/hub/control-client.ts`, `src/adapters/claude-channel.ts`, `src/hub/restart.ts`, `src/hub/recovery-store.ts`, `src/hub/snapshots.ts`, `src/hub/manager.ts`, `src/hub/lifecycle.ts`, `src/hub/crash.ts`, `src/hub/start-mode.ts`, `src/hub/terminal-open.ts`, `src/hub/recovery-error.ts`, `src/hub/db-timeout.ts`, `src/hub/config-reload.ts`, `src/hub/config-apply.ts`, `src/hub/config-tier.ts`, `src/hub/config-edit.ts`, `src/hub/config-edit-host.ts`, `src/hub/claude-launch-proof.ts`, `src/hub/recovery-continuity.ts`, `src/cli/recovery-presentation-options.ts`, `src/cli/setup.ts`, `src/cli/upgrade*.ts`, `src/cli/terminal-recovery.ts`, `src/cli/recovery-package.ts`, `src/cli/facts-hook.ts`, `src/hub/usage.ts`, `src/hub/memory-profile.ts`, the memory code modules (`src/hub/memory-symbols.ts`, `src/hub/memory-code-overlay.ts`, `src/hub/memory-code-head.ts`, `src/hub/memory-stale-anchors.ts`, `src/cli/memory.ts`), the task brief host (`src/memory/brief.ts`, `src/hub/memory-plan-guard.ts`), `src/hub/native-hook-launch.ts`, `src/cli/output.ts`, `src/ui/index.html`, `src/hub/task-code-group.ts` (dashboard Code group), the agent drift sweep (`src/hub/agent-drift.ts`, `src/cli/agents.ts`) or the instance modules (`src/hub/instances.ts`, `src/hub/instance-config.ts`, `src/hub/instance-execution.ts`), or before adding a native peer.
|
|
17
22
|
|
|
18
23
|
- The plugin bundle is installed apart from the daemon. Breaking a control WS message contract bumps `PROTOCOL` in `control-client.ts`; a new request answered as unknown or an optional field preserves it, as does additive optional public task metadata in `ui_snapshot` that older clients ignore (such as `deps` in #246).
|
|
19
|
-
- `peer_stop` is console
|
|
24
|
+
- `peer_stop` is the console's or an admitted conductor's (`admitOperational(sock, "stop")`) and stops, through its adapter, only an owner whose adapter reports `hubOwned === true` and whose `form` is not `tui` (today headless ACP, Pi, local, print-reviewer and Claude implementer); a present `undefined` form refuses. Refuse native-terminal peers and recovery-held mutations; serialize with starts and permission changes, and refuse a recovery `prepare` while a stop runs. An admitted stop holds delivery through `BusOptions.deliveryHeld` before any teardown await (never a console pause that resume can undo, or a global recovery hold that blocks unrelated peers) and withdraws the peer's pending approvals before the adapter stops. A requested stop holds an in-flight delivery as `needs_review` with an ids-only stop reason, suppresses restart/escalation, and never resolves that delivery automatically.
|
|
20
25
|
- Console review guidance (#711): `r` on a task in review opens `reviewChoice` (`src/cli/console-review.ts`, modelled on `modeChoice`) from the row or the open task detail, never a bare prefill; `a` and `c` prefill `review N approved|changes_requested ` with a guide line, a `changes_requested` line with no note is refused in `reduceConsole` before any effect, and the result of a review command is shown as a notice in panels mode (`reviewResult`). The console only prefills and runs `ahub review` as the person: never grant it extra authority, and keep a private task's title out of the choice line (`[pii]`). The CLI cause line (`agentShellCause`, `src/cli/identity.ts`) is printed for the `tools` identity and a person-only hub refusal only, never for the console identity, and never replaces the refusal.
|
|
21
26
|
- Console changed-symbols block (#684 2b): `symbolFields` (`src/cli/console-symbols.ts`) adds `symbols` after `plan` for a task_show-shaped detail only, from the public `codeSymbols`; never from a raw `Task`, never for a private task, never from a stored receipt (one with `repositoryKey` reads as unknown: only the screened public receipt is shown), never as a second copy of the match rule (`planSymbolMatches` in `review-brief.ts`). Unknown is `unknown (no code facts)`, never an empty list.
|
|
22
|
-
- Console Queue `D` (#713) only prefills `queue discard --peer <selected row's peer> --reason `; Enter refuses a missing peer or reason and arms a `y` confirm that names the count, then runs the person's `ahub queue discard ... --yes`.
|
|
27
|
+
- Console Queue `D` (#713) only prefills `queue discard --peer <selected row's peer> --reason `; Enter refuses a missing peer or reason and arms a `y` confirm that names the count, then runs the person's `ahub queue discard ... --yes`. Beyond `list`, the `queue` op is the console's or an admitted conductor's (`admitOperational`, `show`/`resolve`/`discard` only); `discard` without `confirmed` only counts, and a confirmed one needs a reason, is refused during recovery or shutdown and discards row by row through `resolveDelivery` (rule: `bus.md`).
|
|
23
28
|
- Console Usage panel (#683 U3): tab `7`, text from `src/cli/console-usage.ts` (pure strings, no terminal or state import), rendered in `console-state.ts`, fetched in `console.ts` `refresh()` only while panel 7 is open. It reads the `usage` op and nothing else: no event-log read, no board field beyond the task id selected in Tasks. A refusal shows `usage unavailable`, never empty usage; an unknown counter is `unknown`, never 0; a private task is `#id [pii]` from the op's `taskPii`, never a title. The `usage` op folds with `catchUp(250)` and answers `behind: true` when it is not done: never answer from one pass of a big log. Keep Left/Right from 7 wrapping like Settings so the `1`..`5` cycle (#309) stays pinned, and keep PAUSE and 7D out of the width drop list.
|
|
24
29
|
- Dashboard snapshot `view` (#683 U4): the daemon's `ui_snapshot` and the manager's forward pass a request's `view` only through `dashboardView` (`DASHBOARD_VIEWS` in `src/hub/ui.ts`); keep that list equal to the page's `DASHBOARD_VIEWS` in `src/ui/index.html`. Any other value is dropped, never forwarded.
|
|
25
30
|
- Dashboard Usage view (#683 U4): `uiSnapshot` builds `usage` only when the request's view is `agents`, never on a hidden view's poll. It folds with `catchUp(100)` and sets `behind: true` when that is not done, so a partial figure is never shown as the whole. A failed read answers `{ unavailable: true }`, never `undefined`, which the page reads as a hidden view and keeps the old figures. The snapshot carries task ids, a `pii` flag and numbers only, never a title; the page shows a private task as `#id [pii]`. Daily figures come only from `UsageRollup.daily` (rule: `budget.md`).
|
|
@@ -28,7 +33,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
28
33
|
- SDLC band timer (#481 S7): the daemon's band interval (`bandsEveryMs`, default 5 min) is the only caller of `tasks.proposeFromBands`. It skips while stopping or in recovery, when `tasks.bandsState()` is not ready or `bandsHeld` holds, and when `board.statusRevision()` with the band signature is unchanged since the last pass that ran; it records that key after the write. A failure is one sanitized `hub.log` line. Writer rules: `tasks.md`.
|
|
29
34
|
- `ahub board` (#724): `renderBoard` prints one line per task only with a terminal width and without `--full`; `--full`, `ahub task show` and output without a width keep the full table and SDLC advice. `STAGE_CODES` (`src/cli/output.ts`) keys on blocker-text prefixes from `src/hub/sdlc.ts`: rewording a blocker there updates its code in the same PR. TITLE is cut, never wrapped; STAGE is never dropped (it falls back to the meter and the code); a PII row keeps its `(ahub task show N)` hint while 8 title columns remain.
|
|
30
35
|
- CLI output default (#814): a command prints for a person unless `--json` is given. The recovery and upgrade commands follow it too (`src/cli/upgrade-output.ts` composes `planLines`, `renderPlan`, `renderOperation`, `renderBlocked` and `choiceInstruction`, so the non-interactive text never says what the screens would not): a pipe and `--dry-run` print the plan with blockers, reconnect-only and fresh-start peers in stdout, `--json` keeps the document and the stderr lines (#206), and `recoveryDisplay` takes a pipe's width from `COLUMNS`. Hub tool text (`task show`, the backlog and release commands) is parsed on the CLI side by `showToolText` and rendered by `src/cli/task-output.ts`; `--json` prints the text byte for byte, unparseable text prints as it came, and a private task is rendered as the stub the hub sent, never asked for more. A command that takes free text reads `--json`, `--full` and `--color=` through `splitPresentation` (`edges`): only a leading or trailing run is a flag, and a value flag's value never is, so a reason keeps its words. `test/cli-json-guard.test.ts` also scans every file in `src/cli/` for a print of a hub tool's text (`console.log(await taskOp(<op>))`, or `console.log(x)` where `x` is bound from one) and passes only when the print is inside a `--json` branch, the op is in the named `SENTENCE_OPS` (ops whose daemon case returns prose, each checked against the daemon), or it is named debt with a reason (`TEXT_DEBT` for JSON ops a later task converts, `CONDUCTOR_DEBT` for the conductor-role commands, `REFUSAL_FALLBACK` for the refusal sentence printed after `--json` and the table were handled); a new JSON-returning op printed bare fails it, and an entry that matches no print fails too. Convert a command by rendering it through `showToolText`, never by adding it to a list. A renderer is a pure function in `src/cli/` (`sdlc-output.ts` for the `sdlc` read commands) and the call site keeps its own visible `if (options.json) console.log(JSON.stringify(...))`, so `test/cli-json-guard.test.ts` can see the branch. Never print JSON by default: add the command's `--json` branch, or name the site in the guard's `DATA_FORMAT` (JSON by purpose: `export`, `sdlc export`, `bench export`, the PreToolUse hook reply) with its reason. The guard accepts a `--json` branch only when the condition PROVES the flag (`selectsJson`): `X.json` on an option holder (`options`, `display`, `present`), `args|flags.includes("--json")`, a known alias, `&&` when either side does, `||` only when both do, `=== true`. Anything else (`!x`, `=== false`, `|| other`, `?? x`, a comparison, an unknown name) is not a guard, so a new shape fails until it is written one of these ways. A renderer table never exceeds the terminal: `grid` (`sdlc-output.ts`) wraps cells, then falls back to one labelled block per row, and keeps every identifier whole. `NOT_YET_CONVERTED` is debt the later #814 tasks delete; an entry that matches no site fails the test, so the list only shrinks. `sdlc` drops `--json`, `--full` and `--color=` from its operands only for its read subcommands (`sdlcOperands`): the others keep their argv exactly. A hub tool's JSON text that does not parse prints as it came, never as an empty report.
|
|
31
|
-
- Settings (#269): the dashboard's `setting`, `setting_undo`, `setting_preview` and `setting_reload` actions and the console-role `settings_get|set|undo|preview|reload` requests go through `settingsSet`, `settingsUndo`, `settingsPreview` (`settingsUndoPreview` for `undo: true`) and `reloadConfig` (`src/hub/daemon.ts`) and the registry in `src/hub/settings.ts`. A new editable setting is a registry entry, never a new write path; a key that chooses what the hub runs or where secrets or task text go joins `NEVER_EDITABLE` instead. Writes go only to `.agenthub/config.local.json` and `.agenthub/routing.local.toml` through `store()`: the loader's own validation first, temp file and rename, never a file git tracks. Authority comes from the dashboard session (`DashboardSession`, `src/hub/ui.ts`) or the console role, never from the request body; `ui_action` from the manager is an ordinary session. Never return a setting's value in an event or notice except through `recordSetting`.
|
|
36
|
+
- Settings (#269): the dashboard's `setting`, `setting_undo`, `setting_preview` and `setting_reload` actions and the console-role `settings_get|set|undo|preview|reload` requests go through `settingsSet`, `settingsUndo`, `settingsPreview` (`settingsUndoPreview` for `undo: true`) and `reloadConfig` (`src/hub/daemon.ts`) and the registry in `src/hub/settings.ts`. A new editable setting is a registry entry, never a new write path; a numeric operational tunable (#489) is a fixed row in `OPERATIONAL_NUMBERS` (`src/hub/operational-settings.ts`) plus its explicit `config-apply.ts` leaf (`operationalNumberCheck`), its `DEFAULT_CONFIG` default and a reader asked at each use, never registered from a file, request or environment variable; a key that chooses what the hub runs or where secrets or task text go joins `NEVER_EDITABLE` instead. Writes go only to `.agenthub/config.local.json` and `.agenthub/routing.local.toml` through `store()`: the loader's own validation first, temp file and rename, never a file git tracks. Authority comes from the dashboard session (`DashboardSession`, `src/hub/ui.ts`) or the console role (for `settings_set` and `settings_preview` of an `operationalSetting` or `operationalRosterSetting` key, also an admitted conductor), never from the request body; `ui_action` from the manager is an ordinary session. Never return a setting's value in an event or notice except through `recordSetting`.
|
|
32
37
|
- Structural config edits (#689): `src/hub/config-tier.ts` is the one tier rule (editor, dashboard refusal and `structuralLeaf` in `authorizeConfigLeaf`), `src/hub/config-edit.ts` plans and writes (loaders first, revision checked twice, bounded backups, atomic), `src/hub/config-edit-host.ts` owns authority, the typed tier 3 confirmation, the `config_edit` event and the scoped publish. The daemon only supplies ports (`createConfigEditor` in `daemon.ts`); never validate, write or classify in `daemon.ts`. A new editable structural key is an entry in `config-tier.ts` and nothing else; a key that chooses what the hub runs or where secrets go is tier 3 and is never offered to the dashboard (the hub refuses it, the page also hides it). The audit event and every notice carry key paths and value hashes, never a value. `structural` reaches `authorizeConfigLeaf` only through `structuralScopes` for the one scoped reload. `sdlc.toml` is read once at start, so it joins `pendingRestart` through `allPendingRestart()`, never through `reloadConfig`. A new ahub settings subcommand that writes is classified `console` by default; keep it that way.
|
|
33
38
|
- Live reload (#439): `reloadConfig` is the only publication path. Settings writes and undo, `ahub settings reload`, the dashboard reload and the debounced watcher of the four `CONFIG_SOURCE_FILES` (`src/hub/config-reload.ts`) all reach it; `prepareConfiguration` validates the complete candidate before anything is published, and recovery or shutdown holds it. A write or undo passes its own key as `scope` and publishes only that path, and `acknowledgeConfiguration` records only the accepted paths, so other edits stay pending. Every changed key, inside the registry or not, is decided by `authorizeConfigLeaf` (`src/hub/config-apply.ts`): a refused key keeps its accepted value and is reported by name while the others apply, `roster.*` applies only through its own scoped roster write, and a change of the settled planner or conductor holders (`roleHolders` over `resolveSettledRoles`) is guarded even when a role list shrinks. Outside a named write, only the console's `settings_reload` (authority `terminal`) adopts guarded file edits, by sending back the `confirmation` token bound to the candidate `fingerprint` and `configRevision`; changed bytes or accepted state refuse it, and a watcher, dashboard or settings session never confirms. An undo that names `key` and `undoDigest` writes only while `pendingUndoIdentity` still matches both. Keep config-source snapshots plain and cloneable; classify dynamic ids with validated running/source identity scopes, never suffix discovery or a builtin fallback. Compare restart-bound declarations with the running config on every reload, never a pending desired value accepted by an earlier reload; derived defaults stay bound to that running declaration. A `restart` key (`configApply`) keeps its running value and is listed in `pendingRestart`; reload notices and events carry key names and reasons, never values. The console settings editor (the Settings panel, or the Peers panel's `w` WIP shortcut, which opens only an existing `tasks.wip_limits.<peer>` row for a rostered peer listed in status `taskWip`) stays bound to the panel that opened it (`SettingsEdit.panel`) and arms a write only from a preview reply that matches that panel and its open edit's token, key and value; pasted text never confirms. Console Tasks backlog actions (`p`, `a`, `e`, `R`, `c`) only prefill person CLI commands (`backlog pull-ready|plan|return`, `task cancel`) with the row's immutable id and its planning or flow revision; `validateLockedRow` rechecks the row's state and revision at Enter and again at `y`, and no console path writes the board directly (#532).
|
|
34
39
|
- Planner setting (#755): `planner` is one id or a list of one to eight distinct eligible ids, validated only by the `planner` row of `projectSettings` (eligible declared instances join its `values`) and `checkSettingValue`, in `loadConductorPolicy` and `validateRawConfig` alike. Every `resolveSettledRoles` call (`settlePlanner`, `roleHolders`) passes the same `plannerEligible` predicate built from that row, so reload guards compare holders the way the running map settles them.
|
|
@@ -49,7 +54,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
49
54
|
- Adding a native peer requires testing the command emitted by the actual recovery driver, not only its inspection or launch helpers. Never let a generic non-Codex branch treat a new peer as Claude.
|
|
50
55
|
- Cross-version recovery must use the authenticated source protocol for prepare/commit/abort and the target protocol after startup. Prove the transition against a real prior release before claiming compatibility.
|
|
51
56
|
- Preserve `task_cancel` target-feature preflight before source holds, capture or commit. Reuse stored target features only after verifying the same retained root and digest; legacy receipts still capable of commit must obtain feature proof. Do not apply pre-commit compatibility checks to sources already past commit. Every prepare, including the lease renewal before a terminal close and after a resume, sends the operation's stored `targetFeatures` (#478).
|
|
52
|
-
- A recovery target-feature floor (`task_cancel`, `peer_instances`, `role_stages` while a declared Claude-kind instance holds `implementer`, `task_planners` while a list/instance planner is configured or `Board.hasPlannerState()` finds a task/release binding or plan-review authorship marker, `release_trains` while a configured train is active or frozen, `reviewer_pool` while a fresh reviewer or `review.pool` is configured, `configured_acp` while the project declares `adapters`, `external_adapter_modules` while external module definitions are loaded) lives in `incompatibleRecoveryTarget`, which every existing check site calls, and its name in `DAEMON_FEATURES`; never check a floor at one site only.
|
|
57
|
+
- A recovery target-feature floor (`task_cancel`, `peer_instances`, `role_stages` while a declared Claude-kind instance holds `implementer`, `task_planners` while a list/instance planner is configured or `Board.hasPlannerState()` finds a task/release binding or plan-review authorship marker, `release_trains` while a configured train is active or frozen, `reviewer_pool` while a fresh reviewer or `review.pool` is configured, `configured_acp` while the project declares `adapters`, `external_adapter_modules` while external module definitions are loaded, `external_adapter_instances` with `external_adapter_modules` and `peer_instances` while an external module-kind instance is declared, `conductor_recovery` while `Board.hasConductorManualState()` finds open conductor manual-assignment intent) lives in `incompatibleRecoveryTarget`, which every existing check site calls, and its name in `DAEMON_FEATURES`; never check a floor at one site only.
|
|
53
58
|
- Defer hub notices only for an authenticated recovery `prepare` or `commit` whose target features include `recovery_deferred_hub`, or for a restored daemon. Otherwise `setRecoveryHold(true)` moves the deferred notices into queues before commit records expected integrity, and a bare `fenceRecovery` defers nothing. Abort and lease expiry release the bus hold before clearing the operation; if that release fails, keep the operation and let expiry retry on its 1 s timer (#478).
|
|
54
59
|
- Resume a prepared `commitSent` source that is unavailable or database-busy inside the existing bounded shutdown poll; never re-send commit, re-prepare or close terminals again on that path. Require confirmed `stopped` before target startup, refuse a replacement instance, and retain the prepared receipt on timeout.
|
|
55
60
|
- Keep restoration receipts explicitly tagged as pending, waiting with origin/binding, done with binding, or failed. Project waiting origins as `waiting:pending` and `waiting:failed`; preserve the chosen binding and origin across interruption instead of inferring completion from an object.
|
|
@@ -71,7 +76,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
71
76
|
- The managed Claude hook (`src/cli/facts-hook.ts`) may add `permissionDecision: "allow"` on `PreToolUse` (for `ask-when-needed` only what `projectFileTool` accepts) and, since #636, `permissionDecision: "ask"` for a call the hub classed (`actionClasses` on its `facts` reply, in every permission mode, and an `ask` replaces any `allow`); it never adds `deny`, and a value that is present but unreadable asks. A hub that is down, slow or refuses decides nothing, so Claude's own policy stays in force. The daemon puts `permission` in a `facts` reply only for the hook of this instance's recorded `ahub claude` launch (the same `launchId`, not `--unattended`) or of a headless Claude implementer's current child (rule below). For a declared instance with a `worktree` it answers `ok: false` without a current verified owner root (`ownerWriteRoot`), never a canonical-tree fallback, and that root is the reply's `projectRoot`. A permission-only hook (`hookPurpose`) never drives busy state (#242). With `AGENTHUB_HOOK_EVENT` set, the hook serves only the peer definition's `hooks.factsHook` port: input over 1 MiB or refused by its `decode` decides nothing, it sends `native_facts`, and it outputs only the port's `encode` (agy: an ephemeral message on SessionStart/PreInvocation, never a permission decision). The daemon refuses plain `facts` from a peer whose definition has `factsHook`.
|
|
72
77
|
- `ahub claude` (`src/cli/launch.ts`, #270) publishes `claude-launch.json` (`claude-launch.<id>.json` for an `--as` instance) for every launch against a running hub, atomically and without a lock: hooks, permission status, session observation and recovery read its `launchId`, so never make it wait for native-proof state. The native child's pid and signature go only into the private `claude-native-<binding hash>.json`, keyed by the exact launch record, written once under `claude-launch.lock` and removed at that child's own exit; never put them in `claude-launch.json`, where a late callback of an earlier launch could replace a newer record. Crash cleanup of a previous launch's settings file (`cleanupStaleClaudeSettings`) needs the launcher gone by `processLiveness` and that native proof, and for a `direct-child` proof the native pid absent from an available `processTable`: a direct child may exec in place, so its signature certifies nothing. A script or interpreter gets no proof and keeps its settings file.
|
|
73
78
|
- Read a `claude-launch*.json` record in the daemon only through `readClaudeLaunchProof` (`src/hub/claude-launch-proof.ts`: no-follow, 16 KiB cap, this instance only), never by parsing the file. An `--as` launch records the `executionCwd` its entry got from `resolveExecutionCwd`; the entry never chooses or creates a worktree.
|
|
74
|
-
- Terminal Claude implementer roots (#525-5): bind `executionRoots`/`executionOwners` only at a hello where the resolved worktree is not the project root, the private launch record names this hello's `launchId` and that root, and the channel's reported `cwd` resolves to it; bind it to that socket and session generation. `currentExecutionOwner` rechecks the record and the worktree in each event-loop turn (reuse only within synchronous code), and `revokeTerminalExecution` drops the proof on a mismatch, detach or new claim. Missing or unknown evidence keeps the channel attached but never grants isolation; the channel omits `cwd` when it cannot resolve its own.
|
|
79
|
+
- Terminal Claude implementer roots (#525-5): bind `executionRoots`/`executionOwners` only at a hello where the resolved worktree is not the project root, the private launch record names this hello's `launchId` and that root, and the channel's reported `cwd` resolves to it; bind it to that socket and session generation. `currentExecutionOwner` rechecks the record and the worktree in each event-loop turn (reuse only within synchronous code), and `revokeTerminalExecution` drops the proof on a mismatch, detach or new claim. Status (`ownerIsolation`, `ownerIsolationNotes`, `tasks.stalled({ ownerIsolation })`) reads only `ownerIsolationProjection`, the retained binding plus owner, generation and socket checks: never add a terminal-root Git or filesystem validation to the status path, and never authorize admission, facts grants or recovery from that projection. Missing or unknown evidence keeps the channel attached but never grants isolation; the channel omits `cwd` when it cannot resolve its own.
|
|
75
80
|
- A headless Claude implementer's tools socket (#525-4) is admitted only with the launch id of that peer's current child (`ownedTools`) and a verified worktree root, and bound as `toolsOwner`: every request and every reply rechecks `currentImplementerTools` and fails closed. Its `task` ops are limited to `IMPLEMENTER_HUB_TOOL_NAMES` and run inside `childToolAuthority`, so `Tasks.requestCheck` refuses before an effect once the child changed. Its `inbox` refuses, before `bus.pull`, a batch holding a private item or one attributed to another task. Its person `permission_request` path is `classed: true`, offers allow-once and deny only, and denies on expiry, abort or a changed child. Automatic Bash permission uses only the fixed host-wrapper contract in `adapters.md`, including the explicit client protocol capability; a raw native allow is never an automatic grant. Its facts get `permission` only for the current child's launch id, session id and this instance.
|
|
76
81
|
|
|
77
82
|
- Keep SQLite recovery contention in the existing blocked receipt as retryable `database_busy`, retaining bounded throw-site frames and the ordinary `ok: false` rid reply. Use a neutral control error; the receipt's next actions name resume or the partway disposition. Clear a superseded failure on abort, disposal, fresh-session choice or a refused resume of a disposition. After a commit request, treat busy inspection as unconfirmed and keep polling only inside the existing shutdown bound.
|
|
@@ -106,7 +111,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
106
111
|
- Instance `model` and `review_scope` (#844, #852): `validateInstances` admits `model` outside Jcode only for a kind whose `headlessVariant` declares the fixed-builtin `instances.modelSelect` (the headless Claude implementer), only on a headless writer and only as one `NATIVE_WORD`; the daemon passes it to that peer's native turns (`claudeImplementerArgs`) and shows it as `modelSource`, and a change applies at the next daemon start. `review_scope` is admitted only on a fresh reviewer, as `code` or `full` (verdict rules: `tasks.md`). Both are `TIER3_INSTANCE_FIELDS` (`src/hub/config-tier.ts`), which `planConfigEdit` also reads to refuse a whole-instance replacement that would drop one: a new instance field that chooses a home, account or model, or widens what a verdict may skip, joins that list.
|
|
107
112
|
- Channel-kind instances (#525 R1, `validateInstances`): a terminal instance declares roles within its kind's `instances.roles` (Claude: planner, implementer, verifier, reviewer). An `implementer` or `verifier` needs its own `worktree`; a planner or reviewer refuses one. A headless channel-kind instance is either a reviewer with roles exactly `["reviewer"]` or (#525-4) a writer whose roles include `implementer` and hold only `implementer` and `verifier`: it resolves to the kind's `headlessVariant`, needs its own `worktree` and accepts `permission_mode` `ask` or `ask-when-needed` only. `planner` needs `projection.plannerEligible`: `validateInstances` refuses it, `loadConductorPolicy` drops it with the ceiling notice, and `effectiveInstanceRoles` filters it for instance-keyed `roles`. A declared worktree never proves owner isolation (rule: `tasks.md`).
|
|
108
113
|
- Every start resolves `resolveInstanceExecutionCwd` and passes that `executionCwd` as adapter cwd, sandbox, grant and Pi session root; set `executionOwners` only for the adapter actually started, or at a verified terminal Claude hello (rule above). Its git calls share one `GIT_BUDGET_MS` allowance through `gitWithin` (#225): a call that finds it spent refuses without starting git; never give each call its own timeout. Extra-instance native state is per instance, while its MCP binding keeps the canonical hub state dir.
|
|
109
|
-
- Recovery: record an extra Codex launch row only after the start reply supplies `executionCwd`; keep `projectRoot`, `executionCwd` and `terminalRoot` separate and never bind an extra by kind metadata. Abort and archive validate receipts without current declarations; restore, fresh and reattach require them; a screen that only shows a recorded receipt reads it with `restoreReceiptStatus(value, { authority: "historical" })`, never for a decision. `instanceRecoverySourceRefusal` refuses before effects when the source lacks `external_adapter_modules` while `adapter_modules` is declared, and lacks `configured_acp` or `peer_instances` while a configured or declared id is not offline or the source's peer states are unreadable; a source whose such ids all read offline proceeds. Extra-instance turns write `turn_workspaces` in the `turns` transaction and leave the legacy snapshot fields unknown; undo requires the same trusted declaration and validated root.
|
|
114
|
+
- Recovery: record an extra Codex launch row only after the start reply supplies `executionCwd`; keep `projectRoot`, `executionCwd` and `terminalRoot` separate and never bind an extra by kind metadata. Abort and archive validate receipts without current declarations; restore, fresh and reattach require them; a screen that only shows a recorded receipt reads it with `restoreReceiptStatus(value, { authority: "historical" })`, never for a decision. `instanceRecoverySourceRefusal` refuses before effects when an external module-kind instance is declared and the source lacks `external_adapter_instances` (with `external_adapter_modules` and `peer_instances`) or reports no matching `externalInstance` binding for it (offline ones included), when the source lacks `external_adapter_modules` while `adapter_modules` is declared, and lacks `configured_acp` or `peer_instances` while a configured or declared id is not offline or the source's peer states are unreadable; a source whose such ids all read offline proceeds. Extra-instance turns write `turn_workspaces` in the `turns` transaction and leave the legacy snapshot fields unknown; undo requires the same trusted declaration and validated root.
|
|
110
115
|
|
|
111
116
|
- Name every Claude per-peer state file through `claudeStateFile` (`src/hub/claude-state.ts`) with that peer's id, never a hard-coded builtin name; pick a peer's `terminal-recovery.json` row only through `ownTerminalRow`. `sessionPointers` in `src/cli/reset.ts` must match any new per-peer pointer (#450 I3). Claude state files belong to Claude peers only: select those peers by `transport === "claude-channel"`, never by `identity.mcp === "channel-plugin"` or `turnEnd === "native-hook"` (agy has both), and `claudeStateFile` throws for any other id (#624).
|
|
112
117
|
- A declared Claude channel extra (one that resolves to the `claude-channel` definition) is never hub-started: keep it out of `STARTABLE_PEERS`, instance start modes and `RESERVED_IDS`. A launcher admits `--as <id>` only when the running daemon's status `activeInstances` lists it (`requireActiveInstance`), never from the config on disk.
|
|
@@ -119,3 +124,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
119
124
|
|
|
120
125
|
- Channel tool-result hints (#493, #221): `withPendingHint` (`src/adapters/claude-channel.ts`) appends fresh `delivery_holds` counts, and drops the "call hub_inbox" advice whenever the same reply carries a held-delivery hint, because `bus.pull` refuses under that hold and the advice would send the session into a refused read. A `hub_inbox` result that already says how many messages remain unread adds only the important count, never a second queued count. Do not add a Claude client-version gate on steer without a probe of the older client and an optional hello field that carries the version (background: README).
|
|
121
126
|
- 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`. 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.
|
|
127
|
+
- 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.
|
|
128
|
+
- 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.
|
|
129
|
+
- Corpus (#510a, `src/hub/corpus.ts`, `corpus-config.ts`): `corpus.paths` is machine-local and `corpus` is never editable from settings. Keep `corpus-config.ts` free of file-system imports, because the config schema (`config-apply.ts`) reaches the Claude channel bundle. A refusal of a root is a reason string, never a thrown error and never a partial read: evidence the check cannot read refuses, and only a completed read certifies a folder. The vault check is `.obsidian` at the real root, in every ancestor and anywhere below (any entry of that name), always on the `realPath`. A folder the pii, deny or local.deny rules protect is never listed, so it cannot be shown vault-free: the root that holds it is refused, never certified with that subtree skipped (a protected file is only skipped).
|
|
130
|
+
- Domain packs (#513a, `src/hub/packs.ts`): `ahub init --profile` resolves a shipped profile name first, then a pack through `loadPacks` (shipped: `templates/packs/` listed in `src/hub/shipped-packs.ts`; a project pack in `.agenthub/roles/<name>.pack.json` replaces a shipped one of the same name); keep `parsePack` closed (exact fields, bounded, plain ASCII, ids through `criterionId` from `src/hub/workflow.ts`) and never give a pack a field that carries a command, a path or an authority. The preview names a pack by ids only, never rubric or check text, and the apply goes through `packOverrides`, which `resolveProfile` validates. Reads use `lstat` and `O_NOFOLLOW` and refuse a hardlinked file, as the role catalog does. A pack whose roles are not in `loadRoleCatalog` is refused whole.
|
|
@@ -12,9 +12,11 @@ Scope: the hub-native local worker, its tools, path guard, denylist and seatbelt
|
|
|
12
12
|
- SBPL strings go through `sbplString` (the plain `"..."` form). In the raw `#"..."` form a backslash escapes nothing, so a `"` in a path ends the literal and the whole profile fails to parse.
|
|
13
13
|
- Seatbelt sees absolute paths, so `local.deny` entries are anchored under the project root.
|
|
14
14
|
- `guardPath` walks with `lstat`, never `existsSync`, which follows symlinks.
|
|
15
|
+
- `guardPath` judges the denylist and the hub-owned names (`.git`, `.agenthub`) on two names, the resolved one and the one the caller typed (#541): Bun's realpath sometimes names another hardlink of the same file, so the resolved name alone let a denied `.env` hardlink through about 1 call in 100 on macOS. A refusal on either name stands; the typed name only adds refusals, and only a typed name INSIDE the project is judged (`insideRoot`, #555, the same bound `Facts.rel` and the deliverable vetting use: a name like `../o/.env` that links into the project is no project path, and judging it made main red after #943 and #944 met); containment, the `data.readonly` hardlink refusal and `matchesPolicy` are unchanged. `test/path-policy.test.ts` runs 5000 iterations per layout, because a single call passes the unfixed guard 99 times in 100; keep the loop counts.
|
|
15
16
|
- git arguments never pass through `guardPath`; `gitArgsProblem` refuses absolute paths, `..`, `--no-index` and denylisted `rev:path` forms.
|
|
16
17
|
- A worker turn builds its messages in a local array and joins the history only as a whole. Never push to `history` mid-turn: one tool call without its result poisons every later request.
|
|
17
18
|
- After a tool with side effects ran, a failed turn is reported, never redelivered.
|
|
19
|
+
- A turn with any remote envelope (`from` containing `@`, rooms #941) never joins `history` and never reaches `capture.observe`, like a PII turn, and every failure it reports (answer, delivery reason, log line) says only `remote turn failed`. Keep all three when you touch the turn's settlement or the tool loop.
|
|
18
20
|
- `ctx.permit` answers a `ToolApproval` (`true`, `false`, `"expired"`, `"aborted"`, #253): only `true` permits an effect, so never test it for truthiness. A tool returns `approvalError`'s text for the rest: a denial, the expiry (which tells the model not to retry) or the withdrawal. A turn that two unanswered approvals ended (`ApprovalWaitStop`; counting rule: `adapters.md`) completes its missing tool results, settles its delivery `completed` with the reason and is never handed back for replay, also when no tool had a side effect.
|
|
19
21
|
- `Facts` (`src/hub/facts.ts`, #428) names a large file whose ctime is under `RACY_MS` by its git blob id: in `load()` and `observe()` only through `racyBlob()` (per-file `RACY_HASH_MAX_BYTES`, per-store `RACY_BURST_BYTES` per event-loop turn, memo cleared at `boundary()`), past which it keeps the stat-only stamp and makes no claim; `atHead()` keeps its own `HASHED_BYTES` bound, `tree()` reads large files by stat only, and `largeBlob()` caches only settled stamps.
|
|
20
22
|
- 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,3 +26,5 @@ Scope: the hub-native local worker, its tools, path guard, denylist and seatbelt
|
|
|
24
26
|
|
|
25
27
|
- 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.
|
|
26
28
|
- 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.
|
|
29
|
+
- `web_fetch` (#515b, `src/local/web-fetch.ts`) is opt-in like `doc_read` (never in `TOOL_SCHEMAS`; offered only when `local.web_fetch.allow` names a host) and carries the owner's PII rule: it is refused for the WHOLE turn of a PII task, whatever the host, and that refusal is the first thing `webFetch` does, before the URL, the list or the proxy is read, so the answer and the timing do not depend on the request. Never move another check in front of it, and never give it a way to accept a PII turn "for a clean URL": the URL or a query could carry PII to the web, which `pii.paths` and `sanitize()` do not cover. It runs only through its OWN `startEgressProxy` (`webEgress` in `daemon.ts`, list `local.web_fetch.allow`), never the bash proxy: do not merge the two lists or reuse the bash proxy for it. The model sets only `url`, `offset` and `limit`; keep the request a bare GET with no model-chosen header, body or method, keep redirects manual and re-checked, and keep the result and the URL out of `capture.observe` and the research observation on every turn. The pure host checks live in `src/local/web-fetch-config.ts` because the config schema reaches the Claude channel bundle: keep that file free of file-system and process imports.
|
|
30
|
+
- `doc_read` (#515a, `src/local/doc-read.ts`) is an opt-in tool, never part of `TOOL_SCHEMAS`: the worker offers `DOC_READ_SCHEMA` only when the daemon passes `tools.docRead` (`local.doc_read`, machine-local, never editable from settings), and `runTool` refuses it without the port. It goes through `guardPath` (read), then `pii.paths` by path, then the converter (`TextProjections`, the person's `text_projection`), then `signals.pii_patterns` by text, then `sanitize()` and the output cap; a PII task turn passes the first two screens, an ordinary turn does not. Its result is never passed to `capture.observe` or `onTool`: keep both exclusions when you touch the tool loop in `src/adapters/local-worker.ts`.
|