@staix/agent-hub 0.12.41 → 0.12.43

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +7 -4
  3. package/docs/agent-notes/adapters.md +4 -2
  4. package/docs/agent-notes/bus.md +2 -0
  5. package/docs/agent-notes/daemon.md +7 -2
  6. package/docs/agent-notes/tasks.md +1 -0
  7. package/docs/agent-notes/tests.md +4 -2
  8. package/docs/operations.md +98 -24
  9. package/docs/quickstart.md +11 -3
  10. package/docs/sdlc.md +1 -1
  11. package/docs/security.md +3 -3
  12. package/docs/smoke.md +1 -1
  13. package/docs/specs/2026-09-19-agent-hub-design.md +51 -9
  14. package/docs/verification/2026-10-20-0.12.42.md +48 -0
  15. package/docs/verification/2026-10-21-0.12.43.md +50 -0
  16. package/docs/verification/2026-10-21-agy-1.4.3-t0.md +65 -0
  17. package/docs/verification/2026-10-21-pii-screen.md +14 -0
  18. package/docs/verified.json +13 -13
  19. package/package.json +1 -1
  20. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  21. package/plugins/agent-hub/server.js +79 -29
  22. package/src/adapters/agy/entry.ts +11 -1
  23. package/src/adapters/agy/hooks.ts +7 -2
  24. package/src/adapters/pi.ts +46 -16
  25. package/src/cli/completion.ts +173 -0
  26. package/src/cli/console-state.ts +107 -20
  27. package/src/cli/console.ts +9 -6
  28. package/src/cli/help.ts +3 -2
  29. package/src/cli/identity.ts +2 -2
  30. package/src/cli/main.js +5 -1
  31. package/src/cli/main.ts +6 -0
  32. package/src/cli/terminal-recovery.ts +18 -9
  33. package/src/hub/daemon.ts +138 -33
  34. package/src/hub/hub-tools.ts +1 -1
  35. package/src/hub/main-checkout.ts +2 -2
  36. package/src/hub/manager.ts +10 -2
  37. package/src/hub/settings.ts +1 -1
  38. package/src/hub/snapshots.ts +252 -17
  39. package/src/hub/tasks.ts +16 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
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.
4
4
 
5
+ ## Unreleased
6
+
7
+ - Incoming HEAD moves during open turns no longer produce false concurrent-edit warnings or attributed undo paths; bounded per-worktree reflog evidence distinguishes them from peer commits and sequencer replay changes, which stay attributed unless ambiguous. Rebase/pull --rebase start checkout is incoming; uncommitted edits carried through autostash or checkout -m are never excluded. Real concurrent change sets publish at most one notice per affected task; snapshot Git work shares a 250 ms burst budget and records unknown boundaries on exhaustion. Status reuses a checkout-branch probe only within the current synchronous burst. Pi terminal reconciliation uses the bounded existing owner monitor instead of spawning `ps` on every read; effect admission and teardown still verify fresh process identity (#576).
8
+
9
+ ## 0.12.43
10
+
11
+ A busy daemon no longer stalls on git HEAD movement under open turns (#576); upgrade restore, task assignment and the agy peer get fixes, `ahub completion` arrives, and the verification gate changes for contributors (#609, #610).
12
+
13
+ - The daemon stays responsive when the checkout's HEAD moves (fast-forward, checkout, worktree merge) while turns are open: snapshot and diff Git calls in one burst share a 250 ms budget and an exhausted budget records unknown evidence, concurrent-edit notices go out once per affected task instead of once per task pair, and HEAD moves are classified from the worktree's HEAD reflog so an incoming checkout, merge or fast-forward is not attributed to a peer while the peer's own commit, amend, rebase replay or cherry-pick still is; ambiguous history stays unknown and paths still dirty at turn end are always kept. On a scratch burst `status` went from 21.3 s to 0.27 s and false conflicts from 144 to 0 (#576).
14
+ - An upgrade or restart no longer blocks on a restored Claude session whose launch record or terminal identity appears a few seconds after its terminal reads idle: the mapping is re-read until the restore deadline, and only a positive contradiction or the deadline blocks (#599).
15
+ - `hub_task_assign` and `ahub task assign` refuse with the routing reason (offline, not attached, excluded, plan review missing ...) instead of returning the unchanged task, and a proposal whose named owner could not take it says why in its result (#601).
16
+ - `ahub completion zsh|bash|fish` prints a completion script; candidates come from the help entries through the hidden `ahub __complete`, so they cannot drift from `ahub help` (#603).
17
+ - agy: native file reads are recorded as peer facts, for agy 1.4.2's `view_file` and 1.4.3's `read_file`, with a line range counted as a partial read (#555). agy 1.4.3 stays unverified (T0 re-run, #267); in its TUI the hub tools appear from the second turn (#607).
18
+ - The console's Peers tab shows each peer's planner WIP against its limit, and `w` edits the limit through the existing settings flow, for declared instances too; the Tasks tab toggles a ranked backlog view (`b`) and adds, pulls, plans, reserves, returns and cancels backlog work from the row, each action bound to the row's id and revision and confirmed with `y` (#532).
19
+ - A manager open that loses the race to another manager stops the process it spawned (#584).
20
+ - Contributors: CI runs per pull request (`check-pr-<number>`, a newer pending run replaces the older) instead of one repository-wide queue; `scripts/check.sh --changed` runs only the tests a change touches, and the gate pins its test environment (`GIT_CONFIG_GLOBAL=/dev/null`, no `NO_COLOR`, `TERM=xterm-256color`, temp root under the OS per-user temp dir); the full gate is CI's (#609, #610). Two timing-dependent tests were fixed (#617, #618). The live PII screen calibration is recorded with the served model id (#198).
21
+
22
+ ## 0.12.42
23
+
24
+ `ahub agy` starts again after the Antigravity CLI updated itself (#589); the rest is contributor tooling (#591, #593, #595).
25
+
26
+ - `ahub agy` accepts the Antigravity CLI 1.4.x line instead of exactly 1.4.2: 1.4.2, the T0-verified version, launches as before, another 1.4.x patch launches with one stderr warning naming its version, and any other version is still refused before a hub connection or plugin write. agy updates itself, and 0.12.41 refused 1.4.3 (#589).
27
+ - `scripts/build.mjs` builds the plugin bundle with the Bun named in `.bun-version`: when the running Bun differs it re-runs itself once through `bun x bun@<pinned>`. A self-updated local Bun made the bundle check call a clean `main` stale and its remedy rebuild with the wrong Bun (#591).
28
+ - The test gate's whole-suite watchdog cut off passing runs: hosted CI now gives macOS 1260 s and Linux 1080 s inside a 25-minute job, and the local default is 960 s; the 20 s per-test timeout is unchanged (#593).
29
+ - `test/terminal-recovery.test.ts` no longer records an ambient `CLAUDE_CONFIG_DIR` from the shell running the tests (#595).
30
+
5
31
  ## 0.12.41
6
32
 
7
33
  A one-fix release for a 0.12.40 regression.
package/README.md CHANGED
@@ -5,7 +5,7 @@ Codex, Kimi Code, Pi and a hub-owned local worker (Qwen Code, Jcode and Antigrav
5
5
  optional) share a message bus and a task board per project and one memory on the
6
6
  machine, each attached through its own native interface.
7
7
 
8
- Status: 0.12.41, control protocol 16. Bun + TypeScript, one loopback daemon per
8
+ Status: 0.12.43, 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
  ## What it does today
@@ -42,7 +42,8 @@ project. Site: [agent-hub.staix.net](https://agent-hub.staix.net).
42
42
  running sessions across versions. Settings reload without a restart where their
43
43
  apply class allows; guarded values wait for a person's confirmed
44
44
  `ahub settings reload` in a terminal. The console supports revision-checked task
45
- assignment/unassignment and delivery-queue resolution. Peers with independent
45
+ assignment/unassignment, delivery-queue resolution, per-peer planner WIP limits
46
+ and backlog management from the Tasks tab. Peers with independent
46
47
  tool turns can request one uncertain redelivery; the delivery-only local worker
47
48
  and headless ACP peers leave held deliveries for the person to resolve.
48
49
 
@@ -85,14 +86,16 @@ Since 0.12.39 `ahub jcode` opens Jcode's native TUI on the hub-owned ACP session
85
86
  default (#464); `--mode headless` or `ahub settings set peers.jcode.start_mode headless`
86
87
  keeps headless startup. On 0.12.39 the TUI launcher stopped after the first hub turn;
87
88
  0.12.40 opens it again (#564). On 0.12.40 `ahub codex` in a recorded Orca terminal
88
- failed with `invalid remote address`; 0.12.41 starts it again (#586).
89
+ failed with `invalid remote address`; 0.12.41 starts it again (#586). Since 0.12.42
90
+ `ahub agy` accepts the Antigravity CLI 1.4.x line (1.4.2 verified; other patches warn),
91
+ because agy updates itself and 0.12.41 refused 1.4.3 (#589). Since 0.12.43 agy's native reads are recorded as peer facts on 1.4.2 and 1.4.3 (#555); 1.4.3 stays unverified, and in its TUI the hub tools appear from the second turn (#267).
89
92
  Cancellation needs a daemon advertising `task_cancel`; controlled downgrades
90
93
  refuse while cancelled rows exist. Live-test delegated CLI commands need a daemon
91
94
  advertising `cli_audit_delegated` and refuse with an upgrade remedy otherwise
92
95
  (#410). For local/Pi implementation, opt in to an available
93
96
  `checks.runner = "container"`; issue-backed owners also need authenticated
94
97
  spec access. PII work and designated reviewers retain their separate exemptions.
95
- See [release verification](docs/verification/2026-10-20-0.12.41.md) for the live legs
98
+ See [release verification](docs/verification/2026-10-21-0.12.43.md) for the live legs
96
99
  still open, including #418 AC8, #438 AC7, the native console settings leg (#439 AC5),
97
100
  real upgrades with reattach (#463) and with a managed terminal and cancelled tasks
98
101
  (#478), a live headless Claude review (#223), a Claude session over 32 MiB (#517), a
@@ -70,7 +70,9 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
70
70
  - Report reviewer failures (`onUnavailable`: `start_failed`, `quota`, `no_verdict`) only after the child's group is confirmed gone and only while its generation is current: a person's stop owns its hold and never reroutes. `quota` comes only from the backend's structured `failure(event)`, never from vendor text. An NDJSON frame over 256 KiB or more than 4 MiB of output aborts the review as `needs_review`.
71
71
  - Codex reviewer isolation (`src/adapters/codex-review-isolation.ts`): accept only the pinned `codex-cli` version; prove model-free that Apps, plugins and hooks are disabled (`features list`), ambient MCP servers are replaced by disabled stubs, and the hub server alone is enabled with exactly `hub_review` and `hub_task_show`; run the native sandbox fixture probe before every launch; hash the binary once and fence binary and config files by metadata. Credential denies come only from the bounded `HOST_CREDENTIAL_PATHS` and `HOST_CREDENTIAL_ENV` lists, denied at the lexical and canonical path; refuse glob syntax or a root inside the checkout. A root that is dangling or unreadable refuses too; the reason names only its path class (`~/.docker`, `$DOCKER_CONFIG`), never a path or contents. Never pass the control token in a reviewer child's argv, env or MCP config: keep the hub state dir in its `privatePaths`.
72
72
 
73
- - 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` other than the pinned 1.4.2, and a hub without the `native_hook_launch` feature.
73
+ - 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` outside the 1.4.x line (1.4.2 is T0-verified; other 1.4.x patches launch with a stderr warning, #589), and a hub without the `native_hook_launch` feature.
74
74
  - 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.
75
- - 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.
75
+ - 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).
76
76
  - `terminalOwner(launchId)` (`src/hub/peers.ts`, #482) answers only for the adapter's current native TUI owner bound to that launch (Codex: the claimed link with that `launchId`, both sockets open; the launcher sends the id as the upgrade's bearer token through `--remote-auth-token-env`, because `codex --remote` refuses any path or query (#586); Pi: a started, verified TUI owner on its requested session); a start acknowledgement is never ownership. A Codex link that is not the claimed one or has a closed socket never adopts a thread.
77
+
78
+ - Pi terminal ownership reads only the existing monitor's current pid/signature/session/file/token observation, expiring after 1 s; unknown verification withdraws proof. Keep fresh shared process-signature checks at tool/effect admission and teardown, and never revive terminal proof across detach, offline, stop or a changed owner/session (#576).
@@ -35,3 +35,5 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
35
35
 
36
36
  - `relevant` may return a string: the envelope is dropped with that string as its journal discard reason. Read relevance only through `irrelevance()`.
37
37
  - A review-only instance (a declared extra whose roles are exactly `reviewer`) receives only person messages, hub `review` items and hub `task` items addressed to it. Keep `reviewOnlyItem` in both `silence` (at send) and `relevant` (at delivery) (#223).
38
+
39
+ - Concurrent-edit fan-out is linear in affected tasks: retain each notice's own-task stale/owner fence, but never persist the same change-set notice once for every task pair. Preserve per-task-pair research evidence separately (#576).
@@ -11,7 +11,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
11
11
  - 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).
12
12
  - `peer_stop` is console-only and stops only a hub-owned headless ACP, Pi or local runtime through its adapter. Refuse native-terminal peers and recovery-held mutations; serialize with starts and permission changes, and refuse a recovery `prepare` while a stop runs. An admitted stop holds delivery through `BusOptions.deliveryHeld` before any teardown await (never a console pause that resume can undo, or a global recovery hold that blocks unrelated peers) and withdraws the peer's pending approvals before the adapter stops. A requested stop holds an in-flight delivery as `needs_review` with an ids-only stop reason, suppresses restart/escalation, and never resolves that delivery automatically.
13
13
  - 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`.
14
- - 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. 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 panel arms a write only from a preview reply that matches its open edit's token, key and value, and pasted text never confirms.
14
+ - 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. 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).
15
15
  - Retained configuration (#544): a key the reload refused, or a file edit outside a scoped write that still differs from the accepted value, is listed in `retainedConfig` (key and sanitized reason only, never a value) in status, `status.json`, the settings view and the console snapshot until a reload accepts or reverts it (`retainedConfigAfterReload`). A config leaf that selects a peer's native launch belongs in that definition's `cli.launchConfigKeys`: the `start` and `peer_start` replies then carry `retainedLaunchConfig` (the running values), the launcher starts with them and prints `retainedLaunchWarning` before its start request.
16
16
  - `pet` is console-local presentation (#483): validate it only through `validatePetConfig`, which both `loadConfig` and the reload schema call, and keep it applied live with no authority (`authorizeConfigLeaf`).
17
17
  - Start modes (#269): every start the hub makes itself goes through `hubStart` (`src/hub/daemon.ts`): the dashboard's `start_peer`, the console-role `peer_start`, the conductor's `start` hook and `pi.auto_start`. It takes the mode from `startMode(peer)` and nothing else; never add a caller-chosen mode, flag or command to it, and never fall back to headless when a terminal cannot be opened (refuse with the manual command). The words a terminal runs are assembled there from `process.execPath`, the CLI entry, the project root and `startWords(peer)`. The only continuity exception is a validated Pi bookmark on `pi.auto_start`: append its recorded session file (or its session id when no file was recorded), backend and model; never pass both session selectors or accept caller-written replacements. Recovery and `startPeer` callers that pass a recorded mode keep it. A launcher with no `--mode` takes the daemon's accepted start mode from `status.json` `startModes` (`readControl`) before the file's value. A TUI start of an `owned-acp` peer (Jcode) from the console's `peer_start` opens its terminal even while the ACP owner is online: that terminal is a frontend on the owner. The conductor and the dashboard answer `already` for an attached one (#559). The real terminal providers are wired only in `src/hub/lifecycle.ts` (`DaemonOptions.terminal`): a daemon built without them, as in every test, opens nothing; a test injects a recording provider and never the real one.
@@ -36,7 +36,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
36
36
  - Judge every recorded owner pid (hub manifest, registry claim, recovery runner, manager owner, launcher record) with `processLiveness` in `src/pi/process-signature.ts`, never a bare `process.kill(pid, 0)`: after a reboot a recorded pid can belong to another process, even one we may not signal, and the bare probe wedged recovery and `ahub up` (#226). A writer records `processSignature(process.pid)` beside its pid; a registry claim stores it as `<instance id> <signature>` because an older daemon's claim rewrites the pid but not that column. Records without a signature keep the bare-pid answer; unknown is never gone.
37
37
  - A restored daemon accepts a changed native session id (Kimi and local aside: they restart as fresh workers) in two ways only: the #64 rule for a Claude session that never persisted a transcript, and the operation-fenced `recovery-waivers.json`, which the coordinator writes wherever it accepts a session other than the planned one: a reconnect-only Claude (#206), an operator-chosen fresh session, the plan's `freshStart` of a Codex thread the hub saw start with no turn, or a zero-turn Claude it accepted (#215). Never waive on any other evidence. Re-preparation after an expired lease keeps every effect receipt, and `restored:<peer>` = `failed` never counts as restored. Judge a restoration launcher by its `terminal-recovery.json` record and process signature (and a `terminal_handle_stale` answer), never by the terminal's exit; an unreadable record is unknown, never "none".
38
38
  - A target that stopped after it started is restarted only through `recordRestart` (`src/cli/upgrade.ts`), recorded before the new one starts and at most once per project per resume: each done or waiting `restored:<peer>` binding becomes `retired:<peer>`, whose terminal is closed (`closedRetired:<peer>`) or found absent before that peer is relaunched, and is never revalidated against the new instance. Keep repaired-session provenance across every target restart, disabling automatic fresh/zero-turn replacement. A person may explicitly accept loss of a repaired Codex/Claude conversation only on typed positive missing transcript/rollout evidence and a disposal-time recheck; refuse live-test repaired overrides after read-only receipt classification and before runner/lock or coordinator mutation, fail closed on unclassifiable scope, and repeat in core before lock and after under-claim read, preserving ordinary #408 delegation; record the one-use grant and freshOverride evidence, and consume the grant on exact attachment. Pi stays excluded: positively missing accepted Pi files get stop-and-archive plus the normal project-scoped ahub pi command; bookmark retirement never falls back fresh in the same start. Clear the repair fence only for a done or waiting binding retired from that target; an uncertain creation without a durable binding keeps its fence. Launcher evidence is read for every instance in `restarts`, and a session attached without an id is unknown, never gone (#225).
39
- - After a managed terminal is created, persist its binding before waiting up to `RECONNECT_WAIT_MS` for authenticated attachment to the expected target instance, operation and conversation (#414). Resume waits on the saved binding without creating another terminal. Count only attached, non-offline session ids; when the chosen terminal has no current launch record, a confirmed-ended row for another handle or an id-less attachment retains the bounded wait after older-launcher checks. A late current record must match the saved terminal; unknown or live replacement evidence still blocks. Timeout keeps the receipt blocked and records only terminal/hub metadata; a last native connect attempt that was never recorded is unknown, never inferred from terminal readiness or offline state. Final identity and preservation verification stays strict.
39
+ - After a managed terminal is created, persist its binding before waiting up to `RECONNECT_WAIT_MS` for authenticated attachment to the expected target instance, operation and conversation (#414). Resume waits on the saved binding without creating another terminal. Count only attached, non-offline session ids; when the chosen terminal has no current launch record, a confirmed-ended row for another handle or an id-less attachment retains the bounded wait after older-launcher checks. A late current record must match the saved terminal; unknown or live replacement evidence still blocks. Timeout keeps the receipt blocked and records only terminal/hub metadata; a last native connect attempt that was never recorded is unknown, never inferred from terminal readiness or offline state. Final identity and preservation verification stays strict. `createTerminal` re-reads the launch record and Orca binding of an idle replacement every `MAP_POLL_MS` until it maps to the original peer and session, a known identity or session contradicts it, or the deadline passes; only then does it report `ambiguous-create`. Missing identity alone is never a mapping (#599).
40
40
  - An operation receipt stays `schema: 1`. Only `abandoned()` writes 2, on an operation with a recorded `disposition` whose coordinator predates #215: in the same write as the disposition, or, for a disposition 0.12.20 recorded at 1, by the next command that holds the runner claim. `validateReceipt` accepts 2 only with a disposition (#227). Never write 2 on an operation that can still be resumed.
41
41
  - Four checks read another release's source text for an exact line: `coordinatorCurrent` (`export async function disposeRecovery` in `src/cli/upgrade.ts`), `targetReadsWaivers` (`export function readRecoveryWaivers` in `src/hub/restart.ts`), `coordinatorRestarts` (`export function recordRestart` in `src/cli/upgrade.ts`) and `supportsAction` (`export async function reattachRecovery` in `src/cli/upgrade.ts`). Renaming, moving or un-exporting one makes this release read as an older one: keep those lines until a capability list replaces the checks. The reattach CLI dispatch must use the same `supportsAction` check as its printed command; never redirect a fallback command back to a coordinator without that action. Recovery also reads a target's literal `OPTIONAL_PEERS` (`src/hub/peer-catalog.ts`, through `legacyTargetSupport`) and `START_MODE_PEERS` (`src/hub/start-mode.ts`, through `legacyHeadlessArgv`), declared in `src/adapters/definitions/native-recovery.ts`; keep both literal declarations until a published capability replaces them.
42
42
  - Publish terminal ownership only after the current native owner is proven through `terminal_owner` (#482); capture and start acknowledgement are pending evidence only. Preserve unrelated and invalid rows, scope current evidence to project/instance/peer, and recheck the same native generation after asynchronous Orca reads. Keep Claude's hook record pre-spawn. Compatibility and retention ceilings are described in Operations, controlled recovery.
@@ -53,6 +53,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
53
53
  - Recovery inspection uses its existing 30 s control cap for the first status read and consumes a structured busy reply without a second status probe; ordinary lifecycle probes keep their default. Treat `busy_timeout` as a ceiling for eligible waits, not a guarantee that every write waits: a WAL snapshot conflict may fail immediately. Account for synchronous 3 s hub-database waits and serial timer callbacks when choosing a timeout; several waits may consume the 30 s control margin. Never multiply the busy wait through retries or infer stopped from a shorter probe's timeout.
54
54
  - Every `new Database(` under `src/` is followed at once by its own `PRAGMA busy_timeout` from a named `*_BUSY_TIMEOUT_MS` constant (`HUB_DB_BUSY_TIMEOUT_MS` in `src/hub/db-timeout.ts` for `hub.db`), and nothing sets `busy_timeout` again later: `test/db-busy-timeout.test.ts` reads the source and fails on any other opening idiom, so a new one extends that guard first (#303).
55
55
  - The manager forwards a dashboard `start_peer` with the terminal provider cap (`MAX_COMMAND_MS`) plus 5 s and reports a missed deadline as `unconfirmed`, never as a failure; it recognises the deadline by the exact text `ControlClient.request` resolves with, so change the two together. The hub's terminal reopen guard and the dashboard's retry hold share `PEER_START_RETRY_MS` (`src/hub/start-mode.ts`): never give either its own number (#298).
56
+ - `openManager` keeps only the manager that answered `/open`: when the answering pid is not the process this open spawned, it stops its own spawn with SIGTERM, so a losing concurrent open never leaves an unowned manager (#584).
56
57
 
57
58
  - Use the registry's `KNOWN_PEERS` (`src/adapters/registry.ts`) for the native catalog (the `src/hub/peer-catalog.ts` literal is a cross-release probe only) and `rosterAllows` for participation. Change live membership only at the authorized roster settings write or undo, applying exactly that key. Keep external ids and projects without roster settings compatible with the existing admission and lazy status rules. Fence starts and attachment before detaching or stopping a removed runtime; retain its queue, uncertain deliveries and independent pauses. Refuse removal of the conductor holder and require explicit confirmation and `--now` for an active turn.
58
59
 
@@ -76,3 +77,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
76
77
  - 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).
77
78
  - A declared Claude channel extra is never hub-started: keep it out of `STARTABLE_PEERS`, instance start modes and `RESERVED_IDS`. A launcher admits `--as <id>` only when the running daemon's status `activeInstances` lists it (`requireActiveInstance`), never from the config on disk.
78
79
  - Verify a native Claude Stop and its start baseline only through `readClaudeTranscriptCompletion` (4 MiB backwards chunks, at most 64 MiB plus one boundary byte; incomplete or out-of-bound rows are unavailable), never the size-capped `readClaudeTranscriptUsage`; an `unavailable` refusal leaves the peer busy and notifies once per turn start (#517).
80
+
81
+ - Classify HEAD moves with bounded, append-fenced per-worktree reflog evidence. Exclude only incoming checkout/merge/pull/proven-fast-forward reset paths; preserve commit/amend/cherry-pick and sequencer replay changes; classify rebase/pull --rebase start checkout as incoming, with overlapping replay paths unknown. Never exclude paths still dirty against end HEAD (autostash/checkout -m included). Unreadable or ambiguous movement/capture evidence is unknown, never an exclusion. Compute a concurrent change set once and publish no more than one conditional notice per affected task. Keep per-task-pair research events. Cache only raw checkout-branch probes within one synchronous burst, and reapply current privacy screening on every status rendering (#576).
82
+
83
+ - Share one monotonic 250 ms snapshot Git budget until the next event-loop I/O turn. Repository/HEAD probes, capture and end diffs consume the same remaining deadline; exhaustion or unreadable diff evidence records unknown trees and no attribution/undo, with one diagnostic per burst. Never substitute a stale baseline or apply a late result (#576).
@@ -9,6 +9,7 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
9
9
  - Role withdrawal matches the task id, role kind and owner-request header. Owner acceptance evidence must be that peer's accepted entry after the latest ownership event, never another owner's state or an earlier assignment span. Withdraw unread requests without inventing delivery evidence. Find them with `bus.pendingEnvelopes(peer)`, which includes recovery-deferred notices, never the snapshot's queues (#478). Preserve the original former-owner stop notice after a superseding escalation, while suppressing stale success/decision tails.
10
10
  - Capacity declines (#531): `hub_task_decline` takes an optional `capacity: true`, recorded as `capacity: true` on the `declined` history entry. `excluded()` skips such an entry, and `Tasks.decline` passes the decliner as `exclude` to that move's own reroute only, so a later reroute or planner pull may offer the task to it again from its reservation. Never infer a capacity decline from the note text, and never treat a row without the field as one: older rows are refusals. `plannerRefusal` (peer ids and counts only, never task text) names why `pullPlanned` found no owner, and `planBacklog` refuses a reservation for a peer `excluded()` returns. `assign()` stays pure, so `route explain` reads the same `excluded()` and `plannerFull` inputs as assignment.
11
11
  - `assign()` never defaults to the task's current owner, or a decline can only come back to the decliner. A reservation (#207) is the same risk: the reserved owner goes first only through `blocked()`, and a decline or an escalation for reason `rejections` or `manual` records the owner it left in the history entry's `from`, which `excluded()` reads, so no later reroute hands the task back through its reservation.
12
+ - A named owner that `assign()` refuses is never a silent no-op (#601): `assignTo` passes `refuseNamed` so `assignOwner` throws with the routing refusal reason before any write (pre-effect for tool callers, a plain `Error` for the console), and a proposal's unmet named owner is reported in the propose result through `namedOwnerNote`, read from the stall the proposal recorded. The task stays where it was; never deliver to an offline peer to make the name stick.
12
13
  - The state machine allows `in_progress -> approved` only for classes without a reviewer; `review()` checks `in_review` itself.
13
14
  - Treat `cancelled` as closed through `isClosedTaskState`, but keep dependency release restricted to approval. Cancellation goes through `Tasks.cancel`, preserves the row and roles, clears its reservation/cohort, sends revision-bound stop notices and writes no outcome memory; never map it to approval or cancel a native turn or worktree. Planner cancellation stays limited to its own historically unaccepted proposed/backlog work; accepted work remains person-only.
14
15
  - Capture `TaskOffer.pullOnly` and the exact offer envelope id at publication. Keep unread-offer attention independent of attachment and restart until acceptance, decline, supersession or `offerRead` evidence; never replace that evidence with a generic inbox timestamp or completion state.
@@ -12,17 +12,19 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
12
12
  - `ahub upgrade` without `--to` asks the registry (`npm view`) and hands a target newer than the CLI to `bun x` (#272): a test that reaches either puts a fake `npm` first on `PATH` and a recording stand-in for `process.execPath`, as `test/upgrade-cli.test.ts` does. The interactive screens are tested through a scripted `ScreenIO`; a CLI test that needs them marks `process.stdin` and `process.stdout` as terminals in a wrapper script.
13
13
  - Cross-process fixtures wait for the consuming client to process its handshake (the channel connected diagnostic), not merely the daemon's attached/idle state. Deadline-routing fixtures hold and release responses at the fallback decision; do not place a fixed sleep between deadline fractions and assume scheduler headroom (#287). Receipt-order fixtures use the normal watchdog for ordinary I/O, trigger expiry after observed native acceptance, and await a consumed transport barrier before asserting on late results.
14
14
  - A bounded polling wait must assert its condition after the last attempt, or document why giving up is intentional. Append-only event histories wait for a correlated closing event, not removal of the opening event. Before changing a slow wait, identify the unreached state and repair its cause (#296).
15
+ - A test of turn or notice behavior that is not about the snapshot budget yields an I/O turn (`await new Promise(r => setImmediate(r))`) between captures: captures in one synchronous burst share the 250 ms budget and may correctly record unknown evidence. A wait on concurrent-edit evidence correlates the expected turn ids and paths (#617).
15
16
  - A request that a test expects a stop or shutdown to cut gets its rejection handler where the request is created, never after the awaited teardown: a late `catch` races the reset and the run fails on an unhandled rejection.
16
17
  - Run `bun scripts/seeded-check.ts` before reporting the seeded-guard gate complete. Keep the `scripts/seeds.json` cases sequential; require the same named test green before its seeded assertion fails. Seed rot, survivors, setup/compiler failures, timeout/watchdog termination and process leaks fail the gate. Never run the full seed runner from a unit test or mutate the source checkout; inspect preserved failed fixtures before removing them. CI runs it as four sequential Linux shards (`--shard i/4`) beside the ordinary check, separately from `scripts/check.sh`; the required `seeded guards` job passes only when every shard passed, or when the decision job reused a verified identical-tree full gate (`admission == 'reuse'`). Keep that job name: branch protection and `scripts/ci-gates.mjs` read it (#457).
17
18
 
19
+ - `scripts/check.sh` pins the test environment (#610): `GIT_CONFIG_GLOBAL=/dev/null`, `NO_COLOR` unset, `TERM=xterm-256color`, temp root under the OS per-user temp dir, agent markers cleared. A test that needs a person's git config or color settings sets them itself. `--changed` selects tests with `scripts/changed-tests.mjs`: changed tests (nested suites included) plus every test that names a changed file directly; docs need none; any other changed file no test names, and any gate, package or shared-fixture change, runs everything.
18
20
  - `scripts/check.sh` holds the repo's heavy-job slot (`<git common dir>/ahub-heavy.lock`, #398) for the whole run, so a test never runs the real gate; `test/check-lock.test.ts` runs a copy in a scratch repo.
19
- - The whole-suite watchdog is `AHUB_CHECK_HANG_S` (default in `scripts/check.sh`, hosted values for both OSes in `.github/workflows/check.yml`; #378, macOS CI hit it while tests still passed): raise it only when the suite was still progressing at the cutoff, never for a hung test, and keep the per-test 20 s timeout. The check job's `timeout-minutes` must leave room above that value for setup: raise them together.
21
+ - The whole-suite watchdog is `AHUB_CHECK_HANG_S` (default in `scripts/check.sh`, hosted values for both OSes in `.github/workflows/check.yml`; #378 and #593, macOS CI hit it while tests still passed; currently 960 s local, 1080 s Linux, 1260 s macOS): raise it only when the suite was still progressing at the cutoff, never for a hung test, and keep the per-test 20 s timeout. The check job's `timeout-minutes` must leave room above that value for setup: raise them together.
20
22
  - A seed's named test must run on Linux, where the seeded job runs: a darwin-only test reports no result there and fails the job (#382). Cover a macOS-only protection with its regression test instead.
21
23
 
22
24
  - Keep CI's Bun version sourced from `.bun-version` through the pinned setup-bun action; update the version file rather than adding per-job version literals.
23
25
  - Release verification requires both OS checks and the aggregate `seeded guards` success for an identical tree, a tag matching `package.json` on `origin/main`, and fresh ancestor-bound documentation stamps. Preserve the separate npm OIDC publish job and GitHub Release write job; neither job gets the other's credential authority.
24
26
  - `test/conformance/` runs real adapters against fake transports. Record a case that does not apply as an explicit unsupported outcome, never synthetic success, and never change native production behavior to satisfy a generic case.
25
27
 
26
- - Keep the CI admission lock around the whole controller workflow, not just its decision job. Preserve successful identical-tree full proof and required job names; a skipped job, pending controller or unavailable active inventory is never a successful full gate. CI reuse tests use fixed metadata fixtures and never dispatch real duplicate workflows.
28
+ - Keep the CI admission lock around the whole controller workflow, not just its decision job, in its per-PR (`check-pr-<number>`, `queue: single`: a newer pending run replaces the older) or main (`check-main`, `queue: max`) group, never with `cancel-in-progress: true` beside `queue: max` (GitHub refuses it) (#609); two groups admitting one tree at once is an accepted duplicate run, never a reason to fail closed on another tree. Preserve successful identical-tree full proof and required job names; a skipped job, pending controller or unavailable active inventory is never a successful full gate. CI reuse tests use fixed metadata fixtures and never dispatch real duplicate workflows.
27
29
 
28
30
  - Headless-reviewer tests run `test/fakes/fake-claude-print.ts` through `command` or a PATH stand-in, and fresh Codex reviewer tests run `test/fakes/fake-codex-reviewer.ts` through `codex_bin`; a test that reaches the daemon's reviewer start must never run the real `claude` or `codex`. The fake's sandbox markers test host admission only, never native isolation. Fresh Kimi reviewer tests set and restore `KIMI_CODE_HOME` to a fixture native home (the backend links that home's `credentials/`, `oauth/` and `device_id`, else the person's `~/.kimi-code`) and run `test/fakes/fake-kimi-reviewer.ts` through `command` or a PATH stand-in, never the real `kimi`. agy tests put a fake `agy` first on PATH and set `HOME` to a temp dir before any transcript readback (`readAgyTranscript` reads `$HOME/.gemini`).
@@ -1,6 +1,6 @@
1
1
  # Operations guide
2
2
 
3
- This guide describes ahub 0.12.41 and control protocol 16. Live verification
3
+ This guide describes ahub 0.12.43 and control protocol 16. Live verification
4
4
  results and remaining prerequisites are recorded separately in [the smoke ledger](smoke.md).
5
5
 
6
6
  ## Agent-operated live verification
@@ -93,6 +93,27 @@ cyan, arguments and descriptions the terminal's default foreground. It has no
93
93
  not `dumb` and `NO_COLOR` is empty or absent. Pipes and redirects get plain text
94
94
  without escape sequences, laid out for 80 columns.
95
95
 
96
+ ### Shell completion
97
+
98
+ `ahub completion zsh|bash|fish` prints a completion script; it never installs
99
+ files or edits shell configuration. On each Tab the script runs
100
+ `ahub __complete`, which answers from the help entries above (commands,
101
+ subcommands, options, their listed values and file names for path arguments)
102
+ without loading the rest of the CLI or contacting a hub, so completion always
103
+ matches the installed `ahub`. Agent shells may print the script too.
104
+
105
+ ```sh
106
+ # zsh: any directory on fpath works; this one needs no .zshrc change with oh-my-zsh
107
+ ahub completion zsh > ~/.oh-my-zsh/custom/completions/_ahub && rm -f ~/.zcompdump* && exec zsh
108
+ # bash
109
+ ahub completion bash > ~/.local/share/bash-completion/completions/ahub
110
+ # fish
111
+ ahub completion fish > ~/.config/fish/completions/ahub.fish
112
+ ```
113
+
114
+ Upgrading ahub needs no regeneration. To remove it, delete that file and start
115
+ a new shell.
116
+
96
117
  ## Operator console and panels
97
118
 
98
119
  `ahub console` combines the existing tail stream with peer, quota, approval-age
@@ -121,7 +142,7 @@ without color. The Approvals tab counts pending requests and the Queue tab
121
142
  counts `needs_review` deliveries, in the attention and failure tones.
122
143
 
123
144
  The console attention companion (#280) shows waiting, blocked, review, working,
124
- idle or asleep with separate action/watch counts. `n` selects the exact approval,
145
+ idle or asleep with separate action/watch counts. Outside the Tasks table, `n` selects the exact approval,
125
146
  held queue or review row, or prints applicable guidance; it never answers a
126
147
  request or runs a command. Editing, choices and confirmation retain their controls.
127
148
  `P` cycles the five built-in characters and off for the current console.
@@ -145,7 +166,8 @@ chunk can only edit the command line, but a read that holds nothing but arrow or
145
166
  Home/End sequences is that many key presses, so a held key repeats its move. Home
146
167
  or `<` jumps to a panel's first row and End or `>` to its last; the Events tab
147
168
  follows new events while the last row is selected and stays on a row you moved to.
148
- `w` opens the dashboard and `o` the pet window; `:ui --all` and `:pet --window --close`
169
+ `w` opens the dashboard outside the Peers tab; in Peers it edits the selected
170
+ peer's WIP limit. `o` opens the pet window; `:ui --all` and `:pet --window --close`
149
171
  work on the command line too. In the Peers tab the planner's row reads `(planner)`, and
150
172
  `l` then `y` makes the selected peer the planner (`ahub settings set planner <peer>`). In the Events
151
173
  tab a row's header line is tokenized and coloured exactly as the same event in
@@ -160,14 +182,54 @@ meter keeps a fixed column at the right edge.
160
182
 
161
183
  | Panel | Columns |
162
184
  |---|---|
163
- | Peers | PEER STATE MODE LINK Q ! REVIEW PAUSE QUOTA MODEL |
185
+ | Peers | PEER STATE MODE LINK Q ! WIP REVIEW PAUSE QUOTA MODEL |
164
186
  | Approvals | ID PEER LEFT TITLE |
165
187
  | Tasks | ID STATE OWNER REVIEWER CLASS AGE TITLE STAGE |
188
+ | Tasks, backlog view (`b`) | ID PRI RESERVED DEPS CLASS AGE TITLE |
166
189
  | Queue | ID PEER STATE REV AGE |
167
190
  | Settings | GROUP KEY VALUE SOURCE APPLIES |
168
191
 
192
+ In Tasks, `b` toggles open work and backlog. The backlog heading counts its rows;
193
+ rows sort by rank, then id. PRI shows priority/rank, RESERVED the reserved peer,
194
+ and DEPS either `ready` or the unapproved dependency ids. Titles use the public
195
+ board projection, so PII work remains `[pii]`. Narrow layouts keep ID, RESERVED
196
+ and TITLE before the other columns.
197
+
198
+ On a backlog row, `p` prefills `backlog pull-ready <id> <planningRevision> <reason>`,
199
+ `a` prefills `backlog plan` with a reservation JSON object, and `e` prefills the
200
+ same command with priority, rank and deps. Edit the JSON and supply a reason;
201
+ `c` uses the existing revision-bound `task cancel` command. Once pulled into open
202
+ work, `a` uses the usual task assignment flow. In either Tasks view, `n` prefills
203
+ `backlog add implement `; edit the class and title, or use
204
+ `:task propose --backlog` with paths/deps flags. On proposed or in-progress open
205
+ work, `R` prefills `backlog return <id>` for an ownerless proposal, or the
206
+ planning-revision-and-reason form for accepted, untouched work. The hub refuses
207
+ returns with execution, branch or commit evidence; its refusal appears as a notice.
208
+
209
+ These actions run the existing person CLI commands, after explicit confirmation;
210
+ Esc sends nothing. Row-prefilled ids and revisions cannot be edited, and stale
211
+ row revisions require a reload. `:backlog list` reads without confirmation;
212
+ backlog mutations (including pull, pull-ready, return, plan, split and undo)
213
+ confirm first. The console adds no write path or planner authority.
214
+
169
215
  The MODE column appears only when at least one peer has a non-`ask` mode.
170
- Zero counters and unknown values read `-`. Durations read `45s`, `12m`, `3h05m`
216
+ Peers' WIP column shows planner work in progress as `wip/limit` from `taskWip`,
217
+ including `0/2`; a peer without an entry reads `-`. At or above the limit, the
218
+ cell uses the attention tone. At narrow widths WIP gives way before PEER, STATE
219
+ and QUOTA, whether or not MODE is present.
220
+
221
+ In Peers, select a row and press `w` to edit `tasks.wip_limits.<peer>`, prefilled
222
+ with its current limit. The editor stays on Peers and uses the existing Settings
223
+ value, daemon preview, nonempty reason and `y` confirmation flow; Esc cancels without writing. The
224
+ setting definition supplies the bounds (currently 0..32), risk label (`raises`)
225
+ and `applies: live`; the shortcut adds no configuration write path. The Peers
226
+ hint reads `w WIP limit` and shortens to `w WIP` at narrow widths, alongside
227
+ `m permission mode` shortening to `m mode`. Without a `taskWip` entry, `w` shows
228
+ a notice and opens no edit. Press `6`, then `u` to preview and confirm the usual
229
+ Settings undo. These controls remain person-only, and `ahub settings` retains
230
+ its agent-shell refusal.
231
+
232
+ Other zero counters and unknown values read `-`. Durations read `45s`, `12m`, `3h05m`
171
233
  or `2d03h`; quota resets and budget pauses read `in 2h13m`, and a pause by the
172
234
  console user reads `user`. When a peer's status carries #205's optional
173
235
  `toolsOnly` field (a Claude session attached without channel pushes), LINK reads
@@ -899,7 +961,12 @@ later. Your own `ahub task assign` is not blocked by those exclusions, and it
899
961
  drops the agent's reservation. The PII constraint and capability limits
900
962
  still apply, so a PII task goes to `local` or nobody whoever was reserved.
901
963
  `ahub task assign` on a task that waits changes its reserved owner instead of
902
- handing it over. `ahub route explain <id>` names the reserved owner; once the
964
+ handing it over. On a task that does not wait, an assignment whose named owner cannot take
965
+ the task now (offline or not attached, excluded, a capability or PII limit, or
966
+ an enforce-mode SDLC gap) is refused with the reason and the task is unchanged;
967
+ assign again once the peer is attached (#601). When a proposal names an owner,
968
+ a self-claim included, and that owner did not get the task, the proposal's
969
+ result says why and names `hub_task_assign`. `ahub route explain <id>` names the reserved owner; once the
903
970
  task is ready it also says whether routing would honor the reservation now, and
904
971
  why not.
905
972
 
@@ -1080,6 +1147,7 @@ ahub undo <turn> --yes --context # Codex's latest turn: also drop it from Codex
1080
1147
  Nothing is restored then. It plans again right before restoring and stops if
1081
1148
  anything moved meanwhile. A turn that is already undone says so.
1082
1149
  - A file the turn created is deleted; a file it deleted comes back.
1150
+ - When HEAD moves during a turn, the hub reads the appended HEAD reflog (bounded to 64 moves and 128 KiB). The turn's own commits, amends, cherry-picks and rebase picks still count as its changes. Files brought in by a pull, merge, rebase start, branch checkout or fast-forward reset are left out, unless they are still dirty at the turn's end. An unreadable or ambiguous reflog, a rewinding reset, or a file touched by both kinds of move makes the turn's files unknown (#576).
1083
1151
  - `--context` asks Codex (`thread/revert`) to drop the turn, and every later one,
1084
1152
  from its thread's saved history before the files are restored; Codex receives
1085
1153
  nothing while that runs. It changes no file itself, works only on Codex's
@@ -1093,7 +1161,11 @@ ahub undo <turn> --yes --context # Codex's latest turn: also drop it from Codex
1093
1161
  snapshotted by later turns like any other file. Keep PII files in an ignored
1094
1162
  directory.
1095
1163
  - A turn the hub stopped in the middle of shows the state `no end snapshot` and
1096
- cannot be undone.
1164
+ cannot be undone. So does a turn whose boundary evidence is unknown: snapshot
1165
+ Git calls made in one callback burst share a 250 ms budget, and a capture that
1166
+ runs past it, fails, or sees HEAD move while it is taken is unknown, never a
1167
+ stale baseline (hub.log says `snapshot unavailable` once per burst). A turn
1168
+ whose diff or HEAD-move evidence cannot be read is unknown as well (#576).
1097
1169
  - Claude's turns are not recorded: its channel shows the hub no turn boundary.
1098
1170
  Claude Code's own checkpoints cover its edits, though not its shell commands.
1099
1171
  - The snapshots are unreferenced objects in the repository's own object store, so
@@ -2044,21 +2116,21 @@ section is what they run.
2044
2116
 
2045
2117
  Upgrade running projects with the target release's own coordinator. It accepts
2046
2118
  a running source on control protocol 9 (0.6.x), 10 (0.7.0 through 0.12.0),
2047
- 11 (0.12.1 and 0.12.2), 12 (0.12.3), 13 (0.12.4 through 0.12.15), 14 (0.12.16), 15 (0.12.17 through 0.12.19) or 16 (0.12.20 through 0.12.41), and only
2119
+ 11 (0.12.1 and 0.12.2), 12 (0.12.3), 13 (0.12.4 through 0.12.15), 14 (0.12.16), 15 (0.12.17 through 0.12.19) or 16 (0.12.20 through 0.12.43), and only
2048
2120
  a target on its own protocol, so the target's coordinator fits every supported
2049
2121
  source and carries every recovery fix released up to it. Protocol 8 and older
2050
2122
  (0.5.x and earlier) are refused as `manual-bootstrap-required`. Run from the
2051
2123
  project directory, without replacing the global CLI first:
2052
2124
 
2053
2125
  ```bash
2054
- bunx --package @staix/agent-hub@0.12.41 ahub upgrade --to 0.12.41 --dry-run
2055
- bunx --package @staix/agent-hub@0.12.41 ahub upgrade --to 0.12.41 --yes
2126
+ bunx --package @staix/agent-hub@0.12.43 ahub upgrade --to 0.12.43 --dry-run
2127
+ bunx --package @staix/agent-hub@0.12.43 ahub upgrade --to 0.12.43 --yes
2056
2128
  ```
2057
2129
 
2058
2130
  | Running now | Coordinator to use |
2059
2131
  | --- | --- |
2060
2132
  | 0.6.x (protocol 9) | the target's, through `bunx` as above |
2061
- | 0.7.0 through 0.12.0 (protocol 10), 0.12.1 and 0.12.2 (protocol 11), 0.12.3 (protocol 12), 0.12.4 through 0.12.15 (protocol 13), 0.12.16 (protocol 14), 0.12.17 through 0.12.19 (protocol 15), 0.12.20 through 0.12.41 (protocol 16) | the target's, through `bunx` as above |
2133
+ | 0.7.0 through 0.12.0 (protocol 10), 0.12.1 and 0.12.2 (protocol 11), 0.12.3 (protocol 12), 0.12.4 through 0.12.15 (protocol 13), 0.12.16 (protocol 14), 0.12.17 through 0.12.19 (protocol 15), 0.12.20 through 0.12.43 (protocol 16) | the target's, through `bunx` as above |
2062
2134
  | any supported source, with the installed CLI already at the target | `ahub upgrade` below, which is the same coordinator |
2063
2135
  | 0.5.x or earlier (protocol 8 and older) | not supported: bootstrap by hand with the matching CLI |
2064
2136
 
@@ -2090,14 +2162,14 @@ projects first:
2090
2162
 
2091
2163
  ```bash
2092
2164
  ahub restart --dry-run
2093
- ahub upgrade --to 0.12.41 --dry-run
2165
+ ahub upgrade --to 0.12.43 --dry-run
2094
2166
  ```
2095
2167
 
2096
2168
  Apply only after reviewing the plan:
2097
2169
 
2098
2170
  ```bash
2099
2171
  ahub restart --yes
2100
- ahub upgrade --to 0.12.41 --yes
2172
+ ahub upgrade --to 0.12.43 --yes
2101
2173
  ahub recovery status [<operation-id>]
2102
2174
  ahub recovery resume [<operation-id>]
2103
2175
  ahub recovery abort [<operation-id>]
@@ -2174,7 +2246,7 @@ A lost commit or start reply is reconciled against the actual daemon identity,
2174
2246
  package digest, terminal binding, and phase receipt. An uncertain terminal
2175
2247
  creation is never repeated blindly. Recovery retains the exact-pinned first-party memory package with the preserved runtime, includes its identity and bytes in the digest, and refuses missing, symlinked or changed retained dependencies instead of resolving a mutable ancestor/global installation. It does not copy the full dependency tree.
2176
2248
 
2177
- After creating a managed terminal, the coordinator saves its exact handle, incarnation and worktree before waiting up to 600 seconds (10 minutes) for authenticated attachment to the expected target hub and conversation. A pending or failed creation receipt also waits when the current target has a verified live launcher; its saved binding retains the pending/failed origin across interruption. An idle terminal alone is not attachment proof. Without a current launch record, a confirmed-ended row for another terminal and an attached session still reporting no id keep the chosen terminal waiting within the same bound; a late matching record continues that wait. Live replacement and unreadable records still block. Only non-offline session ids count, and older-instance launchers are checked before declaring a launch gone. Timeout retains the binding and diagnostic receipt; resume waits on that same binding rather than opening a duplicate.
2249
+ After creating a managed terminal, the coordinator saves its exact handle, incarnation and worktree before waiting up to 600 seconds (10 minutes) for authenticated attachment to the expected target hub and conversation. A pending or failed creation receipt also waits when the current target has a verified live launcher; its saved binding retains the pending/failed origin across interruption. An idle terminal alone is not attachment proof: after idle, the coordinator re-reads the launch record and Orca's identity every second within the same bound, until the terminal maps to the expected session or contradicts it (#599). Without a current launch record, a confirmed-ended row for another terminal and an attached session still reporting no id keep the chosen terminal waiting within the same bound; a late matching record continues that wait. Live replacement and unreadable records still block. Only non-offline session ids count, and older-instance launchers are checked before declaring a launch gone. Timeout retains the binding and diagnostic receipt; resume waits on that same binding rather than opening a duplicate.
2178
2250
 
2179
2251
  The wait checks the selected launcher's signed identity on each poll. When that exact launcher exits and the target confirms no session or other live launcher, a pending-origin wait (including a new terminal) records failure and offers the failed-restoration choices, including fresh-session for Codex or Claude. A failed-origin wait returns to its existing retry path. A replacement terminal, missing or unreadable launcher identity, unreadable target or different attached conversation blocks without treating unknown as gone. Success clears the waiting origin and preserves exact terminal/session revalidation. A launcher from an older target instance still blocks without another terminal. The 10-minute limit matches terminal creation readiness; terminal idle can precede native resume and MCP handshake completion. The zero-turn Claude exception still applies only when no original transcript exists. If Claude shows its development-channel confirmation screen, confirm it in
2180
2252
  the captured terminal, then run `ahub recovery resume <operation-id>`; resume must not open a duplicate terminal.
@@ -2484,9 +2556,11 @@ next actions").
2484
2556
  gone. One that dies before recording itself leaves no record to read, and
2485
2557
  what happens next depends on whether Orca reports the bare login shell it
2486
2558
  returned to as TUI-idle, which is not verified. If it does, the coordinator
2487
- stops at once with "original session restoration needs manual verification"
2488
- (the terminal cannot be mapped to the session). If it does not, it stops with
2489
- the same error after the 10-minute wait. Either way the receipt stays
2559
+ re-reads the terminal's mapping every second (#599) and stops at once only when
2560
+ Orca names another peer or session; otherwise it stops with "original session
2561
+ restoration needs manual verification" (the terminal cannot be mapped to the
2562
+ session) when the 10-minute wait ends. If it does not, it stops with the same
2563
+ error after the 10-minute wait. Either way the receipt stays
2490
2564
  `pending`; close that terminal in Orca (or wait, if the session may still
2491
2565
  attach), then `resume`: with no live launcher and no attached session it
2492
2566
  records `restored:<peer>` `failed` and lists the failed-restoration choices.
@@ -2944,10 +3018,10 @@ PR or push check with the identical Git tree and all three successful jobs. An a
2944
3018
  unreadable result runs the main gate again and refuses release. A manual
2945
3019
  `prepare_bundle` dispatch builds reviewable plugin assets without publishing;
2946
3020
  it is never accepted as full-gate evidence. A `timing_runs=20` dispatch runs only the daemon, PII screen and ACP
2947
- test files twenty times on macOS, in its own concurrency group; it does not replace the full gate.
3021
+ test files twenty times on macOS, in its own concurrency group; it does not replace the full gate. Each PR has its own CI concurrency group, so PRs run side by side; within a PR a newer pending run replaces the older pending one and an admitted run is never cancelled; main pushes and dispatches keep queueing every run. Two groups admitting the same tree at once may run it twice, never a false green (#609). `scripts/check.sh --changed [<base>]` runs every non-test step but only the affected test files, and the full suite when it cannot tell; the full gate stays CI's (#610).
2948
3022
  `scripts/check.sh` stops a suite still running after `AHUB_CHECK_HANG_S` seconds
2949
- (600 by default); hosted macOS CI sets 780 inside the unchanged 15-minute job
2950
- limit (#378).
3023
+ (960 by default); hosted CI sets 1080 on Linux and 1260 on macOS inside a 25-minute
3024
+ job limit (#378, #593).
2951
3025
  The gate holds `<git common dir>/ahub-heavy.lock` across every worktree of a
2952
3026
  repository. A second gate waits up to `AHUB_CHECK_LOCK_WAIT_S` (1800 seconds by
2953
3027
  default; 0 fails immediately) and names the holder. A holder whose PID is gone
@@ -3235,11 +3309,11 @@ Native policy is unmanaged and has NO hub sandbox; no permission_modes.jcode or
3235
3309
 
3236
3310
  ## Optional Antigravity (agy) peer
3237
3311
 
3238
- Antigravity is optional and off roster by default. Use `ahub roster add agy`, then run `ahub agy` in a terminal: it runs the Antigravity CLI 1.4.2 as a fresh pull-only TUI in that terminal and refuses any other version. The running hub must advertise `native_hook_launch`; an older hub refuses before any write with an upgrade-or-restart remedy. Only the person's launcher starts it: conductor starts are refused, there is no controlled restore or resume, and extra agy instances are unavailable. Headless/print, resume (`--conversation`, `--continue`, `--new`), `--mode`, `--dangerously-skip-permissions` and `--unattended` are refused until their native mapping is verified; `--print-command` previews without writing.
3312
+ Antigravity is optional and off roster by default. Use `ahub roster add agy`, then run `ahub agy` in a terminal: it runs the Antigravity CLI as a fresh pull-only TUI in that terminal. It accepts the 1.4.x line: 1.4.2 is the verified version, any other 1.4.x launches with a stderr warning (agy updates itself), and other versions are refused (#589). The running hub must advertise `native_hook_launch`; an older hub refuses before any write with an upgrade-or-restart remedy. Only the person's launcher starts it: conductor starts are refused, there is no controlled restore or resume, and extra agy instances are unavailable. Headless/print, resume (`--conversation`, `--continue`, `--new`), `--mode`, `--dangerously-skip-permissions` and `--unattended` are refused until their native mapping is verified; `--print-command` previews without writing.
3239
3313
 
3240
3314
  The launcher writes exactly `plugin.json`, `mcp_config.json` and `hooks.json` in `.agents/plugins/agent-hub/`, refusing linked or foreign entries before any write, and removes only its own matching files when the TUI ends. It never writes project-level `.agents/hooks.json` or the person's `~/.gemini` settings. Run `ahub init` afterwards to ignore the untracked plugin directory; a tracked plugin is written with a warning. The native model sees the hub tools under the server name `agent-hub_agent-hub`.
3241
3315
 
3242
- Like Kimi's TUI, agy is pull-only: it reads `hub_inbox` and shares results with `hub_send`, messages do not interrupt it, and automatic routing never picks it. Its roles are capped at implementer and verifier; a plan or review assignment and PII work are refused. Native permissions are unmanaged, with no hub sandbox, hub permission mode, quota source or session resume. Plugin hooks bound to that launch carry the hub's turn facts and report each completed native turn. The live TUI/account leg is unverified ([T0 receipt](verification/2026-10-19-agy-p1-t0.md)).
3316
+ Like Kimi's TUI, agy is pull-only: it reads `hub_inbox` and shares results with `hub_send`, messages do not interrupt it, and automatic routing never picks it. Its roles are capped at implementer and verifier; a plan or review assignment and PII work are refused. Native permissions are unmanaged, with no hub sandbox, hub permission mode, quota source or session resume. Plugin hooks bound to that launch carry the hub's turn facts and report each completed native turn. A native read is recorded as a peer `Read` fact: `view_file` (`AbsolutePath`) in 1.4.2 and `read_file` (`Path`) in 1.4.3; a `StartLine`/`EndLine` range counts as a partial read, never a whole-file view (#555). The live TUI/account leg is unverified ([T0 receipt](verification/2026-10-19-agy-p1-t0.md)). The 1.4.3 T0 re-run keeps 1.4.3 unverified: its `call_mcp_tool` schema and native tool names changed, and the TUI attaches the hub tools only from the second model turn ([1.4.3 receipt](verification/2026-10-21-agy-1.4.3-t0.md), #607).
3243
3317
 
3244
3318
  # Headless ACP watch (#286)
3245
3319
 
@@ -3416,7 +3490,7 @@ unapproved dependencies, frozen scope and overriding person P0 order/reservation
3416
3490
  The person uses `ahub backlog pull-ready <id> <revision> <reason>` or Pull within
3417
3491
  WIP on a card. Explicit manual pull/assignment retain their existing authority.
3418
3492
 
3419
- Capacity defaults to one per peer. `ahub settings set tasks.wip_limits.codex 2`
3493
+ Capacity defaults to one per peer. Each declared instance has its own `tasks.wip_limits.<instance>` key. `ahub settings set tasks.wip_limits.codex 2`
3420
3494
  uses the existing machine-local registry path; integers 0..32 apply live (the next
3421
3495
  admission reads the current value), with 0 disabling planner admission for that
3422
3496
  peer. Status and the agent legend count
@@ -3498,7 +3572,7 @@ recovery action can be shown. The output never resumes, aborts or disposes a run
3498
3572
  Person-reviewed tasks contribute review actions; an offline peer holding
3499
3573
  unfinished work contributes one watch item with its lowest task id. The console
3500
3574
  receives those structural facts through its existing authenticated status reply.
3501
- `n` selects an exact visible review task, or prints the task/review guidance if
3575
+ Outside the Tasks table, `n` selects an exact visible review task, or prints the task/review guidance if
3502
3576
  that row is unavailable. Offline guidance names status/task reads and an explicit
3503
3577
  assignment command. No command is executed by navigation. There is no extra
3504
3578
  console poll, timer or task-list request for stream mode.
@@ -17,7 +17,7 @@ ahub setup # installs the Claude Code channel plu
17
17
  Or use the matching GitHub release:
18
18
 
19
19
  ```bash
20
- bun add -g github:STAIxBWLB/agent-hub#v0.12.41
20
+ bun add -g github:STAIxBWLB/agent-hub#v0.12.43
21
21
  ahub setup
22
22
  ```
23
23
 
@@ -30,13 +30,21 @@ and upgrade or crash recovery, see the [operations guide](operations.md). To
30
30
  move running projects to a newer release later, run `ahub upgrade` in a
31
31
  terminal: it reviews the plan with you, applies it and follows it.
32
32
 
33
+ ## Upgrading to 0.12.43
34
+
35
+ A busy hub no longer stalls when the checkout's HEAD moves under open turns: snapshot Git work in one burst shares a 250 ms budget, and incoming checkouts, merges and fast-forwards are told apart from a peer's own commits through the HEAD reflog, so they no longer raise false concurrent-edit warnings (#576). `ahub task assign` and `hub_task_assign` now refuse with the reason when the named peer cannot take the task (#601). `ahub completion zsh|bash|fish` prints a shell completion script (#603). The console's Peers tab shows each peer's planner WIP, and `w` edits its limit (#532). agy native file reads are recorded as peer facts on 1.4.2 and 1.4.3 (#555); 1.4.3 stays unverified, and in its TUI the hub tools appear from the second turn (#267, #607). Coming from 0.12.41 or older, the notes below apply too.
36
+
37
+ ## Upgrading to 0.12.42
38
+
39
+ 0.12.42 lets `ahub agy` start again after the Antigravity CLI updated itself to 1.4.3: the launcher accepts the 1.4.x line, launching 1.4.2 (the verified version) silently and another 1.4.x patch with a warning (#589). The rest of the release is contributor tooling (#591, #593, #595). Coming from 0.12.40 or older, the notes below apply too.
40
+
33
41
  ## Upgrading to 0.12.41
34
42
 
35
43
  0.12.41 fixes one 0.12.40 regression: `ahub codex` in a recorded Orca terminal stopped at `invalid remote address`, because the launcher put the terminal launch id into the `--remote` URL, which Codex accepts only as `ws://host:port`. The id now travels as the upgrade's bearer token (#586). Coming from 0.12.39 or older, the 0.12.40 notes below apply too.
36
44
 
37
45
  ## Upgrading to 0.12.40
38
46
 
39
- Restart Claude sessions after upgrading: each keeps the plugin server it started with until restarted, so a restart loads the 0.12.41 plugin. That plugin asks Claude Code to list the hub tools again when a hub reconnect changes them (#490), and one from an older `ahub claude` launch is refused and stops reconnecting instead of reclaiming the peer (#551).
47
+ Restart Claude sessions after upgrading: each keeps the plugin server it started with until restarted, so a restart loads the 0.12.43 plugin. That plugin asks Claude Code to list the hub tools again when a hub reconnect changes them (#490), and one from an older `ahub claude` launch is refused and stops reconnecting instead of reclaiming the peer (#551).
40
48
 
41
49
  `ahub jcode` opens its TUI again; on 0.12.39 it stopped after the first hub turn (#564). A busy Jcode now takes an `important` message into its running turn (#572), and `ahub doctor` lists the extra MCP servers the hub-owned Jcode loads from its config files. See [Jcode operations](operations.md#optional-jcode-peer).
42
50
 
@@ -58,7 +66,7 @@ Upgrading from 0.12.37 or earlier, the 0.12.38 notes below also apply.
58
66
 
59
67
  The 0.12.38 plugin reports a reply stopped by the hop cap as not delivered instead of as sent (#546); the restart above loads it.
60
68
 
61
- Antigravity (`agy`) can join as an optional pull-only implementer (#485). It is off roster by default: install the Antigravity CLI 1.4.2, run `ahub roster add agy`, then run `ahub agy` in a terminal. Its native permissions are unmanaged and it has no hub sandbox. See [Antigravity operations](operations.md#optional-antigravity-agy-peer).
69
+ Antigravity (`agy`) can join as an optional pull-only implementer (#485). It is off roster by default: install the Antigravity CLI 1.4.x (1.4.2 verified; other 1.4.x patches launch with a warning; on 1.4.3 the hub tools appear from the second TUI turn), run `ahub roster add agy`, then run `ahub agy` in a terminal. Its native permissions are unmanaged and it has no hub sandbox. See [Antigravity operations](operations.md#optional-antigravity-agy-peer).
62
70
 
63
71
  Reviews can rotate across fresh Claude, Codex and Kimi reviewer sessions (#519, #533). Declare headless reviewer-only instances in `.agenthub/config.local.json`; Codex and Kimi join only with `review_mode: "fresh"`. Codex also needs an installed Codex 0.146.0 (select it with `codex_bin` when PATH resolves another version), and Kimi needs Kimi Code 2.2.0 on PATH, signed in with its own OAuth login. A headless reviewer instance without `review_mode` keeps its native behavior and stays out of the pool. agy is not a reviewer: naming it in `review.pool` shows an unavailable skip in `ahub route explain`, never a review. Once a train is active or frozen, or `review.pool` is set or fresh reviewer instances are declared, `ahub upgrade` refuses a target release without that support, so an older release cannot take the project over. See [fresh reviewer pool](operations.md#fresh-reviewer-pool-519).
64
72
 
package/docs/sdlc.md CHANGED
@@ -229,7 +229,7 @@ The planner can provide scope with `hub_plan_review`; the person can run `ahub s
229
229
 
230
230
  A review request is premature if its task head does not match the PR head, configured required checks are not green, or a pending bot thread is unanswered. A resolved thread is settled; an unresolved one needs a user reply after its last bot comment. The hub reads identity, timestamps, authorship type and resolution only, never comment bodies. Without explicit required checks the native floor is both OS checks plus `seeded guards`. Advise reports the gap; enforce refuses before the requesting done/cohort writes. A private turn may finish its own PII task in advise with a generic unknown-readiness notice and no remote metadata read; the private-turn refusal for ordinary work remains. Delayed checks revalidate before completing. Background roster/quota role changes remain fact, but enforce holds a new review envelope without an admission readback; refresh it through a manual reviewer assignment after CI and replies are ready. The brief and verdict coverage belong to #520, separate from this admission.
231
231
 
232
- CI controllers are serialized across the repository for the entire workflow, without canceling an admitted controller. `queue: max` preserves up to 100 waiting controllers instead of replacing the previous pending run ([GitHub concurrency documentation](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency)). This is intentionally stronger than a tree-only lock and retains the required job names and read-only token permissions. Before PR or push gates, `scripts/ci-reuse.mjs` reuses successful full proof for the identical Git tree, including its workflow. On reuse the two OS matrix jobs still expand and report their exact required names, while the seeded aggregate reports its exact name. They run only a `Reuse verified identical-tree full gate` marker step naming the original proof; full setup and gates are skipped. Skipped jobs and successful reuse-reporting jobs never count as fresh full proof. A running identical-tree gate refuses duplicate admission; unknown active coverage refuses safely. Metadata reads are bounded to 100 recent successes/active runs and 240 calls within 60 seconds. Pending serialized controllers are not admitted gates. Run focused tests locally and one full gate at the final head; a known flake warrants only one rerun of its failed job with recorded evidence.
232
+ CI controllers are serialized per pull request (`check-pr-<number>`) and for main pushes and dispatches (`check-main`), for the entire workflow and without canceling an admitted controller (#609). Within a PR `queue: single` lets a newer pending run replace the older pending one; main keeps `queue: max`, which preserves up to 100 waiting controllers ([GitHub concurrency documentation](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency)). Different PRs run side by side: a repository-wide lock serialized every PR (17-18 min alone, 40-54 min with 4-5 PRs in flight on 2026-10-20). Two groups admitting the same tree at the same instant can run it twice; that duplicate costs one run and never a false green. Required job names and read-only token permissions are unchanged. Before PR or push gates, `scripts/ci-reuse.mjs` reuses successful full proof for the identical Git tree, including its workflow. On reuse the two OS matrix jobs still expand and report their exact required names, while the seeded aggregate reports its exact name. They run only a `Reuse verified identical-tree full gate` marker step naming the original proof; full setup and gates are skipped. Skipped jobs and successful reuse-reporting jobs never count as fresh full proof. A running identical-tree gate refuses duplicate admission; unknown active coverage refuses safely. Metadata reads are bounded to 100 recent successes/active runs and 240 calls within 60 seconds. Pending serialized controllers are not admitted gates. Run focused tests locally and one full gate at the final head; a known flake warrants only one rerun of its failed job with recorded evidence.
233
233
 
234
234
  `hub_sdlc_costs` and the person's `ahub sdlc costs [owner/repo]` report linked merged PRs without changing the board. A review round is a board done-to-verdict handoff, not each reviewer in a multi-reviewer round. An observed handoff still counts when its declared scope was none. The report counts distinct check-workflow run ids, extra attempts (`run_attempt - 1`), full re-reviews, delta rounds and summed review intervals, with GitHub review counts alongside them. Unspecified scope and unavailable evidence stay unknown/incomplete. Only current public links are projected; a private counterpart withholds the whole PR. Up to 16 linked PR reads share 20 seconds; each PR covers at most 100 commits, reviews and recent check-workflow runs, and the pure aggregator bounds 500 tasks, 200 PRs and 10,000 history entries. Truncation is explicit. S10 exposes pure band-breach proposal data; S7 owns rolling baselines, band configuration, automatic scheduling and board-card creation.
235
235