@staix/agent-hub 0.12.21 → 0.12.23

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 (87) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +27 -12
  3. package/docs/agent-notes/adapters.md +15 -7
  4. package/docs/agent-notes/benchmarks.md +4 -1
  5. package/docs/agent-notes/budget.md +4 -0
  6. package/docs/agent-notes/bus.md +2 -1
  7. package/docs/agent-notes/daemon.md +12 -1
  8. package/docs/agent-notes/local-worker.md +4 -1
  9. package/docs/agent-notes/tasks.md +2 -1
  10. package/docs/agent-notes/tests.md +4 -0
  11. package/docs/events.md +17 -4
  12. package/docs/operations.md +478 -41
  13. package/docs/quickstart.md +14 -6
  14. package/docs/security.md +23 -11
  15. package/docs/smoke.md +133 -7
  16. package/docs/specs/2026-09-19-agent-hub-design.md +115 -15
  17. package/docs/specs/2026-09-20-upgrade-recovery-design.md +84 -8
  18. package/docs/verification/2026-10-10-cli-output-phase1.md +309 -0
  19. package/docs/verification/2026-10-10-cli-output-phase2.md +672 -0
  20. package/docs/verification/2026-10-11-0.12.23.md +90 -0
  21. package/docs/verified.json +32 -17
  22. package/package.json +1 -1
  23. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  24. package/plugins/agent-hub/server.js +14 -3
  25. package/src/adapters/acp-quota.ts +13 -0
  26. package/src/adapters/acp.ts +105 -17
  27. package/src/adapters/codex-appserver.ts +70 -35
  28. package/src/adapters/local-worker.ts +44 -10
  29. package/src/adapters/pi.ts +158 -19
  30. package/src/cli/bench.ts +2 -1
  31. package/src/cli/console-state.ts +28 -22
  32. package/src/cli/console.ts +20 -10
  33. package/src/cli/help.ts +25 -19
  34. package/src/cli/identity.ts +3 -3
  35. package/src/cli/init.ts +1 -1
  36. package/src/cli/launch.ts +182 -7
  37. package/src/cli/main.ts +409 -177
  38. package/src/cli/output.ts +363 -0
  39. package/src/cli/preview.ts +2 -1
  40. package/src/cli/recovery-package.ts +17 -0
  41. package/src/cli/reset.ts +3 -0
  42. package/src/cli/status-lines.ts +0 -28
  43. package/src/cli/terminal-recovery.ts +27 -1
  44. package/src/cli/upgrade-interactive.ts +297 -0
  45. package/src/cli/upgrade-runtime.ts +80 -9
  46. package/src/cli/upgrade.ts +50 -15
  47. package/src/hub/board.ts +2 -0
  48. package/src/hub/budget.ts +39 -7
  49. package/src/hub/bus.ts +15 -14
  50. package/src/hub/child-process.ts +11 -0
  51. package/src/hub/conductor.ts +23 -11
  52. package/src/hub/config-trust.ts +16 -5
  53. package/src/hub/conflicts.ts +2 -0
  54. package/src/hub/context-window.ts +8 -1
  55. package/src/hub/crash.ts +2 -1
  56. package/src/hub/daemon.ts +483 -83
  57. package/src/hub/db-timeout.ts +3 -0
  58. package/src/hub/delivery-journal.ts +2 -1
  59. package/src/hub/envelope.ts +1 -1
  60. package/src/hub/events.ts +4 -1
  61. package/src/hub/execution-budget.ts +2 -0
  62. package/src/hub/facts.ts +5 -4
  63. package/src/hub/git.ts +32 -0
  64. package/src/hub/hub-tools.ts +1 -1
  65. package/src/hub/lifecycle.ts +10 -4
  66. package/src/hub/manager.ts +18 -5
  67. package/src/hub/peers.ts +7 -5
  68. package/src/hub/permission-mode.ts +14 -0
  69. package/src/hub/project.ts +2 -1
  70. package/src/hub/recovery-error.ts +31 -0
  71. package/src/hub/recovery-store.ts +43 -4
  72. package/src/hub/registry.ts +2 -1
  73. package/src/hub/routing.ts +114 -13
  74. package/src/hub/settings.ts +392 -0
  75. package/src/hub/snapshots.ts +4 -2
  76. package/src/hub/start-mode.ts +48 -0
  77. package/src/hub/tasks.ts +158 -43
  78. package/src/hub/terminal-open.ts +64 -0
  79. package/src/hub/ui.ts +25 -13
  80. package/src/local/deny.ts +71 -2
  81. package/src/local/sandbox.ts +47 -6
  82. package/src/local/tools.ts +25 -6
  83. package/src/memory/recall.ts +2 -2
  84. package/src/models/mlx.ts +2 -1
  85. package/src/pi/extension.ts +43 -20
  86. package/src/pi/tool-receipts.ts +19 -2
  87. package/src/ui/index.html +172 -21
@@ -151,7 +151,9 @@ hub CLI / console ── control WS 127.0.0.1:<ctl> ─────────
151
151
  when it has one, otherwise what the `tool_call` update announced for the same `toolCallId`.
152
152
  A payload that cannot be resolved either way is titled as unresolved and its `allow_always`
153
153
  option is withheld, so a blind click cannot grant every later call of the session. The
154
- watchdog sends `session/cancel` before forcing idle. `--model` maps to `kimi --model <alias> acp`
154
+ watchdog sends `session/cancel`. A durable delivery goes offline with `needs_review`;
155
+ without a journal, the peer stays busy until its cancelled prompt reports, bounded by
156
+ 30 s, then goes offline with queued work retained if no report arrives. `--model` maps to `kimi --model <alias> acp`
155
157
  (verified flag). Reusable for `opencode acp`.
156
158
  - Local worker (amended in M3): a hub-native tool-calling loop over non-streaming
157
159
  `POST /chat/completions` with plain `fetch`, no SDK (the repo keeps zero runtime
@@ -243,9 +245,13 @@ interface Envelope {
243
245
  Every queue is bounded (`queue_cap`, default 200): on overflow the oldest non-`important`
244
246
  envelope is dropped and reported on the console.
245
247
 
246
- Inactivity watchdog per busy turn (default 300 s) forces `idle` after cancelling the silent
247
- turn (`session/cancel` for ACP, `turn/interrupt` for Codex); a late result of the cancelled
248
- turn is discarded. Queues are drained in order on `idle`. Delivery is at-least-once with
248
+ The inactivity watchdog per busy turn defaults to 300 s. ACP sends `session/cancel`: a
249
+ durable delivery goes offline with `needs_review`; on a bus without a journal, the peer
250
+ stays busy until the cancelled prompt reports, then drains on idle. After the 30 s bound
251
+ without a report it goes offline with queued work retained, never idle. Cancelled results
252
+ and errors are discarded; an error during that wait leaves one log line. Other adapters
253
+ keep their cancellation rules (`turn/interrupt` for Codex). Queues drain in order only
254
+ when the adapter is idle. Delivery is at-least-once with
249
255
  idempotency by `id`: a failed delivery returns to the queue head and is retried after 1 s, and
250
256
  after 3 failures it is reported as undeliverable on the console so one envelope cannot block a
251
257
  peer. Peer ids claimed over the control WS must not be `user` or a hub-managed adapter id.
@@ -382,12 +388,23 @@ inside a peer.
382
388
  and runs the original command unchanged. `~/.claude/settings.json` is never edited; a
383
389
  user-supplied `--settings` wins and turns the tee off. The `--settings` injection is
384
390
  verified live (2026-09-19: `claude-usage.json` appears within seconds, the wrapped HUD
385
- renders unchanged, `ahub budget` shows both windows). Kimi: `usage_update` tokens over a
386
- rolling 5 h against `budget.kimi_tokens_5h` (off by default). Verified live on kimi
391
+ renders unchanged, `ahub budget` shows both windows). Kimi has an optional soft
392
+ rolling 5 h ceiling for validated consumed-token readings, `budget.kimi_tokens_5h`
393
+ (off by default), and the observed Kimi Code CLI 2.1.1 five-hour provider refusal
394
+ is a hard source (#306). Only the pinned rejected-prompt text is recognised,
395
+ bound to verified native identity and the current turn. It reports `5h`, used 1,
396
+ source `kimi ACP usage-limit refusal`, and a conservative 300-minute reset
397
+ estimate; weekly/monthly limits and other ACP providers remain unverified. Verified live on kimi
387
398
  2.0.1: one update per turn, payload `{"sessionUpdate":"usage_update","used":<tokens>,
388
399
  "size":<context window>}`; `used` is the session's context occupancy against `size`
389
400
  (1M), not billed quota. ACP records this shape as context occupancy diagnostics,
390
- never as cumulative consumed tokens. Only validated cumulative totals or input/output
401
+ never as cumulative consumed tokens. Since #285 phase 1 it is also the peer's
402
+ native context reading (source `acp_usage_update`), shown in `ahub status`, the
403
+ console and the dashboard like Claude's and Codex's; the adapter accepts the
404
+ update whenever it names the current session, since 2.1.1's source text emits it
405
+ after the prompt resolves (an order read from the source, not yet observed live
406
+ through the hub; docs/smoke.md). A gate crossing records the
407
+ event and notice for an ACP peer but sends it no checkpoint request yet. Only validated cumulative totals or input/output
391
408
  pairs feed native token readings; unsupported shapes remain unknown. `ahub budget set`
392
409
  feeds a reading by hand. `local` has no quota and is never paused.
393
410
  - Gate at `budget.gate` (default 0.9) on any fresh window; readings older than
@@ -405,7 +422,17 @@ inside a peer.
405
422
  released, so it leads the first delivery. Moved tasks stay with their new owners. A manual
406
423
  `ahub pause` is never lifted by the coordinator; `ahub resume` does not override a budget
407
424
  pause, `ahub budget resume <peer>` does, and the coordinator then leaves that peer alone
408
- until the window that paused it has reset. A handoff waits until another peer is attached
425
+ until the window that paused it has reset, unless a provider refuses another turn:
426
+ that hard refusal ends the override and supersedes a pending soft checkpoint. An
427
+ untouched ACP refusal is requeued only after the hub confirms its budget pause;
428
+ otherwise it stays held for review. After activity, the delivery remains held
429
+ even though open tasks are relayed, and the queue stays held after the budget
430
+ reset until a person inspects and resolves it. Keep one pending resume notice carrying
431
+ every public move since the previous notice was delivered; retain an older unstructured
432
+ notice as public text. Within one hub run, retain a still-future reset bound for the same
433
+ window across an override, while an explicit provider reset remains authoritative. A
434
+ restored pause without per-window reading identity starts a fresh estimate at the next refusal.
435
+ A handoff waits until another peer is attached
409
436
  (right after a restart nobody is), readings keep their own timestamp, and a window whose
410
437
  reset time has passed no longer counts. A peer whose window reset while the hub was down is
411
438
  not paused again, and still gets the resume envelope once it attaches. With no status line
@@ -667,7 +694,7 @@ M6 internal inference, packaging
667
694
  | Anthropic-to-chat translation drops `tool_use` | Claude never runs on local models in this design |
668
695
  | Local worker executes untrusted room text | Untrusted framing, cwd scope, denylist, no `--unattended` by default |
669
696
  | Steering a Codex turn changes its plan mid-flight | Only `important` steers; default tier is `status` |
670
- | Kimi ACP has no quota API | Token-based soft limit only; documented |
697
+ | Kimi ACP has no quota API | Optional token soft ceiling plus the observed five-hour provider refusal as a hard source; conservative reset, other limits unverified |
671
698
  | claude-mem worker API is internal and unversioned for third-party clients | Client isolated in `src/memory/`, pinned to a plugin version range, fail-open everywhere, contract test against a fake worker plus one live smoke |
672
699
 
673
700
 
@@ -812,8 +839,15 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
812
839
  Loaded non-default Kimi modes are reset for project ask.
813
840
  - Codex overlays on-request/never on every outgoing turn/start without changing
814
841
  sandbox fields. Ask restores a known native policy once after an accepted
815
- turn; rejected restoration remains pending. Unknown native baseline refuses
816
- restoration. Actual native unattended flags, not broker-wide flags, govern
842
+ turn; rejected restoration remains pending. Restoration debt and captured
843
+ policies are per thread and survive proxy detach and thread/start or thread/resume.
844
+ A known restoring turn retains its baseline even if the TUI echoes the resumed
845
+ response's sticky override, in any hub permission mode. After restoration is
846
+ admitted, a later repeated TUI policy is indistinguishable from an explicit
847
+ native choice and passes through; only the restoring turn is protected.
848
+ Unknown native baseline refuses restoration while attached, with instructions
849
+ to close the TUI and run `ahub permission codex ask`; detached ask succeeds and
850
+ returns a CLI-visible note about the old thread's unchanged native policy. Actual native unattended flags, not broker-wide flags, govern
817
851
  the refusal to change a bypassed session.
818
852
  - Claude ask-when-needed allows only Read/Edit/Write/MultiEdit/NotebookEdit/LS
819
853
  and bounded Glob/Grep targets resolved with realPath beneath project root,
@@ -823,8 +857,17 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
823
857
  Merged/generated settings are 0600 state files passed by path, never inline
824
858
  in argv. Preview writes no settings file. The owning launcher removes its settings file
825
859
  after native exit; earlier active or unknown launchers retain theirs. Verified
826
- dead wrapper and native identities permit crash fallback cleanup. Without
827
- separate native identity proof, prior files remain for manual inspection.
860
+ dead wrapper and native identities permit crash fallback cleanup. Direct binary
861
+ launches publish the native child PID/signature while running, bound to the exact
862
+ launch record; crash cleanup additionally requires its PID absent from a verified
863
+ process table. Script/interpreter native ownership remains unknown. Without
864
+ separate native identity proof, prior files remain for manual inspection. Every
865
+ launch atomically publishes its managed hook/session record without depending
866
+ on the native-proof lock. Exact-binding native proof is stored separately, so
867
+ an old callback cannot replace a later launch record. The populated signed
868
+ native-proof lock waits up to 250 ms for a live owner; an unknown/busy lock
869
+ disables only native identity/crash cleanup with exact-path guidance. Managed
870
+ hooks keep working for the current launch.
828
871
  - Permission-only Claude PreToolUse never drives busy state. Only the former
829
872
  observation purposes retain native turn bookkeeping. Hook identity/purpose
830
873
  are launch/session bound; unverified hook status reads unverified.
@@ -1386,12 +1429,19 @@ Codex reports `tokenUsage.last.totalTokens / tokenUsage.modelContextWindow` in
1386
1429
  and [native TUI](https://github.com/openai/codex/blob/main/codex-rs/tui/src/token_usage.rs)
1387
1430
  distinguish the last active context from the accumulated `total`; the displayed
1388
1431
  raw occupancy does not apply the TUI's baseline-adjusted remaining percentage.
1432
+ The Kimi peer reports `used / size` in `usage_update` (source
1433
+ `acp_usage_update`, #285 phase 1; only Kimi is wired): the session's context
1434
+ occupancy, accepted whenever it names the adapter's current session, including
1435
+ outside a turn; 2.1.1's source text emits it after the prompt resolves (read
1436
+ from the source, not observed live), when the peer may already be idle.
1389
1437
  Pi's RPC state exposes no measured native counter. Its extension context API
1390
1438
  returns an estimate, so Pi and unsupported peers remain unknown here.
1391
1439
 
1392
1440
  Each reading records source and measurement time. Claude's status-line file is
1393
1441
  bound to the daemon instance, managed launcher and native session; Codex's
1394
- notification is fenced by its owning link and native thread. Stale, invalid,
1442
+ notification is fenced by its owning link and native thread; an ACP reading is
1443
+ bound to the adapter's own session id and dropped when that is not the daemon's
1444
+ current session. Stale, invalid,
1395
1445
  disconnected or replaced-session readings expose unknown occupancy, never zero.
1396
1446
  `context.gate` defaults to 0 (off); `context.stale_min` defaults to 30.
1397
1447
  A valid above-gate reading emits one metadata-only `context_pressure` event
@@ -1430,6 +1480,15 @@ approval. Deny is direct. Pending requests include expiry and are withdrawn by
1430
1480
  `permission_closed` on any answer or cancellation. Answer audits have no title.
1431
1481
  Panels expose Peers, Approvals, Tasks, Queue and Events with bounded polling and
1432
1482
  Unicode cell widths, fall back below 80x24, and restore terminal state on exit.
1483
+ Tabs change with the digits 1 to 5 or with Left and Right, wrapping at both
1484
+ ends, with the digit's guards and resets (an arrow under a confirmation dismisses
1485
+ it like any key but y, without moving a tab); a read holding nothing but arrow
1486
+ sequences is that many key presses, never a paste. An Events row's header is
1487
+ tokenized by the stream's own tokenizer on the same tab-flattened text, with the
1488
+ event's tone and kind; body lines stay plain, and selection changes only the
1489
+ `>` marker. The tail prints a peer's context reading once and repeats it only
1490
+ when the printed line changes apart from its time (the printed percentage, the
1491
+ freshness, the source, or known against unknown).
1433
1492
 
1434
1493
  Whole-board progress (#246) uses one pure `taskProgress` function shared by the
1435
1494
  console and hashed dashboard script. It reads only public id/state/deps fields;
@@ -2075,7 +2134,7 @@ acts on the stopped state directory.
2075
2134
  ## Amendment: proven Pi tool refusals and idle exit recovery (issues #254, #255)
2076
2135
 
2077
2136
  - 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.
2078
- - 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. Internal teardown notices direct inspection and `ahub pi`, without claiming a person requested them. Console notices name the next action or automatic-start state; headless code 0 is still an exit and settles active deliveries as uncertain.
2137
+ - 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.
2079
2138
  - 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.
2080
2139
  ## Amendment: benchmarks (issue #251)
2081
2140
 
@@ -2104,3 +2163,44 @@ acts on the stopped state directory.
2104
2163
  settings for the rest.
2105
2164
  - The dashboard snapshot carries run summaries and per-arm measures only. `bench run` is a person's (identity gate,
2106
2165
  wherever `--json` stands); reading results is not. Schema and commands: [bench](../bench.md).
2166
+
2167
+
2168
+ ## Amendment: Pi/local approval expiry and cancellation (issue #253)
2169
+
2170
+ - 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.
2171
+ - 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.
2172
+ - 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.
2173
+ - 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.
2174
+
2175
+ ## Amendment: requested headless peer stop (issue #278)
2176
+
2177
+ - 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.
2178
+ - 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.
2179
+ - 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.
2180
+ - `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.
2181
+
2182
+ - 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.
2183
+
2184
+ ## Amendment: settings from the dashboard and the terminal (issue #269, part 1)
2185
+
2186
+ - The dashboard stops being read-mostly for one closed list: a settings registry in `src/hub/settings.ts`, one entry per editable setting (key, type and allowed values, the store, when it applies, and whether it can raise authority). The dashboard's Settings section and `ahub settings list|get|set|undo` render from it and write through it; a key outside it cannot be written from either. Keys that choose what the hub runs or where secrets and task text go (`kimi_cmd`, `codex_bin`, `pi.cmd`, `checks`, `mlx`, `omniroute`, `memory.worker_url`, `local.read_allow`, `local.bash_network`, `local.network_allow`, `bench.enabled`, `terminal`) are refused by name from every surface.
2187
+ - First list: `permission.<peer>` (the running mode, the console's `permission` request), `permission_modes.<peer>` (the default at hub start), `pi.auto_start`, `routing.stay_switch`, per class `routing.classes.<class>.peers`, `.escalate_to`, `.route` and `.pi_backend`, and the switches `research.enabled`, `approvals.notify`, `snapshots.enabled` and `coordination`. Start modes (`peers.<id>.start_mode`, the terminal provider, Pi's TUI default) are part 2 of the issue.
2188
+ - Stores: config values go to `.agenthub/config.local.json`; routing values go to a new machine-local overlay, `.agenthub/routing.local.toml`, which may hold only those routing keys and is merged over `routing.toml` key by key by the one loader (`parseRouting`). `assign()` stays pure; its trace, and so `ahub route explain`, names the keys the overlay set. The tracked `config.json` and the hand-written `routing.toml` are never written, a machine-local file that git tracks is refused, and `ahub init` adds the overlay to `.gitignore`. A write is validated by the real loader first (a scratch copy for the config, the parser for the overlay), replaces the file through a temp file and rename, and keeps one previous version: undo restores it, refuses when the file changed since, and validates what it restores. The undo record is written after the file, so a write that failed leaves the earlier undo in place. A write that would change nothing (the same value again, or removing a value that is not set) writes no file, keeps the undo record and records no event. A config setting is refused in a project that has no config file at all (neither `config.json` nor `config.local.json`): its first config file would switch the project defaults on (snapshots, limits, the budget relay), and that is `ahub init`'s to do. In a repository where git cannot say whether the file is tracked, nothing is written; a project outside git has nothing tracked. Removing a machine-local value (`inherit`) is a write like any other. When config trust (#17) drops the written field at load (`permission_modes` in a file git cannot vouch for), the reply and the setting's row say that the hub does not read the value.
2189
+ - Authority. An ordinary dashboard session reads settings, lowers a mode to `ask`, turns `pi.auto_start` off and changes the safe switches. A settings session, opened by `ahub ui --settings` (a console command behind the identity gate, a console-role `ui` request with `settings: true`), may also raise: a non-ask mode or default, routing, `pi.auto_start`, removing a machine-local value, and undo. Its ticket marks the session for 15 minutes; afterwards the session continues as an ordinary one until its hour ends. `never-ask`, running or stored, needs the peer id as the confirmation (typed in the dashboard, `--yes` in the terminal); a stored never-ask default still waits for the console's `y` at each hub start (#242). No other path puts never-ask on disk: an undo that would restore it is refused (set it again with its confirmation), and `inherit` that would uncover a never-ask from the shared file needs the same confirmation. Tool approvals stay deny-only for Pi and local in every session. The unified dashboard forwards settings actions with ordinary authority.
2190
+ - A mode set from the dashboard or `ahub settings` is `changePermission` inside `permissionChange`, the console's path, with its refusals (not attached, unverified Claude hook, unattended Codex or Claude, a stop in progress) and its `permission_mode` event.
2191
+ - Preview before save: a routing change returns `route explain` for up to 20 open tasks under the policy in force and under the candidate (PII tasks are counted, not shown); a mode change returns the peer's bounds and what the mode grants.
2192
+ - Every change is announced in `hub.log` and open consoles with its source and recorded as a `settings` event: key, value before and after, source (`dashboard` or `terminal`), `undo`. Values are closed words, booleans, peer ids or route ids, never free text.
2193
+ - Control protocol: `settings_get`, `settings_set`, `settings_undo` and `settings_preview` are new console-role requests, and `ui` takes an optional `settings` flag and echoes it; an older hub answers the requests as unknown, and the CLI refuses a `ui` reply without the echo, so `PROTOCOL` stays 16. The `ui_snapshot` reply gains an optional `settings` object. Events schema stays 1.
2194
+ - `ahub settings` talks to the running hub (ponytail: a file-only path if setting a start-time value on a stopped hub is ever wanted).
2195
+
2196
+ ## Amendment: TUI by default, headless as opt-in (issue #269, part 2)
2197
+
2198
+ - `peers.<id>.start_mode` (`tui` or `headless`, for pi and codex; `tui` when unset) is the mode a start takes when nothing more specific was asked. Claude has only a TUI; Kimi (ACP) and the local worker have no hub-attached TUI and stay headless. The loader refuses a malformed `peers` block.
2199
+ - A person typing `ahub pi` gets that mode: the TUI in their terminal by default. `--mode headless` or `--headless` asks for headless. Without a terminal (the console's command line, a script) and with the default asking for the TUI, the CLI sends the console-role `peer_start` request and the hub opens a terminal, or refuses with the command; an option given then is refused, because the fixed command carries none. An explicit `--mode tui` is taken at its word. `ahub codex` and `ahub claude` typed by a person are unchanged.
2200
+ - A start the hub makes itself is `hubStart(peer)`: the dashboard's `start_peer` action, `peer_start`, the conductor's `hub_peer_start` and `pi.auto_start` at hub start. With `headless` it starts the peer as before (for Codex: the app-server only). With `tui` it opens a terminal running `<runtime> <CLI entry> --project <root> <peer>` (`--mode tui` for Pi), assembled from the hub's own words and quoted word by word; no caller text is part of it. The peer is reserved before the provider is asked, so starts that arrive together open one terminal, and a second start within 30 seconds of an opened terminal is refused instead of opening another (a failed open frees it at once). A start mode of headless never takes over a Pi that runs in a terminal.
2201
+ - Terminal providers, in order: `terminal.open`, a machine-local argv template (`{command}` in exactly one element, plus `{title}` and `{cwd}`; a `MACHINE_LOCAL` field under config trust and on the never-editable list), then Orca (`orca terminal create --worktree id:<id> --command ...`) when the hub runs in an Orca worktree or a recorded launch of the project names one. Each template placeholder is substituted once; only `{command}` is shell-quoted. The opened terminal's environment carries no agent marker and no Orca terminal identity of the hub's own. A provider's error text is logged, not returned. With no provider the start is refused with the manual command; it never falls back to headless. Only the running product wires the real providers; a daemon constructed without them opens nothing.
2202
+ - The conductor's `hub_peer_start` no longer takes a mode: it takes the peer's start mode, and a `mode` argument that differs is refused. It may now start claude and codex through the provider, and codex headless when a person set that.
2203
+ - Recovery restores each peer in the mode its recorded session had. After an unplanned stop, `pi.auto_start` no longer starts a fresh headless Pi in place of one that ran in a terminal (with or without a recorded session file), unless headless is Pi's start mode. `peers.<id>.start_mode` may be set in the shared config file too: it is not a machine-local field.
2204
+ - Settings: `peers.pi.start_mode` and `peers.codex.start_mode` join the registry (group Start, stored in `config.local.json`, raising except to `tui`). They are the one stored setting a running hub takes up: after a settings write the hub re-reads the block, so the mode applies at that peer's next start. The dashboard gains a Start control per peer that is not attached, saying the mode and the provider (or why there is none); it offers no button when the start would be refused. An ordinary session may press it: it runs only the fixed start. The page blocks repeat starts while the request is in flight and unlocks after a confirmed answer, leaving the hub's terminal reopen guard authoritative. The manager waits 35 seconds (the terminal provider cap plus its margin); a slower headless start can end unconfirmed. After an unconfirmed manager result the page holds that project/instance/peer control for `PEER_START_RETRY_MS` (the same 30-second bound as the hub's reopen guard), or until the `starts` snapshot confirms attachment, and names the retry bound. A confirmed project-local dashboard start behaves as before; no manager or settings authority changes.
2205
+ - Control protocol stays 16: `peer_start` is a new console-role request an older hub answers as unknown, and `ui_snapshot` gains the optional `starts` list.
2206
+ - Migration: Pi's default changes from headless to its TUI. `ahub settings set peers.pi.start_mode headless` (or the same key in `config.local.json`) keeps a headless auto-started Pi; scripts add `--headless`.
@@ -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.
@@ -508,3 +516,71 @@ lose. Detaching forgets nothing; a thread whose start the log does not show
508
516
  (resumed, an older hub, a pruned log) is unsure and stays a plan blocker, as
509
517
  does one with turns. A planned fresh start still needs the store to show the
510
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.