@staix/agent-hub 0.12.20 → 0.12.22

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 (67) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +17 -9
  3. package/docs/agent-notes/adapters.md +13 -3
  4. package/docs/agent-notes/benchmarks.md +1 -1
  5. package/docs/agent-notes/bus.md +7 -3
  6. package/docs/agent-notes/daemon.md +12 -2
  7. package/docs/agent-notes/local-worker.md +5 -1
  8. package/docs/agent-notes/tasks.md +4 -2
  9. package/docs/agent-notes/tests.md +5 -2
  10. package/docs/bench.md +143 -0
  11. package/docs/events.md +15 -5
  12. package/docs/operations.md +510 -54
  13. package/docs/quickstart.md +16 -11
  14. package/docs/research.md +92 -0
  15. package/docs/security.md +49 -11
  16. package/docs/smoke.md +138 -0
  17. package/docs/specs/2026-09-19-agent-hub-design.md +227 -20
  18. package/docs/specs/2026-09-20-upgrade-recovery-design.md +152 -18
  19. package/docs/verified.json +34 -17
  20. package/package.json +1 -1
  21. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  22. package/plugins/agent-hub/server.js +3 -2
  23. package/src/adapters/acp.ts +77 -10
  24. package/src/adapters/codex-appserver.ts +72 -2
  25. package/src/adapters/local-worker.ts +46 -11
  26. package/src/adapters/pi.ts +215 -22
  27. package/src/cli/bench.ts +287 -0
  28. package/src/cli/console-state.ts +226 -28
  29. package/src/cli/console.ts +16 -4
  30. package/src/cli/facts-hook.ts +50 -8
  31. package/src/cli/help.ts +16 -5
  32. package/src/cli/identity.ts +10 -4
  33. package/src/cli/launch.ts +272 -18
  34. package/src/cli/main.ts +290 -30
  35. package/src/cli/preview.ts +13 -3
  36. package/src/cli/recovery-package.ts +17 -0
  37. package/src/cli/status-lines.ts +2 -1
  38. package/src/cli/terminal-recovery.ts +83 -31
  39. package/src/cli/upgrade-interactive.ts +297 -0
  40. package/src/cli/upgrade-runtime.ts +123 -20
  41. package/src/cli/upgrade.ts +108 -19
  42. package/src/hub/bench.ts +211 -0
  43. package/src/hub/bus.ts +24 -7
  44. package/src/hub/conductor.ts +22 -21
  45. package/src/hub/config-trust.ts +1 -0
  46. package/src/hub/control-client.ts +4 -1
  47. package/src/hub/daemon.ts +477 -86
  48. package/src/hub/delivery-journal.ts +6 -6
  49. package/src/hub/events.ts +4 -2
  50. package/src/hub/lifecycle.ts +8 -13
  51. package/src/hub/manager.ts +16 -14
  52. package/src/hub/peers.ts +2 -2
  53. package/src/hub/permission-mode.ts +44 -0
  54. package/src/hub/recovery-store.ts +59 -9
  55. package/src/hub/registry.ts +36 -22
  56. package/src/hub/research.ts +255 -0
  57. package/src/hub/tasks.ts +99 -55
  58. package/src/hub/tool-refusal.ts +7 -0
  59. package/src/hub/ui.ts +4 -1
  60. package/src/local/deny.ts +48 -1
  61. package/src/local/sandbox.ts +6 -3
  62. package/src/local/tools.ts +34 -11
  63. package/src/pi/extension.ts +43 -20
  64. package/src/pi/process-signature.ts +23 -0
  65. package/src/pi/tool-receipts.ts +24 -2
  66. package/src/ui/index.html +98 -17
  67. package/src/ui/task-progress.ts +33 -0
@@ -89,7 +89,8 @@ Facts below are tagged **verified** (measured on 2026-09-19 on the owner's Mac)
89
89
  7. Quota relay hands open tasks to the `local` peer first, then waits for the window
90
90
  reset for judgment-heavy classes. Task-level escalation on verification failure.
91
91
  8. Safety defaults: cross-peer text is framed as untrusted; permission prompts stay on
92
- unless `--unattended`; ports bind loopback only; Kimi ACP permission requests are
92
+ unless `--unattended` or an operator-selected permission mode (issues #240,
93
+ #242); ports bind loopback only; Kimi ACP permission requests are
93
94
  relayed to the hub console. Loopback is not enough on its own (any web page can open a
94
95
  WebSocket to 127.0.0.1), so the control WS requires a per-run token and both hub
95
96
  servers refuse requests that carry an `Origin` header (amended in M1).
@@ -97,8 +98,9 @@ Facts below are tagged **verified** (measured on 2026-09-19 on the owner's Mac)
97
98
  10. Shared memory reuses claude-mem as the single store; the hub never runs its own
98
99
  memory database. Native capture stays where it exists (Claude hooks, Codex plugin,
99
100
  Kimi transcript bridge); the hub captures only the local worker and adds
100
- cross-platform recall, task-scoped briefs and explicit shared notes. The bus still
101
- passes messages, never transcripts.
101
+ cross-platform recall, task-scoped briefs and explicit shared notes. Hub tool calls
102
+ are not captured; Tasks alone decides what screened tool text reaches memory.
103
+ The bus still passes messages, never transcripts.
102
104
 
103
105
  ## Design
104
106
 
@@ -279,9 +281,10 @@ peer. Peer ids claimed over the control WS must not be `user` or a hub-managed a
279
281
  injection is dropped for the same reason as `dynamicTools`.
280
282
  - PII (amended in M4): a task matching `signals.pii_patterns` is owned by `local` or by
281
283
  nobody; its envelopes are `private` (console tail and `hub.log` print a stub), lists show
282
- `[pii]` to everyone but `local`, `ahub task show` is the one place the console reads it; the
283
- reviewer is the console user; `local` answers such a turn to the console only, keeps it out
284
- of its history, refuses it when the only gateway is off campus (Cloudflare Access), and
284
+ `[pii]` to everyone but `local` inside a PII turn; `ahub task show` is the one place
285
+ the console reads it. The reviewer is the console user; `local` answers such a turn
286
+ to the console only, keeps it out of its history, refuses it when the only gateway
287
+ is off campus (Cloudflare Access), and
285
288
  nothing reaches claude-mem (no brief, no note, no capture), because claude-mem's observer is
286
289
  a cloud model (verified: `/api/health` reports `ai.provider: claude`).
287
290
 
@@ -486,7 +489,8 @@ inside a peer.
486
489
  ### Safety
487
490
 
488
491
  - Untrusted framing on all cross-peer text; standing instruction once per session.
489
- - `ahub claude` and `ahub codex` keep normal permission prompts. `--unattended` opts into
492
+ - `ahub claude` and `ahub codex` keep native permission defaults unless the person
493
+ selects a permission mode (issues #240, #242). `--unattended` opts into
490
494
  `--dangerously-skip-permissions` (Claude) and `--dangerously-bypass-approvals-and-sandbox`
491
495
  (Codex 0.154.0 documents this flag, not `--yolo`) and prints a warning.
492
496
  - Kimi and local tools: cwd-scoped, secrets denylist (a secrets directory, `.env*`, keys).
@@ -496,7 +500,7 @@ inside a peer.
496
500
  ### Configuration and state
497
501
 
498
502
  - `.agenthub/config.json` (roles, ports, filter tiers, watchdog), `.agenthub/routing.toml`.
499
- - `.agenthub/state/` gitignored: `hub.pid`, `status.json`, `control-token` (0600, per run),
503
+ - `.agenthub/state/` gitignored: `status.json` (with the daemon's pid and process signature, #226; daemons up to 0.12.20 also wrote `hub.pid`), `control-token` (0600, per run),
500
504
  `hub.db` (tasks, messages, budget; from M4), `hub.log`, `switchyard.toml`, `checkpoint.md`.
501
505
  - Env: `AGENTHUB_STATE_DIR`, `AGENTHUB_OMNIROUTE_URL`, `OMNIROUTE_API_KEY`,
502
506
  `AGENTHUB_SWITCHYARD_BIN`, `AGENTHUB_UNATTENDED`.
@@ -763,7 +767,8 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
763
767
  `hub_send` or a task tool, are answered with their `allow_once` option by the
764
768
  hub, without a console prompt, and logged by tool name only. This matches the
765
769
  `approval_mode = "approve"` Codex already gets for the same tools. Every other
766
- request keeps the console path; `allow_always` is never chosen automatically.
770
+ request keeps the console path unless the person opts into the permission modes
771
+ below (issues #240, #242); `allow_always` is never chosen automatically.
767
772
  - For the other tools, the console payload falls back to the streamed argument
768
773
  text once it is a complete JSON object. A partial stream stays unresolved and
769
774
  keeps `allow_always` withheld.
@@ -776,6 +781,84 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
776
781
  configured server of the same name, so only the hub's `agent-hub` server can
777
782
  produce the approved names.
778
783
 
784
+ ## Amendment: operator permission modes (issues #240, #242)
785
+
786
+ - Modes: ask leaves native decisions unchanged; ask-when-needed uses native
787
+ Kimi/Codex policies or scoped Claude/Pi/local grants; never-ask suppresses
788
+ prompts within each peer's existing execution boundaries.
789
+ - Runtime CLI permission changes are refused from ordinary agent shells.
790
+ Never-ask needs --yes; console mode selection needs a separate y. These
791
+ policy/role checks do not contain a hostile same-user shell: Kimi can clear
792
+ markers, use --yes or read the control token and impersonate the console.
793
+ The security and operations guides name that boundary explicitly.
794
+ - permission_modes defaults merge per peer across config files. Automatic
795
+ effects stop at ask-when-needed. Never-ask from an applied untracked config remains ask until
796
+ a person's source-labelled console y at hub start; no boot --yes applies it.
797
+ Git-tracked mode defaults are ignored and never offered for confirmation.
798
+ Every non-ask start logs mode and source (config file or runtime choice). Successful
799
+ runtime changes clear pending config confirmations; startup choices are reconciled
800
+ under the peer fence before a new adapter is exposed to bus delivery. Runtime changes and
801
+ confirmations remain daemon-local and expire on stop.
802
+ - Newly added native peers replay their non-offline state to drain queued work,
803
+ publish state telemetry and persist sessions. Offline/absent ask clears only
804
+ the hub choice. Unattended metadata is saved after successful startup, and
805
+ concurrent joined starts with different flags are refused. Refused default
806
+ confirmation remains handled until state changes; Esc defers without declining.
807
+ - Kimi mapping requires the actual Kimi Code CLI identity: yolo for
808
+ ask-when-needed, auto for never-ask, default for ask. Another ACP agent starts fresh or resumed as unmanaged: its reported mode is
809
+ untouched, and runtime changes are refused until its table exists. Set-mode replies precede prompts;
810
+ missing ids/refused startup changes fail. A timeout is unknown/offline and
811
+ the owned process group stops, so a late reply cannot restore a live peer.
812
+ Loaded non-default Kimi modes are reset for project ask.
813
+ - Codex overlays on-request/never on every outgoing turn/start without changing
814
+ sandbox fields. Ask restores a known native policy once after an accepted
815
+ turn; rejected restoration remains pending. Restoration debt and captured
816
+ policies are per thread and survive proxy detach and thread/start or thread/resume.
817
+ A known restoring turn retains its baseline even if the TUI echoes the resumed
818
+ response's sticky override, in any hub permission mode. After restoration is
819
+ admitted, a later repeated TUI policy is indistinguishable from an explicit
820
+ native choice and passes through; only the restoring turn is protected.
821
+ Unknown native baseline refuses restoration while attached, with instructions
822
+ to close the TUI and run `ahub permission codex ask`; detached ask succeeds and
823
+ returns a CLI-visible note about the old thread's unchanged native policy. Actual native unattended flags, not broker-wide flags, govern
824
+ the refusal to change a bypassed session.
825
+ - Claude ask-when-needed allows only Read/Edit/Write/MultiEdit/NotebookEdit/LS
826
+ and bounded Glob/Grep targets resolved with realPath beneath project root,
827
+ outside .agenthub/.git/.claude/.codex/.qwen/.kimi/.pi/.mcp.json. Missing/nonresolving or escaping targets,
828
+ protected case aliases, symlinks and uncertain wildcard/filter targets yield
829
+ no decision. Never-ask answers allow; ask and transport errors decide nothing.
830
+ Merged/generated settings are 0600 state files passed by path, never inline
831
+ in argv. Preview writes no settings file. The owning launcher removes its settings file
832
+ after native exit; earlier active or unknown launchers retain theirs. Verified
833
+ dead wrapper and native identities permit crash fallback cleanup. Direct binary
834
+ launches publish the native child PID/signature while running, bound to the exact
835
+ launch record; crash cleanup additionally requires its PID absent from a verified
836
+ process table. Script/interpreter native ownership remains unknown. Without
837
+ separate native identity proof, prior files remain for manual inspection. Every
838
+ launch atomically publishes its managed hook/session record without depending
839
+ on the native-proof lock. Exact-binding native proof is stored separately, so
840
+ an old callback cannot replace a later launch record. The populated signed
841
+ native-proof lock waits up to 250 ms for a live owner; an unknown/busy lock
842
+ disables only native identity/crash cleanup with exact-path guidance. Managed
843
+ hooks keep working for the current launch.
844
+ - Permission-only Claude PreToolUse never drives busy state. Only the former
845
+ observation purposes retain native turn bookkeeping. Hook identity/purpose
846
+ are launch/session bound; unverified hook status reads unverified.
847
+ - Pi/local read never asks. Ask-when-needed grants write/edit inside the project, outside the shared native
848
+ configuration segments (.claude/.codex/.qwen/.kimi/.pi/.mcp.json, compared folded and by the path within the
849
+ project) and never through a hard link; bash and mutating
850
+ git retain console approval. Never-ask grants once, retaining sandbox and
851
+ path guards; no mode gives allow-always. ACP tools have no hub sandbox and
852
+ the never-ask confirmation names that difference.
853
+ - Console-role permission and permission_default requests are additive;
854
+ ordinary tools/conductor/messages cannot change or confirm modes. Status
855
+ includes non-ask/unverified/unknown mode state and pending defaults; facts
856
+ replies include permission and authenticated projectRoot. Unknown requests
857
+ are answered, so PROTOCOL remains unchanged. permission_mode events retain
858
+ schema 1 and include only peer/from/to, never tool arguments.
859
+ - Live benign Kimi/Claude results and unverified Codex/Pi limits are recorded
860
+ in docs/smoke.md. No extra account prompts accompany these review corrections.
861
+
779
862
  ## Amendment: approval notifications and timeout (issue #5)
780
863
 
781
864
  - A waiting permission request raises one macOS notification (`osascript`) with
@@ -994,7 +1077,11 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
994
1077
  awaited an approval. An approval is saved before its dependents are assigned,
995
1078
  so the daemon's 60 s release timer sweeps for ownerless tasks whose last event
996
1079
  is `blocked` or `ready` and that wait for nothing: each is offered once per hub
997
- run, once an attached peer can take it, outside recovery operations.
1080
+ run, once an attached peer can take it, outside recovery operations. An offer
1081
+ is used up only by an assignment, after an approval and on the timer alike;
1082
+ failed dependent reads, ready writes or assignments do not escape a saved
1083
+ approval. Their notices are best effort because notification can read the
1084
+ same unavailable board. An ownerless task stays eligible once a peer can take it.
998
1085
  - A recovery commit also waits for task operations in flight and for completion
999
1086
  checks queued or running: both can write the board after its integrity digest
1000
1087
  (a check the commit's stop kills records `check interrupted`).
@@ -1360,14 +1447,59 @@ approval. Deny is direct. Pending requests include expiry and are withdrawn by
1360
1447
  Panels expose Peers, Approvals, Tasks, Queue and Events with bounded polling and
1361
1448
  Unicode cell widths, fall back below 80x24, and restore terminal state on exit.
1362
1449
 
1450
+ Whole-board progress (#246) uses one pure `taskProgress` function shared by the
1451
+ console and hashed dashboard script. It reads only public id/state/deps fields;
1452
+ PII stubs count like other tasks. Waiting partitions proposed tasks whose public
1453
+ dependencies are not approved, including unknown dependencies. Approved/all gives
1454
+ the done fraction (zero on an empty board). Four stages are proposed, in progress,
1455
+ in review and approved; changes requested returns to stage two and is marked.
1456
+ The console Tasks panel has a 20-cell ASCII summary bar and fixed four-cell row
1457
+ meters, preserving width and color-off geometry. Cells and percentage round down;
1458
+ count labels use importance order changes requested, review, waiting, in progress,
1459
+ proposed, then unknown when nonzero, and drop only from the end. The stream count is in the fixed footer,
1460
+ from existing all-state status counts; it makes no board fetch and retains a
1461
+ row-one scroll region for terminal scrollback. Loading/failed reads never imply
1462
+ confirmed empty progress. Dashboard filters affect rows only, not totals. Its
1463
+ full-width stacked SVG and stages have text equivalents; the existing signature
1464
+ guard prevents unchanged snapshots from mutating the progress live region.
1465
+ System/Light/Dark choice is applied as `data-theme` before first paint, persists
1466
+ in a one-year host-scoped `SameSite=Strict` cookie through guarded access and
1467
+ follows OS CSS preference on System. Cookie port independence shares the choice
1468
+ between hub starts and projects. The shared model is injected before the page's
1469
+ own inline script/style CSP hashes, with no new control message or CLI JSON change.
1470
+ UI public rows expose optional public-view deps for waiting. This is additive
1471
+ metadata enrichment of the existing ui_snapshot payload, not a changed control
1472
+ message envelope/operation, so it does not bump PROTOCOL. Unknown task states
1473
+ count in an explicit unknown bucket with stage zero; no NaN or prototype-key
1474
+ updates are permitted. Approval fraction is derived from counts/total at display
1475
+ time, avoiding a redundant floating-ratio field.
1476
+
1363
1477
  Console semantic colors (issue #201) use spans with a fixed terminal-native
1364
1478
  palette: cyan information, bold cyan active/selected labels, green availability
1365
1479
  and success, yellow attention/waiting, red failure/intervention, and restrained
1366
1480
  bright-black metadata. Offline peers, ordinary titles, action details and body
1367
1481
  lines use the default foreground. Existing labels, selection markers and
1368
- confirmation prompts remain sufficient without color. Stream headers use event
1369
- structure for their tone; body text is never parsed for meaning or passed
1370
- through as terminal styling. Denial requests and observed expiry/cancellation
1482
+ confirmation prompts remain sufficient without color. Console stream headers
1483
+ color meaningful tokens only (#239; dated issue Decisions): Claude bright blue
1484
+ (94), Codex bright cyan (96), all other peers default; `task` magenta (35, never
1485
+ bold), numbers bold default foreground (1), task references magenta underlined
1486
+ (4;35), and other issue references underlined default foreground (4). No token
1487
+ SGR equals a state or peer SGR. Green/yellow/red remain states/approvals/failures,
1488
+ cyan remains navigation. Case-insensitive `task #N`, `Task #N`, `[task #N]` and
1489
+ `[review #N]` identify task references; unbracketed prose `review #N` remains an
1490
+ issue reference. Ordinary words keep default foreground;
1491
+ there is no hashing or user theme.
1492
+ Only hub-written peer slots, state words and structural markers receive their
1493
+ semantic tones; title words and markers cannot choose them. Numeric tokens have
1494
+ whole-token boundaries, so UUIDs and ISO identifiers stay plain. Legacy notices
1495
+ lack typed reference ranges: the task/review prefix heuristic is bounded by a
1496
+ `ponytail:` comment; other `#N` tokens use the issue tone until structured notice
1497
+ spans classify assign/which references. Header text crosses `terminalText` before tokenization. Wrapped headers normalize
1498
+ tabs to spaces first; plain headers retain their original tabs in both color modes. Wrapped spans retain the source classification. After a projection
1499
+ miss on the first physical header, only structural tones remain. A missed
1500
+ continuation and all later continuations are fully plain. Projection advances
1501
+ a span index through the source once, keeping long permission titles bounded. Bodies and every
1502
+ command-output line stay plain. Denial requests and observed expiry/cancellation
1371
1503
  are red; an answered remote closure lacks option-kind metadata and is not
1372
1504
  guessed to be a denial.
1373
1505
 
@@ -1654,7 +1786,10 @@ push `accepted` and show none of them.
1654
1786
  to the newest end of the cache) for `reply_to` like a push, and a read clears
1655
1787
  the peer's failure streak as a completed push does. If that journal write fails, only this peer's queue
1656
1788
  and preface are put back (every pause stays) and the bus stops with
1657
- `delivery journal unavailable`. A pull is not a native turn and is not
1789
+ `delivery journal unavailable`. A refused or failed `resolveDelivery` likewise
1790
+ puts back only that peer's queue and preface and keeps every pause. Queue
1791
+ contents change in place, preserving arrays held by a suspended drain; only
1792
+ the constructor and recovery restore reload a bus snapshot. A pull is not a native turn and is not
1658
1793
  counted as supervision. The row is completed at hand-out, before anything
1659
1794
  shows the model saw it: a reply lost on the way, a tool call cancelled with
1660
1795
  Esc or a plugin that dies loses the batch (a `ponytail:` ceiling in
@@ -1789,9 +1924,9 @@ acts on the stopped state directory.
1789
1924
  Checked, then renamed by path: a symlink swapped in between still wins
1790
1925
  (marked `ponytail:`; `renameat` on a held descriptor would close it).
1791
1926
  - Between stop and act: after `stopProject` the CLI takes the project's
1792
- registry claim (`Registry.claim` with its own pid), the claim a daemon must
1927
+ registry claim (`Registry.claim` with its own pid and process signature), the claim a daemon must
1793
1928
  hold to run, and checks that no manifest names a daemon whose pid is alive
1794
- or uncertain (`processAlive`, the rule `inspectProject` uses); either failing
1929
+ or uncertain (`processLiveness`, the rule `inspectProject` uses, #226); either failing
1795
1930
  means a hub started in between and nothing is reset. A reset's claim carries
1796
1931
  the instance prefix `reset-`, so losing the claim to a concurrent reset says
1797
1932
  so. A crashed daemon's manifest (`status.json`, `control-token`, `hub.pid`
@@ -1800,7 +1935,7 @@ acts on the stopped state directory.
1800
1935
  removes it and `--all` archives it. With the claim held, the CLI checks the
1801
1936
  machine's recovery lock again, so an upgrade or recovery that took it in
1802
1937
  between does not run beside the reset. The claim is released when the reset
1803
- ends; a reset that dies leaves a dead pid, which the next claim takes over.
1938
+ ends; a reset that dies leaves a dead or reused pid with its signature, which the next claim takes over.
1804
1939
  - Failures: the plan is read only for the dry run, so `--all --yes` archives a
1805
1940
  state directory it cannot read as it is (its dry run says what it could not
1806
1941
  read). A failure after the stop says how far it got. A runtime reset that
@@ -1849,9 +1984,8 @@ acts on the stopped state directory.
1849
1984
  text right after the pattern check and before triage. "Held as private" is met
1850
1985
  by holding the task back entirely: until the verdict it is not on the board, so
1851
1986
  no envelope, notice, triage call, brief or note of the hub's can read it. The
1852
- local worker's claude-mem capture of its own tool calls is outside this: in an
1853
- ordinary turn it sends a proposal's arguments whatever the verdict (an existing
1854
- gap for every hub tool, not closed here). Writing it as
1987
+ local worker never captures hub tool calls (#230), so a proposal's arguments
1988
+ cannot bypass the verdict through capture. Writing it as
1855
1989
  a PII task first was rejected: a PII task opening lifts every silent turn-free
1856
1990
  cohort for good and switches facts off (#107, #108), so every proposal would do
1857
1991
  that. The proposal's tool call waits for the verdict, as it already waits for
@@ -1930,3 +2064,76 @@ acts on the stopped state directory.
1930
2064
  before any measurement: the model itself labels at least 90% of the positives
1931
2065
  `pii` (an unknown verdict counts as a miss). The report goes to
1932
2066
  `docs/verification/`; the script exits 1 under the bound.
2067
+
2068
+ ## Amendment: research records (issue #247)
2069
+
2070
+ - Opt-in per project (`research.enabled`, off by default). When a task is approved, the daemon builds its record from
2071
+ the project's `events.jsonl` with the same function `ahub research backfill` uses (`taskRecords` in
2072
+ `src/hub/research.ts`), so live and backfilled records agree, and appends it to the machine-wide store
2073
+ `~/.agenthub/research/<sha256(project id)[0:16]>.jsonl`. Records hold ids, states, counts, tokens and times only,
2074
+ because the events do; schema and fields: [research](../research.md).
2075
+ - The board has no terminal state but `approved`, and it is final, so each approved task gets one record; a person's
2076
+ later verdict (`ahub task label`, including `abandoned`) is a separate label record bound to the task's proposal
2077
+ time. A task is keyed by project, id and proposal time, because `ahub reset --all` restarts the ids and the store
2078
+ keeps the older records. Measures (success, first pass, rework, check failures over checks run, tokens and wall time
2079
+ per task) are derived when reported, never stored.
2080
+ - The record counts everything attributed to the task, the turn that approved it included: that turn ends after the
2081
+ approval and its usage arrives later, so the writer waits until no open turn is attributed to the task (polled every
2082
+ 10 s, at most 30 minutes; a stopping hub writes once its peers have stopped). The first record written for a
2083
+ task is final; a label binds to the current board's task, else to the store's only record with that id, or with
2084
+ `--created` to a named one from before a reset. Task events carry the board's move `reason` (a closed
2085
+ list) so reassignments are counted by reason.
2086
+ - Recording never blocks or fails the task flow: the writer runs after the board event, and a failure is one
2087
+ hub.log line per run. ponytail: it reads the whole events file for each record; an incremental per-task index
2088
+ if that file grows large.
2089
+ - Agents may read the measures and export; backfill and labels are a person's (identity gate).
2090
+
2091
+ ## Amendment: proven Pi tool refusals and idle exit recovery (issues #254, #255)
2092
+
2093
+ - A trusted guard uses `PreEffectToolRefusal` only when that operation has performed no task or board effect; PII screening and its telemetry record may precede the refusal. Pi receives the same `error: <message>` refusal as the control client, and its session/call receipt is `done`. Initial task ownership/dependency/state checks and conductor entry peer/mode/assignment/escalation validation opt into the type; shared helpers default to ordinary errors, and conductor checks after an awaited release validation stay ordinary. Generic errors, board readback failures after a write, and receipt settlement write failures remain uncertain/pending and are never repeated. A legacy pending row has no pre-effect proof in its stored fingerprint/state/result, so startup preserves it; neither present board state nor a matching error message establishes historical refusal.
2094
+ - Each Pi owner reports one exit with cause, exit code or signal, whether startup completed, whether a turn/tool was active, whether teardown was expected, and the last validated tool name only. TUI shutdown/owner-loss and verified stop teardown lack observed OS status and record unknown. A successfully stopped claimed TUI owner reports once; a never-owned launch reports no invented exit. Replacement teardown says the owner stopped for a new Pi owner even if replacement startup is refused. Internal teardown notices direct inspection and `ahub pi`, without claiming a person requested them. Replacement, failed-start and active-turn guidance takes precedence over the automatic-start-disabled notice. Console notices name the next action or automatic-start state; headless code 0 is still an exit and settles active deliveries as uncertain.
2095
+ - With `pi.auto_start`, an unexpected idle headless exit attempts its verified persisted session once in a 60 s window. The daemon owns the attempt/window across adapter replacements, blocks throughout an in-flight attempt, and rearms the 60 s window when that attempt settles. It checks current-owner identity, completed startup, turn/tool inactivity, start serialization, shutdown and recovery before and after awaited receipt drain/session capture. Missing or invalid persisted state leaves Pi offline with `ahub pi` guidance, with no fresh-session fallback. A second exit inside the window stays offline. Requested stops, stale owners, startup failures, active turns/tools and native terminal exits never trigger this automatic path; crash recovery's existing #66 behavior remains separate.
2096
+ ## Amendment: benchmarks (issue #251)
2097
+
2098
+ - `ahub bench` runs a person's suite file (tasks with a starting commit, an optional setup and a verify command)
2099
+ against the peers attached to a hub, in a project opted in with `bench.enabled`, because every attempt resets the work
2100
+ tree (`git checkout --force --detach <commit>` with HEAD checked against the pin, `git clean -ffdx -e /.agenthub`, then a
2101
+ clean-tree check). Before
2102
+ anything changes the runner requires the project to be the root of its repository, nothing tracked under `.agenthub/`
2103
+ (in any letter case) in the tree or in any suite ref, every ref to be a commit, which it pins for the run, and a
2104
+ clean tree. The runner is a console client: it proposes each
2105
+ task as the console does and polls the board, so the task flow is unchanged and no new control request exists.
2106
+ - `setup` and `verify` run in their own process group, bounded; the group is stopped at the bound, on an interrupt and
2107
+ when the command exits. An approved attempt's measures come from #247's `taskRecords` for the task proposed last with
2108
+ that id, read once the turns started since its proposal have ended (waiting only while their peers are busy or
2109
+ paused, at most 10 minutes, outside the attempt's duration); a timeout, an error, an interrupted wait, a wait that
2110
+ runs out with the turn still open, or a hub status that cannot be read while it is open leaves none, never partial
2111
+ ones. The fingerprint lists only the peers attached at the start.
2112
+ - Results go to `~/.agenthub/bench/<run>.jsonl` (ids, outcomes, exit codes, counts, times; never suite text or command
2113
+ output). A run's state comes from its end record or its runner's liveness (`processLiveness`). Comparisons are
2114
+ derived when asked: an arm is every run of it that is over (a stopped run included), runs of different suites or
2115
+ suite versions are not mixed unless asked, and each difference has a seeded 95% bootstrap interval, inconclusive
2116
+ below five attempts counted for that measure per arm (errors are not counted for pass rate or wall time).
2117
+ - A task that times out stops the run, because an agent may still work in the tree the next attempt would reset.
2118
+ ponytail: a board state to withdraw an open task would let the run continue. The fingerprint holds what the hub's
2119
+ status reports (a requested model for Pi, permission modes once #242 lands); ponytail: read each peer's launch
2120
+ settings for the rest.
2121
+ - The dashboard snapshot carries run summaries and per-arm measures only. `bench run` is a person's (identity gate,
2122
+ wherever `--json` stands); reading results is not. Schema and commands: [bench](../bench.md).
2123
+
2124
+
2125
+ ## Amendment: Pi/local approval expiry and cancellation (issue #253)
2126
+
2127
+ - An unanswered Pi/local approval returns an explicit expiry result: no person answered, do not retry that call, hand off or stop. It is not an answered denial. Two consecutive expiry results in the same native Pi turn or local worker turn end that turn with a console notice. An expiry counts toward the streak only when no person answered a request of that peer while it was pending (a per-peer answer epoch makes a parallel batch safe for an answering person); a grant served from the always cache or a permission mode is not an answer and does not reset the streak, and an actual new turn resets it. The turn ends through the same abort path an execution-budget stop uses, so Pi stays attached and the sender is told the approval reason. Supported Pi and local complete that correlated delivery after the reason reply, allowing the next queued message without an operator release. Pi clears the turn-only approval state at settlement and failure, so idle human shells remain usable and later failures cannot inherit its reason; a delayed approval abort rechecks that the same turn is still running. An extension that cannot abort a turn (one that does not advertise `approvalTurnAbort`) leaves Pi offline instead, with `ahub pi` restart and held-delivery settlement guidance. The second result remains an expiry tool failure before the abort. Approval stops do not invoke automatic safe replay or inference handoff.
2128
+ - Tool cancellation reaches the central approval registry. Settlement atomically removes the request and timer/abort listener before publishing closure or resolving. Late answers fail, cannot populate Pi's exact-tool always cache, and do not hold recovery waiting for approvals. Tool and budget continuations retain their captured session/generation; an admission response rechecks the owner and controller before changing a timer or recording a stop, and an old extension callback never aborts a later turn. Normal tool requests carry the session and generation captured before budget admission; the bridge refuses missing/stale lineage before registry, receipt or effect work, with restart guidance for an older extension. Spawned headless owners are pinned by the actual child process signature and its proven RPC session; TUI owners require the native claim. A Pi native abort is scoped to the verified owner, session, turn generation and call id; it withdraws only that call. Early native aborts fence a bounded set of call ids, with overflow failing closed for the remainder of that running turn. A managed tool that does not settle within the stop grace does not block the teardown: the stop goes on (shutdown, owner teardown, process stop) and reports the failure at the end.
2129
+ - A person's TUI shell command uses the existing authenticated idle reservation, session/owner/generation validation, sandbox and run-budget admission. It bypasses hub approval only in that trusted context and returns the sandbox's structured exit code through the receipt and native extension. Model tool arguments or printed output cannot establish human origin or exit status.
2130
+ - The dashboard counts waiting Pi/local approvals and directs the person to the console or `ahub tail` / `ahub permit`. It keeps those rows deny-only and never reveals private titles.
2131
+
2132
+ ## Amendment: requested headless peer stop (issue #278)
2133
+
2134
+ - A console-role `{ t: "peer_stop", peer }` request stops a hub-owned headless ACP, Pi or local runtime through its adapter's owned-runtime teardown. Success is `{ ok: true, state: "offline" }`; refusal or incomplete stop is `{ ok: false, error }` naming the reason and next action. New requests already receive an unknown-request reply on older hubs, so `PROTOCOL` stays unchanged.
2135
+ - Refuse agent/tools-role callers, unknown or offline peers, Claude/Codex and Pi TUI owners, and recovery/shutdown-held mutations. Starts, requested stops and permission changes are fenced per peer; recovery preparation waits until requested stops finish. A TUI is ended in its terminal, or by the upgrade plan's verified terminal binding.
2136
+ - A requested stop marks each in-flight delivery `needs_review` with a fixed ids-only stop reason, cancels pending approvals, and fences stale native completion/failure callbacks. It never completes or implicitly redelivers interrupted work, invokes failure escalation, or triggers Pi's idle auto-restart. The operator inspects effects and explicitly resolves the delivery before replay. Queued unattempted work remains for the next manual start under existing holds/pauses.
2137
+ - `ahub stop <peer>` is human-only in `classifyPeerCommand`, prints the returned state, and reports an older hub's unknown request as requiring an upgrade, with a nonzero exit. The upgrade plan's headless `endPlannedPeer` uses the same console request; the daemon does not close a native terminal.
2138
+
2139
+ - Requested stop admission also holds per-peer drain, pull and steering before any teardown await, without changing operator/budget pauses. Arriving work remains queued for explicit restart. Idle Pi captures live empty-session proof before stopping; a first accepted prompt with unpersisted history is not silently recreated as empty.
@@ -17,16 +17,24 @@ their matching CLI; the new coordinator never guesses a PID to terminate.
17
17
 
18
18
  ## Commands
19
19
 
20
- - `ahub upgrade --to <exact-version> --dry-run`: inspect the running registered
21
- projects, release identity, native session mappings and blockers.
22
- - `ahub upgrade --to <exact-version> [--yes]`: review the plan, revalidate it and
23
- schedule an independent recovery process. `--yes` accepts the displayed scope;
24
- it never overrides blockers.
20
+ - `ahub upgrade [--to <exact-version>] --dry-run`: inspect the running registered
21
+ projects, release identity, native session mappings and blockers. Without
22
+ `--to` the target is the registry's latest release (#272).
23
+ - `ahub upgrade [--to <exact-version>] [--yes]`: review the plan, revalidate it and
24
+ schedule an independent recovery process. `--yes` accepts the displayed scope
25
+ and needs `--to`; it never overrides blockers. On a terminal without either
26
+ flag the review, the apply and the progress are one interactive session (see
27
+ "Interactive screens").
28
+ - A target newer than the running CLI is handed to that release's own
29
+ coordinator (`bun x --package @staix/agent-hub@<version> ahub upgrade --to
30
+ <version>` with the same flags), named on stderr first and confirmed on a
31
+ terminal (#272).
25
32
  - `ahub --project <path|id> restart [--dry-run] [--yes]`: recover one project using
26
33
  the current package, without changing the global installation or plugin.
27
- - `ahub recovery status|resume <operation-id>`: read redacted progress or resume
28
- the recorded operation after checking live ownership.
29
- - `ahub recovery abort <operation-id>`: cancel a preflight and release its lock
34
+ - `ahub recovery status|resume [<operation-id>]`: read redacted progress or resume
35
+ the recorded operation after checking live ownership. Without an id every
36
+ `recovery` command takes the operation that holds the machine's lock (#272).
37
+ - `ahub recovery abort [<operation-id>]`: cancel a preflight and release its lock
30
38
  only before a runtime has stopped or a terminal mutation has been attempted.
31
39
 
32
40
  Upgrade uses all affected running registrations. Stopped projects stay stopped.
@@ -50,6 +58,9 @@ parents/hops, retries, dedupe state, prefaces, manual pauses and peer descriptor
50
58
  The existing task and budget databases remain authoritative. A mismatched or
51
59
  corrupt snapshot blocks startup. Snapshot restoration precedes peer attachment;
52
60
  release is idempotent and retires replayable snapshot state before delivery.
61
+ Journal-backed restoration retains missing empty queue entries from the committed
62
+ bus snapshot, including peers that remain offline. The journal stays authoritative
63
+ for queued work and receipts; a non-empty snapshot queue is never replayed over it.
53
64
  Release atomically moves that snapshot to a private per-operation archive before
54
65
  lifting the hold. Archives retain evidence for an uncertain release and are never
55
66
  automatically replayed. Preparation freezes the peer roster; a new unplanned peer
@@ -322,7 +333,12 @@ nor accepted when the target cannot read recovery waivers; `status` then says
322
333
  why in its own `freshSession` field, since `next` holds commands only. A runner
323
334
  record that cannot be read shows as `unknown` in `status`, never as no runner;
324
335
  while it reads so, resume, abort and dispose are all refused (the runner claim
325
- cannot tell its owner), so `next` names only `status` again. Otherwise `status` and every error that lists choices build them with one function
336
+ cannot tell its owner), so `next` names only `status` again. The runner row
337
+ carries its runner's process signature (#226): a row whose pid now belongs to
338
+ another process (after a reboot, for example) is no runner and the next claim
339
+ replaces it; one whose process exists but cannot be identified is `unknown`, and
340
+ the claim refuses it with that reason, never a raw `kill` error. A row an older
341
+ runner wrote has no signature and is judged by its pid alone, as before. Otherwise `status` and every error that lists choices build them with one function
326
342
  (`nextActions`), in this order: resume (which also launches a failed peer
327
343
  again), unless it can never get past what is live (`resumeBlocked`): a
328
344
  recorded final refusal (staging refused the fixed target or preserved source), a
@@ -330,11 +346,11 @@ missing project directory, a hub that speaks another control protocol, a
330
346
  runtime other than the one the receipt expects (a replaced source; a target not
331
347
  fenced to this operation, another instance or another version), a stopped
332
348
  pending source, a prepared source of a #215 coordinator that stopped with no
333
- commit request, a target that stopped after it started (restarting it is not
334
- supported in this release: its peers would have to be relaunched against the
335
- new instance), or any other stopped runtime without this operation's unreleased
336
- restart snapshot to start from (unavailable, starting or stopping is waited
337
- for). Then abort where `abortRefusal` allows it; a fresh session for each failed
349
+ commit request, a target that stopped after it started when the operation's
350
+ coordinator predates #225, or any other stopped runtime without this operation's
351
+ unreleased restart snapshot to start from (unavailable, starting or stopping is
352
+ waited for). A target that stopped after it started, with that snapshot, is
353
+ started again by resume (#225, below). Then abort where `abortRefusal` allows it; a fresh session for each failed
338
354
  Codex or Claude restoration, only where resume is offered (resume launches the
339
355
  new session, and dispose refuses `--fresh-session` on the same predicate); and
340
356
  last stop-and-archive where `disposeRefusal` allows it, or else `wait until
@@ -366,7 +382,15 @@ target instance. It is live when its process signature matches, gone when the
366
382
  pid no longer exists (ESRCH) or now belongs to another process, and unknown when
367
383
  the pid exists but its identity cannot be read. Unknown is never treated as gone. At planning, an
368
384
  unknown Claude launcher blocks: whether the attached session is managed cannot
369
- be told.
385
+ be told. A row that fails validation is never dropped and never read as "no
386
+ launcher" (#228): unless its readable `peer`, `projectRoot` or `instanceId` names
387
+ another launcher, it is unknown for that peer (it blocks binding, creation and the
388
+ plan's managed check), and `ahub <peer>` writes every such row back unchanged,
389
+ replacing only its own valid row. A launcher recorded by 0.12.12 or older hashed
390
+ `ps` in its own TZ and locale (the pin came with #177): when the pinned signature
391
+ differs, the signature computed the unpinned way in the reader's environment is
392
+ compared too, and a match is live; a reader in another environment still reads
393
+ such a launcher as gone.
370
394
  Restore reads this evidence in one place: the target's report, which counts only
371
395
  when the target runs as the expected instance, then the recorded launcher. A
372
396
  target that does not, a launcher that cannot be read, or a launcher record file
@@ -382,7 +406,18 @@ operation, and the runner keeps the disposition's own error. Abort and `next` de
382
406
  (`abortRefusal`). `status`, abort and the runner's errors read the same live
383
407
  state: every project not yet verified, its source or its target. A manifest
384
408
  whose pid no longer exists reads as stopped, whatever protocol it names; probing
385
- it would read unavailable forever. A stopped runtime also reports whose
409
+ it would read unavailable forever. So does a manifest whose pid now belongs to
410
+ another process (#226): from 0.12.21 `status.json` carries the daemon's process
411
+ signature (`pidSignature`, in the file only; the status reply is unchanged, so no
412
+ protocol bump), the registry claim carries it bound to the claiming instance, and
413
+ every ownership check (manifest, claim, runner, manager owner) goes through one
414
+ helper that reads live, gone or unknown. Recovery reads the claim from the
415
+ registry as it is now, never from the plan's frozen copy. Daemons no longer write
416
+ `hub.pid`; one an older daemon left is an unsigned legacy record. Records without
417
+ a signature give the answers they always did, so an unsigned claim whose pid was
418
+ reused still reads as starting: no command clears it (a hub started by 0.12.20 or
419
+ older that crashed before a reboot), and the operations guide gives the manual
420
+ clear after the process at that pid is checked. A stopped runtime also reports whose
386
421
  unreleased restart snapshot its state directory holds.
387
422
  An attached session is the target's report of that peer online with a thread
388
423
  (Codex) or session (Claude, Pi) id.
@@ -404,7 +439,9 @@ An attached session is the target's report of that peer online with a thread
404
439
  | project `prepared`, no `commitSent`, operation of a #215 coordinator | source stopped (crashed before any commit request) | blocks; the phase stays `prepared` (nothing was committed, so there is nothing to start from); not offered | abort if no effects, dispose | `<c> recovery abort <id>` without effects, else stop-and-archive (the lock refuses starting that hub by hand) |
405
440
  | project `prepared` | source unavailable, starting, stopping or not inspected | as above when it reads again | none: abort (its hold may still stand) and stop-and-archive are refused until it reads | `<c> recovery status <id>` once it answers |
406
441
  | project `prepared`, operation of an older coordinator (no `commitSent` written) | source not running, or not inspected | as its own runner does | dispose once it reads running, stopped or missing; abort neither offered nor accepted (it may have committed) | `<c> recovery status <id>` |
407
- | project `started` or `peers-restored` | target stopped (a crash or reboot after it started), with or without its snapshot | blocks; not offered: restarting it is not supported in this release | dispose; no fresh session | `the target stopped after it started; restarting it is not supported in this release` |
442
+ | project `started` or `peers-restored` | target stopped (a crash or reboot after it started), this operation's snapshot kept | records the restart (dead instance, time), retires the dead instance's done `restored:<peer>` receipts, moves the project back to `stopped` and starts the target from the snapshot; once per resume | dispose | `the target stopped after it started; resume restarts it from this operation's snapshot` (`stopped again after restart <n> in this resume` when it stops again) |
443
+ | project `started` or `peers-restored` | target stopped, snapshot gone | blocks; not offered | dispose | `this operation's restart snapshot is gone, so nothing can start it` |
444
+ | `retired:<peer>` (a done restoration of a dead target instance) | Orca lists its terminal, or cannot be read | closed under the close receipt rules (`closedRetired:<peer>` `pending` until Orca no longer lists it); an unreadable inventory changes nothing | dispose | `close Orca terminal <handle> by hand` or `check it in Orca`, `then <c> recovery resume <id>` |
408
445
  | project `stopped` | target stopped without this operation's snapshot (never committed) | blocks; not offered | dispose | `the target is stopped and this operation's restart snapshot is gone` |
409
446
  | project `stopped`, `started` or `peers-restored` | target running unfenced, as another instance or another version | blocks; not offered | dispose | `daemon is not owned by this recovery operation` (or `daemon instance changed`, `target daemon version mismatch`) |
410
447
  | any, at staging | preserved source or staged target changed, or (first staging of these bytes only) target protocol or recovery waivers refused | blocks; recorded as `final`, so resume is never offered again | abort if no effects, dispose | the refusal |
@@ -437,10 +474,39 @@ and its `next` entry says what that runner cannot do. Stop-and-archive stops a
437
474
  target that speaks an older control protocol at that protocol (a `kill` fenced
438
475
  by its instance), then waits for its manifest and registry claim as the
439
476
  lifecycle stop does. The disposition and its audit are written before the first
440
- act. A recovery launch of `ahub codex` records its launcher before the hub's
477
+ act. When the operation's coordinator predates #215, that same write sets the
478
+ receipt's `schema` to 2 (#227): an older runner never reads `disposition`, and an
479
+ older global `ahub recovery resume` still runs it, but every older runner and
480
+ abort refuses a schema other than 1 before it takes the lock or claims the
481
+ runner, so it exits having written nothing (an older `resume` still prints
482
+ `scheduled`; an older `status` still prints the receipt). A #215 coordinator's
483
+ operation keeps schema 1: its runner refuses a disposition itself, and a 0.12.20
484
+ CLI can still rerun its dispose. This release accepts schema 2 only with a
485
+ disposition, and the next abort or dispose that holds the runner claim moves a
486
+ schema-1 disposition 0.12.20 recorded on an older coordinator's operation to 2. A recovery launch of `ahub codex` records its launcher before the hub's
441
487
  `start` round trip; an ordinary launch records it after, so a refused start never
442
488
  replaces the record of a Codex already running.
443
489
 
490
+ #225: a target that stopped after it started is restarted only while its state
491
+ directory holds this operation's unreleased snapshot. The runner records the restart
492
+ in the project's receipt (`restarts`: the dead instance and the time) before acting,
493
+ moves each done `restored:<peer>` binding to `retired:<peer>`, clears the instance
494
+ and moves the project back to `stopped`, whose path starts the target from the
495
+ snapshot; the journal imports the snapshot only at revision 0, so the second start
496
+ continues from what the first wrote. `closed:<peer>`, `commitSent`, `fresh`, waivers
497
+ and the install flags stay. Restore closes each retired terminal (or finds it
498
+ absent) before relaunching that peer, and a peer whose retired session was an
499
+ accepted new one (fresh choice, fresh start, zero-turn Claude) is relaunched by that
500
+ id, which the operation-fenced waiver already covers, unless it has nothing to resume
501
+ (a Codex thread with no rollout, a Claude session with no transcript): that one starts
502
+ new under the waiver. Evidence reads the launch
503
+ records of every instance the operation started for the project, the dead ones
504
+ included, and a peer attached to the new instance without a session id is a session
505
+ of unknown identity, never "nothing attached". One restart per project per resume:
506
+ a target that stops again blocks with the restart count, so a crash loop never
507
+ spins unattended. Operations whose coordinator predates #225 keep refusing (their
508
+ own runner does), with stop-and-archive as the way out.
509
+
444
510
  A Codex thread with no rollout is not blocked at the plan only when the hub saw
445
511
  Codex start it (`native_thread` with `fresh`, logged by the adapter when it
446
512
  adopts a thread) and logged no Codex `turn_start` while it was the adopted
@@ -450,3 +516,71 @@ lose. Detaching forgets nothing; a thread whose start the log does not show
450
516
  (resumed, an older hub, a pruned log) is unsure and stays a plan blocker, as
451
517
  does one with turns. A planned fresh start still needs the store to show the
452
518
  rollout missing: an unreadable store blocks the close and the create.
519
+
520
+ ### Interactive screens (#272)
521
+
522
+ `ahub upgrade`, `ahub restart` and bare `ahub recovery` on a terminal are a
523
+ front end to the commands above, never a second coordinator.
524
+
525
+ - The screens (`src/cli/upgrade-interactive.ts`) read the plan, the receipt, the
526
+ runner claim and the live inspections, and change an operation only by running
527
+ `recovery resume|abort|dispose` or `reset` as a child process, with the
528
+ operation's own coordinator (`recoveryArgv`, the argv form of
529
+ `recoveryCommand`). Each command decides its own refusal.
530
+ - The operation screen's choices are `nextChoices`, the structured form of the
531
+ `next` list: `nextActions` is its text, and the invariant test that holds
532
+ `next` to what the commands accept also holds the two together.
533
+ - One choice is not in `next`: cancelling while a runner only waits for its
534
+ source (`cancellableWait`: phase `running`, step `prepare:<project>`, no
535
+ effect receipt, no commit request). The screen stops only a runner that
536
+ `signedRunner` names: a claim whose recorded process signature still matches.
537
+ `recoveryRunner` answers an unsigned claim (a coordinator of 0.12.20 or older)
538
+ from its bare pid, which is enough to wait for and never enough to signal, so
539
+ cancel is not offered for it. The receipt is read again at the key press, and
540
+ `recovery abort` then reads what is live once more. A runner killed after it
541
+ recorded an effect leaves an operation that abort refuses and resume
542
+ continues, as after any runner crash.
543
+ - The plan screen can end attached agents before the operation exists (user
544
+ request, 2026-10-10: choose to end headless or TUI agents during an upgrade).
545
+ A TUI agent is ended by closing the terminal the plan bound to its session; a
546
+ headless agent by the console-role request `peer_stop` to its hub (#278),
547
+ which an older hub answers as unknown. The plan is not acted on as it was
548
+ drawn: `endPlannedPeer` reads again the lock, the hub instance and the peer's
549
+ attachment, and for a TUI agent the attached session and the terminal's
550
+ handle and incarnation (`inspectTerminals` for that one session); any
551
+ difference ends nothing. `closeTerminal` itself compares only the terminal's
552
+ handle, incarnation, worktree and root. An ended agent is offline in the next
553
+ plan, so it is neither waited for nor restored. Nothing is ended once an
554
+ operation holds the lock: the coordinator's roster check reads a planned peer
555
+ that left as a changed source. With several projects in the plan an agent is
556
+ named `<project id>/<peer id>`, and a bare name attached in more than one
557
+ selects none. A Ctrl+C between two agents leaves the rest alone.
558
+ - A Ctrl+C while `apply` checks the plan again creates no operation.
559
+ - Ctrl+C leaves the screens and stops nothing; the terminal is not read again
560
+ after it, because a parent process that died on the same signal (`bun x`, a
561
+ hand-over) may have returned the terminal to the shell. While a progress view
562
+ runs, a line (Enter) opens the operation screen instead. A prompt answers an
563
+ interrupt or the end of input with no value, never with text.
564
+ - A follow that starts right after a command scheduled a runner (resume, a
565
+ fresh session) waits for that runner's first write, up to 5 seconds: until
566
+ then the receipt still shows the blocked state it was scheduled from. It
567
+ starts from the receipt as the command left it (a fresh-session choice writes
568
+ the receipt itself before it schedules the runner).
569
+ - The daemon's `recovery inspect` lists every condition `recoveryReady()` waits
570
+ for in `blockers` (task commands, completion checks, Pi calls and a release
571
+ in flight, an unsettled Pi, besides busy peers, approvals, startup and a
572
+ budget transition). The roster identity comparison is not listed. A hub that
573
+ is not ready and names no cause is shown as such.
574
+ - The progress view prints the receipt's steps and, during `prepare`, the
575
+ source's own readiness blockers. `inspectRecovery` copies them into
576
+ `Inspection.recovery.waiting` for display only; `planFingerprint` leaves
577
+ `recovery` out, so they never change a plan's identity.
578
+ - Without `--to` the target is `npm view @staix/agent-hub version`. `--yes`
579
+ keeps requiring `--to`, so an unattended apply names its release. `--to` is
580
+ checked as an exact version before it reaches the registry or a package spec.
581
+ - The dry-run plan gains `projects[].source.recovery.waiting` when the hub
582
+ reports something in progress; nothing else in the non-interactive output
583
+ changes.
584
+ - Out of scope: a hub-side interrupt of a running turn (the running source may
585
+ be older and would not know the request), and answering approvals from the
586
+ screens.