@staix/agent-hub 0.12.39 → 0.12.41

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 (81) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +16 -10
  3. package/docs/agent-notes/adapters.md +4 -1
  4. package/docs/agent-notes/budget.md +2 -0
  5. package/docs/agent-notes/bus.md +2 -2
  6. package/docs/agent-notes/daemon.md +10 -6
  7. package/docs/agent-notes/models.md +4 -4
  8. package/docs/agent-notes/tasks.md +3 -0
  9. package/docs/agent-notes/tests.md +1 -1
  10. package/docs/memory.md +1 -1
  11. package/docs/operations.md +152 -28
  12. package/docs/quickstart.md +23 -5
  13. package/docs/security.md +20 -8
  14. package/docs/smoke.md +2 -2
  15. package/docs/specs/2026-09-19-agent-hub-design.md +158 -70
  16. package/docs/specs/2026-10-04-switchyard-source-port-design.md +12 -7
  17. package/docs/verification/2026-10-20-0.12.40.md +63 -0
  18. package/docs/verification/2026-10-20-0.12.41.md +44 -0
  19. package/docs/verified.json +38 -19
  20. package/package.json +1 -1
  21. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  22. package/plugins/agent-hub/server.js +415 -151
  23. package/src/adapters/acp.ts +34 -1
  24. package/src/adapters/claude-channel.ts +35 -4
  25. package/src/adapters/codex-appserver.ts +12 -3
  26. package/src/adapters/definitions/builtin-metadata.ts +4 -2
  27. package/src/adapters/definitions/native-cli-codex-launch.ts +2 -2
  28. package/src/adapters/definitions/native-cli-doctor.ts +8 -5
  29. package/src/adapters/definitions/native-cli-entry-claude.ts +7 -2
  30. package/src/adapters/definitions/native-cli-entry-codex.ts +13 -14
  31. package/src/adapters/definitions/native-cli-entry-jcode.ts +13 -3
  32. package/src/adapters/definitions/native-cli-entry-pi.ts +9 -7
  33. package/src/adapters/definitions/native-cli-entry-types.ts +2 -0
  34. package/src/adapters/definitions/native-cli-jcode-mcp.ts +55 -1
  35. package/src/adapters/jcode.ts +110 -4
  36. package/src/adapters/local-worker.ts +21 -37
  37. package/src/adapters/pi.ts +7 -0
  38. package/src/adapters/registry-types.ts +2 -0
  39. package/src/cli/console-state.ts +1 -0
  40. package/src/cli/console.ts +11 -1
  41. package/src/cli/help.ts +13 -8
  42. package/src/cli/identity-audit.ts +33 -7
  43. package/src/cli/jcode-mcp.ts +1 -1
  44. package/src/cli/launch.ts +1 -1
  45. package/src/cli/main.ts +134 -24
  46. package/src/cli/output.ts +5 -2
  47. package/src/cli/status-lines.ts +1 -1
  48. package/src/cli/terminal-recovery.ts +54 -4
  49. package/src/cli/upgrade-interactive.ts +5 -0
  50. package/src/cli/upgrade-runtime.ts +13 -1
  51. package/src/cli/upgrade.ts +67 -1
  52. package/src/hub/board.ts +4 -2
  53. package/src/hub/bus.ts +8 -3
  54. package/src/hub/checks.ts +15 -5
  55. package/src/hub/conductor.ts +2 -1
  56. package/src/hub/config-apply.ts +20 -2
  57. package/src/hub/config-reload.ts +2 -8
  58. package/src/hub/control-client.ts +26 -3
  59. package/src/hub/daemon.ts +155 -68
  60. package/src/hub/delivery-superseded.ts +19 -2
  61. package/src/hub/events.ts +4 -1
  62. package/src/hub/hub-tools.ts +4 -4
  63. package/src/hub/inference.ts +4 -8
  64. package/src/hub/lifecycle.ts +3 -3
  65. package/src/hub/main-checkout.ts +36 -0
  66. package/src/hub/memory-profile-check.ts +6 -2
  67. package/src/hub/memory-profile.ts +18 -1
  68. package/src/hub/peers.ts +2 -0
  69. package/src/hub/ports.ts +1 -0
  70. package/src/hub/routing.ts +24 -14
  71. package/src/hub/settings.ts +1 -1
  72. package/src/hub/task-repository.ts +136 -0
  73. package/src/hub/tasks.ts +101 -21
  74. package/src/hub/terminal-launch.ts +130 -0
  75. package/src/hub/usage.ts +44 -18
  76. package/src/models/route/config.ts +1 -1
  77. package/src/omniroute/client.ts +7 -19
  78. package/templates/AGENTS.block.md +1 -0
  79. package/templates/routing.toml +10 -44
  80. package/src/switchyard/config.ts +0 -45
  81. package/src/switchyard/sidecar.ts +0 -136
@@ -1,6 +1,6 @@
1
1
  # Operations guide
2
2
 
3
- This guide describes ahub 0.12.39 and control protocol 16. Live verification
3
+ This guide describes ahub 0.12.41 and control protocol 16. Live verification
4
4
  results and remaining prerequisites are recorded separately in [the smoke ledger](smoke.md).
5
5
 
6
6
  ## Agent-operated live verification
@@ -32,9 +32,27 @@ Person verdicts and user-authored workflow writes are not delegated. Dashboard
32
32
  sessions issued through the flag retain operational controls but refuse message
33
33
  and task/planning content writes. Use native peer tools for agent-authored work.
34
34
 
35
- Use a CLI/coordinator containing this feature throughout the experiment. Older
36
- daemons do not understand the new delegated audit outcome; run the candidate
37
- daemon to capture its delegated event and console notice. This policy does not
35
+ Use a CLI/coordinator containing this feature throughout the experiment. Ordinary
36
+ delegation requires `cli_audit_delegated` in the authenticated running daemon's
37
+ welcome, rather than its protocol number or disk status. Execution reuses that
38
+ connection; all helper connections are pinned to its authenticated instance, and
39
+ local commands check that instance before dispatch. Replacement control
40
+ connections refuse. Before effects, the same daemon must acknowledge the exact
41
+ audit UUID after successfully appending its event; a feature claim or spool write
42
+ alone is insufficient. That retained event survives later consumer replacement,
43
+ including a delayed local setup confirmation. Missing or unavailable
44
+ support refuses before command effects with an upgrade remedy; its refused audit
45
+ uses an outcome older consumers retain. The private pending audit is reserved
46
+ before connecting, so an unavailable outbox still refuses before any connection.
47
+ Upgrade/recovery uses a compatible `run` spool row and stores the authoritative
48
+ delegation in the operation's `delegatedAudits`: peer, closed command label,
49
+ timestamp and operation id. Creation, resume handoff, runner, abort and disposal
50
+ persist this evidence before runtime/target effects, independently of the source
51
+ daemon's drain. Schema-1 receipts retain it through source replacement and resume;
52
+ stored evidence never grants a later invocation authority. The journal is capped
53
+ at 256 entries and refuses further delegated attempts rather than evicting its
54
+ initiator. Local bootstrap commands with no daemon manifest use the compatible
55
+ `run` row too. This policy does not
38
56
  change external computer-use restrictions. Recovery staging refuses old targets
39
57
  without live-test support before source effects. Delegated headless recovery restores through the ordinary target CLI, carrying
40
58
  its operationId on the delegated console connection; existing native ownership
@@ -195,7 +213,7 @@ In Tasks, `a` assigns the owner in ordinary open states and the reviewer in
195
213
  the corresponding role, and `r` reviews only in `in_review`. In Queue, `r`
196
214
  prefills `discard` for a queued row and `retry` for a needs_review row, with the
197
215
  action still editable before confirmation. `completed` is never the default.
198
- Status, console notices and `route explain` show every held row with its id, sender, kind and age. The hub completes rows made entirely of superseded hub workflow notices or presence items, recording an ids-only reason and the hub actor. Mixed messages, live task notices and unknown references remain held. A Claude session without channel support safely returns unseen pushes for `hub_inbox`; a thrown notification write remains uncertain.
216
+ Status, console notices and `route explain` show every held row with its id, sender, kind and age. The hub completes rows made entirely of superseded hub workflow notices or presence items, recording an ids-only reason and the hub actor. An owner task offer with its structured offer marker also settles when the recipient still owns the task, has reached in-progress, in-review or changes-requested state, and its own acceptance follows the last ownership change. A person's acceptance and an unread replacement-owner offer do not count; missing or older-than-4096-entry proof keeps the hold. This runs on hold entry, hub startup, recovery release and task changes, with an ids-only accepted-task reason and the hub actor; it is a disposition, never proof that the offer was read. Mixed messages, unaccepted offers, unmarked live reminders and unknown references remain held. A requested-stop hold still needs the person. A Claude session without channel support safely returns unseen pushes for `hub_inbox`; a thrown notification write remains uncertain.
199
217
 
200
218
  Claude channel/pull sessions, Codex and Pi may call `hub_delivery_retry { delivery_id }` for their own
201
219
  `needs_review` row once. Its tool results show held-row count, ids, senders,
@@ -783,13 +801,21 @@ A class can carry a completion check in `.agenthub/config.json`, for example
783
801
  `"checks": { "implement": "scripts/check.sh", "timeout_s": 600 }`. When the owner
784
802
  marks such a task done, the tool answers at once, the hub runs the command in the
785
803
  project root (one at a time; what it left in its process group is killed when it
786
- exits, at the timeout or on shutdown), and then either
804
+ exits, at the timeout or on shutdown, and a check killed at its timeout or on
805
+ shutdown settles within one second even if its exit is never reported), and then either
787
806
  sends the task to review with the command, exit code and output tail, or keeps it
788
807
  with its owner and tells it what failed. `checks` is machine-local (see Install
789
808
  and start): a committed file must not choose commands the hub runs. Keys that
790
809
  are not task classes are ignored with a log line. A hub stopped mid-check leaves
791
810
  the task in progress (`check interrupted`); the owner marks it done again.
792
811
 
812
+ While the project root checkout is on a branch other than its default
813
+ (`origin/HEAD`, else `main`), finishing a task adds `the main checkout is on
814
+ <branch>; move the work to a worktree under .worktrees/` to the owner's result
815
+ and the reviewer's request, and `ahub status` shows it as `main checkout`
816
+ (#572). It is advice only. A branch name that fails the hub's public-text screen
817
+ is withheld from the sentence.
818
+
793
819
  A task reaching `approved` is a board transition. It is not proof that an
794
820
  external side effect happened unless the task's refs and a live readback show
795
821
  that effect. After `tasks.escalate_after` consecutive `changes_requested` verdicts
@@ -832,7 +858,12 @@ open tasks on the same paths or symbols get a message with the changed files,
832
858
  the plan's signatures and the first line of the summary, each left out when it
833
859
  matches a PII pattern (the summary also when `signals.pii_screen = "local"` did
834
860
  not clear it); nobody else does. PII
835
- tasks are left out on both sides.
861
+ tasks are left out on both sides. Overlap is compared only between tasks in the
862
+ same repository (#476): a task's repository is its `refs.repo` (an `owner/name`
863
+ slug or a checkout path, accepted by `hub_task_propose` and `hub_task_accept`),
864
+ else the common git top-level (or common parent) of its absolute paths outside
865
+ the project, else the project. A board with one repository behaves as before. `refs.repo` is metadata
866
+ only: it grants no file access and changes no execution root.
836
867
 
837
868
  A task can wait for others: `hub_task_propose` takes `after: [ids]` (`ahub task
838
869
  propose ... --after <id>`). Until every one of them is approved, the task is
@@ -858,7 +889,10 @@ reservation was passed over and why, and the assignment's history note keeps
858
889
  it, unless it is no news (the reserved peer is the owner the task moves away
859
890
  from, or it already refused the task). A peer the task was declined for (by
860
891
  itself or by the console) or escalated away from after repeated
861
- changes_requested or by hand stays excluded on every later reroute; an owner
892
+ changes_requested or by hand stays excluded on every later reroute. A decline
893
+ with `capacity: true` (`hub_task_decline`, "no room right now") is the
894
+ exception: it skips that owner for this move only, and a later reroute or
895
+ planner pull may offer the task to it again (#531). An owner
862
896
  released as gone, or moved by the hub after a failed delivery or inference, is
863
897
  excluded only from that move and may get the task again from its reservation
864
898
  later. Your own `ahub task assign` is not blocked by those exclusions, and it
@@ -979,7 +1013,7 @@ Run `ahub memory profile-check [--json]` from the person's terminal for the read
979
1013
 
980
1014
  Before real migration, first upgrade every hub sharing the home to 0.12.30 or later. Obtain positive ready or unbound/quiesced receipts in every relevant registry; then quiesce consumers and close bound handles before changing the selector. A matching ready handle proves compatibility but is still an open handle. Verify synthetic cross-writes before the separately approved real migration, then reopen through the supported session-preserving lifecycle afterward. Merely installing this release does not authorize migration or provide live capture/device-sync evidence.
981
1015
 
982
- A curated split (`amem migrate --shards`) writes `M/shards.json`; from 0.12.37 the hub then opens this project's shard, with the user shard as its overlay, under the alias-resolved project key the `amem` CLI binds (#534, #537). Before splitting, upgrade or stop every hub using the home: a hub on 0.12.36 or earlier does not detect the split and keeps writing the old layout, which reaches the shards only through `amem migrate --shards --resume`. A split that is not active (captured, malformed, or a dangling or unreadable `shards.json`) makes native memory unavailable with that resume command as the remedy. An `amem alias add` that remaps a running hub's key latches its memory unavailable until a restart. `ahub memory profile-check` is not split-aware yet: on a split home it reports a mismatch for every correctly bound hub (#536). See [the shard binding](memory.md#selected-memory-profile).
1016
+ A curated split (`amem migrate --shards`) writes `M/shards.json`; from 0.12.37 the hub then opens this project's shard, with the user shard as its overlay, under the alias-resolved project key the `amem` CLI binds (#534, #537). Before splitting, upgrade or stop every hub using the home: a hub on 0.12.36 or earlier does not detect the split and keeps writing the old layout, which reaches the shards only through `amem migrate --shards --resume`. A split that is not active (captured, malformed, or a dangling or unreadable `shards.json`) makes native memory unavailable with that resume command as the remedy. An `amem alias add` that remaps a running hub's key latches its memory unavailable until a restart. `ahub memory profile-check` is split-aware: each running hub is compared with the shard profile of its own project, and an inactive split reports that resume command (#536). See [the shard binding](memory.md#selected-memory-profile).
983
1017
 
984
1018
  Managed agents read through `hub_memory_search`, `hub_memory_show` and `hub_memory_graph`, with the server's project identity, the `memory_read` capability and current privacy screening. They cannot supply another project; private turns are refused. The tools bound UTF-8 input/output, screen before counts and truncation, and exclude their arguments/results from hub-managed capture. `hub_memory_correct` has its own `memory_correct` capability and records a screened correction candidate, not a note change. See [the complete memory contract](memory.md) for caps and refusal semantics.
985
1019
 
@@ -1512,7 +1546,7 @@ A guarded file edit that the daemon keeps at its accepted value remains visible
1512
1546
  watcher cannot adopt unrelated raising local edits. A settings session cannot
1513
1547
  adopt routing fields outside its editable registry. Existing confirmation and
1514
1548
  roster lifecycle fences remain; use the confirmed roster setting for membership.
1515
- Pi relay model aliases (`pi.dgx_coding`, `pi.dgx_fast`) and `routing.pi` retain their running values until restart, because existing native sessions use the published relay catalogue and context contract. Routing policy changes apply to future decisions; local model/target changes apply at the next peer or sidecar creation.
1549
+ Pi relay model aliases (`pi.dgx_coding`, `pi.dgx_fast`) and `routing.pi` retain their running values until restart, because existing native sessions use the published relay catalogue and context contract. Routing policy changes apply to future decisions; local model changes apply at the next peer creation.
1516
1550
 
1517
1551
  - Runtime/client ownership still requires restart for memory handles and sync,
1518
1552
  gateway/MLX, runner/check commands, profile/tool contracts, converters, existing
@@ -1794,6 +1828,27 @@ but cannot show pushes, so it attaches tools-only (protocol 16, issue #205):
1794
1828
  started while an `ahub claude` session holds the peer stands by and attaches
1795
1829
  only after that session leaves.
1796
1830
 
1831
+ When a hub reconnect changes the hub tools (names or schemas), the plugin asks
1832
+ Claude Code to list them again (`tools/list_changed`), so a session that listed
1833
+ them while the hub was down does not keep the short list (#490). A status probe
1834
+ that is unavailable right after a restart is retried, then the list is
1835
+ announced anyway.
1836
+
1837
+ ### Replaced or ended Claude sessions (#551)
1838
+
1839
+ A replaced session says it is standing by. Its reconnect requests cannot reclaim
1840
+ an attached or claiming peer, even if its status-file read missed that holder. A
1841
+ channel-capable session may still replace a pull-only holder once its claim has
1842
+ finished. Starting a new session intentionally keeps the newest-hello behavior.
1843
+
1844
+ A plugin carrying an older launch id than this daemon's current per-peer Claude
1845
+ launch is refused with close 4430. It stops reconnecting, and hub tools report that
1846
+ the launch ended. Start a new session with `ahub claude`, or use a plain session
1847
+ with the tools and `hub_inbox`. A session with no launch id, or a hub with no
1848
+ current launch record, keeps the existing behavior. The marker alone cannot
1849
+ prove that a nested host loaded this specific MCP server as a channel; the
1850
+ bridge currently receives no positive per-server activation signal.
1851
+
1797
1852
  ### Claude channel sessions while busy (#493)
1798
1853
 
1799
1854
  A Claude session started with `ahub claude` receives pushes when it is idle. While it is busy:
@@ -1802,7 +1857,7 @@ A Claude session started with `ahub claude` receives pushes when it is idle. Whi
1802
1857
  - `hub_inbox` reads the daemon queue (and any local failed-push buffer) in a channel session too. A read is one `completed` delivery, like a tools-only read; holds refuse it the same way.
1803
1858
  - An important message is pushed into the running turn (steered) instead of waiting for the turn to end. Mid-turn reading was verified on Claude Code 2.1.302; an older client that drops it leaves the delivery `accepted` until a person resolves it. Ordinary messages still wait for the idle push or `hub_inbox`.
1804
1859
  - Do not use `[FYI]` for PR ready, done, submitted, ruling-needed, blocker or changed-contract messages: they need action. `hub_task_done` and `hub_review` already notify their recipients.
1805
- - Turn completion is read from the transcript's last 4 MiB, whatever its size. When no completed turn is found there, the console says once per turn that the peer stays busy and its messages wait in its queue (#517).
1860
+ - Turn completion is read backwards in 4 MiB chunks, up to 64 MiB plus one row-boundary byte, whatever the transcript's size. A row crossing chunks is reconstructed whole; a row starting outside the bound stays unavailable. When no completed turn is found within that bound, the console says once per turn that the peer stays busy and its messages wait in its queue (#517).
1806
1861
 
1807
1862
  ## Graceful shutdown
1808
1863
 
@@ -1989,21 +2044,21 @@ section is what they run.
1989
2044
 
1990
2045
  Upgrade running projects with the target release's own coordinator. It accepts
1991
2046
  a running source on control protocol 9 (0.6.x), 10 (0.7.0 through 0.12.0),
1992
- 11 (0.12.1 and 0.12.2), 12 (0.12.3), 13 (0.12.4 through 0.12.15), 14 (0.12.16), 15 (0.12.17 through 0.12.19) or 16 (0.12.20 through 0.12.39), and only
2047
+ 11 (0.12.1 and 0.12.2), 12 (0.12.3), 13 (0.12.4 through 0.12.15), 14 (0.12.16), 15 (0.12.17 through 0.12.19) or 16 (0.12.20 through 0.12.41), and only
1993
2048
  a target on its own protocol, so the target's coordinator fits every supported
1994
2049
  source and carries every recovery fix released up to it. Protocol 8 and older
1995
2050
  (0.5.x and earlier) are refused as `manual-bootstrap-required`. Run from the
1996
2051
  project directory, without replacing the global CLI first:
1997
2052
 
1998
2053
  ```bash
1999
- bunx --package @staix/agent-hub@0.12.39 ahub upgrade --to 0.12.39 --dry-run
2000
- bunx --package @staix/agent-hub@0.12.39 ahub upgrade --to 0.12.39 --yes
2054
+ bunx --package @staix/agent-hub@0.12.41 ahub upgrade --to 0.12.41 --dry-run
2055
+ bunx --package @staix/agent-hub@0.12.41 ahub upgrade --to 0.12.41 --yes
2001
2056
  ```
2002
2057
 
2003
2058
  | Running now | Coordinator to use |
2004
2059
  | --- | --- |
2005
2060
  | 0.6.x (protocol 9) | the target's, through `bunx` as above |
2006
- | 0.7.0 through 0.12.0 (protocol 10), 0.12.1 and 0.12.2 (protocol 11), 0.12.3 (protocol 12), 0.12.4 through 0.12.15 (protocol 13), 0.12.16 (protocol 14), 0.12.17 through 0.12.19 (protocol 15), 0.12.20 through 0.12.39 (protocol 16) | the target's, through `bunx` as above |
2061
+ | 0.7.0 through 0.12.0 (protocol 10), 0.12.1 and 0.12.2 (protocol 11), 0.12.3 (protocol 12), 0.12.4 through 0.12.15 (protocol 13), 0.12.16 (protocol 14), 0.12.17 through 0.12.19 (protocol 15), 0.12.20 through 0.12.41 (protocol 16) | the target's, through `bunx` as above |
2007
2062
  | any supported source, with the installed CLI already at the target | `ahub upgrade` below, which is the same coordinator |
2008
2063
  | 0.5.x or earlier (protocol 8 and older) | not supported: bootstrap by hand with the matching CLI |
2009
2064
 
@@ -2035,14 +2090,14 @@ projects first:
2035
2090
 
2036
2091
  ```bash
2037
2092
  ahub restart --dry-run
2038
- ahub upgrade --to 0.12.39 --dry-run
2093
+ ahub upgrade --to 0.12.41 --dry-run
2039
2094
  ```
2040
2095
 
2041
2096
  Apply only after reviewing the plan:
2042
2097
 
2043
2098
  ```bash
2044
2099
  ahub restart --yes
2045
- ahub upgrade --to 0.12.39 --yes
2100
+ ahub upgrade --to 0.12.41 --yes
2046
2101
  ahub recovery status [<operation-id>]
2047
2102
  ahub recovery resume [<operation-id>]
2048
2103
  ahub recovery abort [<operation-id>]
@@ -2282,6 +2337,35 @@ Ending a peer's running turn is not part of these screens: finish or interrupt
2282
2337
  it in that peer's terminal, answer approvals with `ahub permit` or the console,
2283
2338
  or cancel and upgrade later.
2284
2339
 
2340
+ Terminal recovery records a launch only after that native session owns the peer
2341
+ (#482). Capturing an Orca terminal or receiving `start: ok` creates only a pending
2342
+ candidate; a second launcher that exits or is refused cannot replace the live
2343
+ owner's row. A running hub without `terminal_owner` support leaves the new launch
2344
+ unrecorded and prints an upgrade remedy. The plan names an attached native TUI
2345
+ without a live launcher or exact Orca session proof as `attached, launcher
2346
+ unrecorded`: end it in its terminal or use its native resume flow in an Orca
2347
+ terminal, then refresh the plan. Plain Claude's same-protocol reconnect-only
2348
+ session still reconnects by itself without a terminal restart.
2349
+
2350
+ Background Orca identity reads are limited to 30 seconds and 1 MiB of output.
2351
+ Shutdown cancels their owned query process groups and waits for reconciliation.
2352
+ A query whose termination cannot be confirmed is logged as incomplete; the
2353
+ remaining peer and daemon teardown still runs. Native terminal execution keeps
2354
+ its foreground TTY behavior. Unknown terminal evidence is shown as unverified
2355
+ ownership, with end or native resume guidance.
2356
+
2357
+ An Orca pane that is orphaned but still connected needs reattachment, not turn
2358
+ cancellation: run `orca terminal switch --terminal <handle>` yourself, then refresh.
2359
+ The coordinator never switches panes. Such a pane is not evidence that its native
2360
+ process ended and never authorizes a duplicate restore. Disconnected terminals
2361
+ keep their existing guidance.
2362
+
2363
+ The ledger retains historical and invalid rows; it does not prune them on launch.
2364
+ Only this project, current daemon instance and peer can supply ownership evidence,
2365
+ and a successful publication replaces only its matching valid row. Old-instance
2366
+ rows cannot claim the current terminal. Older binaries that directly write
2367
+ `terminal-recovery.json` are outside the new writer guarantee.
2368
+
2285
2369
  ### A partial operation
2286
2370
 
2287
2371
  `ahub recovery status <operation-id>` reads the receipt without changing it:
@@ -2610,10 +2694,38 @@ config with `provider: "legacy"`; Ollama errors never launch Python.
2610
2694
 
2611
2695
  ### In-process model routes
2612
2696
 
2613
- `routing.toml` supports `[hub_routes."hub/<id>"]`, separate from sidecar `[routes]`.
2614
- Set `[local] route = "hub/stage"` or a task class's `route` to enable a hub route.
2615
- The shipped examples use OmniRoute `fast` and `coding`; `fixed_model` remains the fallback.
2616
- `--model` pins the worker and bypasses both routing engines. Sidecar `sy/` routes remain optional.
2697
+ `routing.toml` supports in-process `[hub_routes."hub/<id>"]`.
2698
+ A task uses a hub route when `[local] route` or its class's `route` names one.
2699
+ A new `routing.toml` from `ahub init` sets `[local]` and `implement` to `hub/stage`
2700
+ (fixed model `coding` as the fallback) and gives the lighter classes
2701
+ `fixed_model = "fast"`. The shipped examples use OmniRoute `fast` and `coding`; `fixed_model` remains the fallback.
2702
+ `--model` pins the worker and bypasses route selection. A class with `fixed_model`
2703
+ and no `route` also calls that alias directly, even when `[local]` names a route.
2704
+
2705
+ The Switchyard sidecar is retired (#553). No binary or generated model-router
2706
+ config is used, and its reserved port offset remains unused. Existing `[targets]`
2707
+ and `[routes]` tables are ignored and listed as retired settings in startup logs
2708
+ and doctor. Every route without the `hub/` prefix, including named legacy routes
2709
+ and `sy/` selections, uses the fixed alias with one warning. Loading or reloading
2710
+ never rewrites the operator's routing files.
2711
+
2712
+ Migrate the retired settings as follows:
2713
+
2714
+ | Retired setting or route | In-process replacement |
2715
+ | --- | --- |
2716
+ | `stage_router` | `[hub_routes."hub/<id>"]` with `type = "stage"`; select it with `route = "hub/<id>"` |
2717
+ | `efficient_target` / `capable_target` | Put the targets' model aliases in `efficient` / `capable` |
2718
+ | `confidence_threshold` | Keep the same value on the hub stage route |
2719
+ | Passthrough class route | Put its target model alias in the class's `fixed_model` and remove the class's `route` |
2720
+ | Former unshipped classifier escalation | Use the existing hub `escalation` policy |
2721
+ | `[targets]` / `[routes]` | Remove these retired tables after migrating their selections |
2722
+
2723
+ Doctor inventories all local/class fixed aliases and effective efficient/capable/judge aliases, reporting
2724
+ each missing alias once or aggregate success when all are served. A
2725
+ `routing.toml` or `routing.local.toml` that does not parse gets its own `routing`
2726
+ row naming the file and the parse error, without the file's contents, and the
2727
+ alias check is skipped (#563). Hub summaries
2728
+ and triage use their fixed model directly.
2617
2729
 
2618
2730
  Route types: `stage` scores recent tools and holds capable recovery for two calls;
2619
2731
  `plan_execute` plans on capable until the first mutation and then stays efficient;
@@ -3104,9 +3216,15 @@ Qwen controlled recovery and automatic crash resume load the recorded conversati
3104
3216
 
3105
3217
  Use `ahub roster add jcode`, then `ahub jcode [--model <alias>] [--provider-profile <name>]`. It opens the native TUI attached to the hub-owned ACP session by default. Use `--mode headless` or `ahub settings set peers.jcode.start_mode headless` to keep headless startup. The launcher reads roster membership from the hub's status reply; a hub older than 0.12.35 does not send it, so the launcher stops with an upgrade-or-restart remedy and starts nothing (#484). The person launcher configures only agent-hub-jcode in .jcode/mcp.json; run ahub init afterwards to ignore the untracked machine-path file. It writes the plugin bundle the running hub reports in `status`, so a CLI from another install no longer leaves a stale entry; `ahub kimi` does the same for Kimi's TUI entry (#502). Conductor/dashboard starts read that entry without editing it. Preview starts nothing, and ahub stop jcode stops only the owned private daemon/ACP groups. jcode_cmd, jcode_home and jcode_provider_profile are machine-local and never editable from settings.
3106
3218
 
3107
- The native TUI opens only after Jcode has saved the selected session. On 0.98.1, `session/new` does not save it; the first real hub turn does. The launcher prints `jcode's TUI opens after its first hub turn; send it work, or Ctrl-C` once and waits for the same owned launch's saved session. Send Jcode work from the hub, or press Ctrl-C to end only the wait. No artificial prompt is sent. A console/conductor start uses a terminal provider to run the same launcher. Without a provider, run its displayed command in your own terminal. Options with no terminal need that manual path.
3219
+ The native TUI opens only after Jcode has saved the selected session. On 0.98.1, `session/new` does not save it; the first real hub turn does. The launcher prints `jcode's TUI opens after its first hub turn; send it work, or Ctrl-C` once and waits for the same owned launch's saved session. Send Jcode work from the hub, or press Ctrl-C to end only the wait. No artificial prompt is sent. A console `peer_start` uses a terminal provider to run the same launcher, even while ACP is running; a conductor or dashboard start of an attached Jcode answers `already` and opens nothing (#559). Without a provider, run its displayed command in your own terminal. Options with no terminal need that manual path.
3220
+
3221
+ `ahub jcode --dry-run` stays pure and redacted. A person's `--print-command` can show the actual top-level native argv and closed native environment for an already-saved running session; it starts nothing, and like the launch it names a `--model`, `--provider-profile` or `--session-id` that differs from the running session (#574). The private socket/session tuple is not returned to hub tools or status. Native saved-session evidence is read-only and capped at 32 MiB; unsafe, mismatched or larger evidence refuses the TUI launch.
3222
+
3223
+ A busy jcode takes an `important` message into its running turn (#572): the hub sends it as a soft interrupt through jcode's own private server socket, which jcode injects at its next tool boundary, so the message no longer waits for the turn to end. The hub never cancels a turn to deliver. Headless Kimi and Qwen have no such path: an `important` message waits, and `ahub status` and the console show `important messages waiting on a busy ACP peer for N min` once it has waited past `budget.stale_min`, so you can decide to stop the peer.
3224
+
3225
+ jcode has no switch to choose which MCP servers its `serve` and `acp` load, so the hub-owned jcode starts every server named in `<JCODE_HOME>/mcp.json` and the project's `.jcode/mcp.json`, `.mcp.json` and `.claude/mcp.json`, besides the hub's own (#572). `ahub doctor` lists their names in its `jcode MCP servers` row (names only, from those four files only). To keep a server out, remove it from those files, or point `jcode_home` at a separate home.
3108
3226
 
3109
- `ahub jcode --dry-run` stays pure and redacted. A person's `--print-command` can show the actual top-level native argv and closed native environment for an already-saved running session; it starts nothing. The private socket/session tuple is not returned to hub tools or status. Native saved-session evidence is read-only and capped at 32 MiB; unsafe, mismatched or larger evidence refuses the TUI launch.
3227
+ Known ceiling (#559): jcode 0.98.1's top-level TUI spawns a shared server, without `--temporary-server` or `--owner-pid`, when nothing answers on the dialed socket at startup or on its reload-handoff reconnect, and no native flag prevents it. A hub stop or an owned-server crash between the launcher's confirm and the TUI's check can leave an unowned jcode server on the private socket under the person's `JCODE_HOME`; stop it by hand. Revisit if upstream adds an owner flag. An option that differs from the running session (`--model`, `--provider-profile`, `--session-id`) is refused by name.
3110
3228
 
3111
3229
  TUI input may interleave with an ACP turn; there is no single-writer guard. Closing the TUI leaves the owned ACP session running, and native policy remains unmanaged. The isolated 0.98.1 transport receipt is [recorded separately](verification/2026-10-20-jcode-tui-0.98.1.md); it is not personal account or real provider verification.
3112
3230
 
@@ -3410,10 +3528,16 @@ built-ins and off for the current console only. `?` lists this under Pet, even
3410
3528
  while drawing is off. The command prompt keeps its original row and width.
3411
3529
 
3412
3530
  `pet.glyphs` selects auto/ascii/blocks; unsupported locales and dumb/linux
3413
- terminals retain the ASCII floor. `pet.blink:false` is static. Neither choosing
3414
- nor drawing a character changes authority, config files, task state or polling.
3415
- Explicit `ahub pet` and text status use the configured line face; prompt tokens
3416
- remain character-independent. Native glyph width and tmux are unverified.
3531
+ terminals retain the ASCII floor. `pet.blink:false` is static. `pet.palette`
3532
+ selects blue, teal (default), violet or ochre (`a`/`b`/`c`/`d`), and `pet.window`
3533
+ controls standalone window opt-in; both `pet` (`false` or an option block) and its
3534
+ fields validate identically at startup and reload in `.agenthub/config.json` and
3535
+ `.agenthub/config.local.json`, apply live as console-local presentation (including
3536
+ an open console's next refresh and the standalone pet window), and grant no
3537
+ authority (#483). Neither
3538
+ choosing nor drawing a character changes authority, config files, task state or
3539
+ polling. Explicit `ahub pet` and text status use the configured line face; prompt
3540
+ tokens remain character-independent. Native glyph width and tmux are unverified.
3417
3541
 
3418
3542
  ## Dashboard views and attention (#391, replaces the #280 in-page character)
3419
3543
 
@@ -3466,7 +3590,7 @@ a fresh peer status read on the same authenticated connection.
3466
3590
 
3467
3591
  Integer WIP editors save a value, including zero and inheritance; it applies at the next admission. GitHub intake accepts dot-prefixed repository names within the existing bounded owner/repository identity. Split undo also refuses children referenced by another task's persisted phase checklist.
3468
3592
 
3469
- Declining rework returns changes_requested to To Do before rerouting. When a planner-pulled task has no eligible destination capacity, it remains ownerless in To Do and is retried after capacity clears, preserving the declined-owner exclusion.
3593
+ Declining rework returns changes_requested to To Do before rerouting. When a planner-pulled task has no eligible destination capacity, it remains ownerless in To Do and is retried after capacity clears, preserving the declined-owner exclusion. A planner pull that finds no owner names the cause by peer id: a lasting exclusion, WIP full (with wip/limit), a held queue or needs_review deliveries; a capacity-only decline is not an exclusion (#531).
3470
3594
 
3471
3595
  With no release cut, outside approved tasks are reported under unknown cut evidence, including tasks with a known approval time. A child task execution budget also prevents split undo until the person disables that budget.
3472
3596
 
@@ -17,7 +17,7 @@ ahub setup # installs the Claude Code channel plu
17
17
  Or use the matching GitHub release:
18
18
 
19
19
  ```bash
20
- bun add -g github:STAIxBWLB/agent-hub#v0.12.39
20
+ bun add -g github:STAIxBWLB/agent-hub#v0.12.41
21
21
  ahub setup
22
22
  ```
23
23
 
@@ -30,9 +30,27 @@ and upgrade or crash recovery, see the [operations guide](operations.md). To
30
30
  move running projects to a newer release later, run `ahub upgrade` in a
31
31
  terminal: it reviews the plan with you, applies it and follows it.
32
32
 
33
- ## Upgrading to 0.12.39
33
+ ## Upgrading to 0.12.41
34
+
35
+ 0.12.41 fixes one 0.12.40 regression: `ahub codex` in a recorded Orca terminal stopped at `invalid remote address`, because the launcher put the terminal launch id into the `--remote` URL, which Codex accepts only as `ws://host:port`. The id now travels as the upgrade's bearer token (#586). Coming from 0.12.39 or older, the 0.12.40 notes below apply too.
36
+
37
+ ## Upgrading to 0.12.40
38
+
39
+ Restart Claude sessions after upgrading: each keeps the plugin server it started with until restarted, so a restart loads the 0.12.41 plugin. That plugin asks Claude Code to list the hub tools again when a hub reconnect changes them (#490), and one from an older `ahub claude` launch is refused and stops reconnecting instead of reclaiming the peer (#551).
40
+
41
+ `ahub jcode` opens its TUI again; on 0.12.39 it stopped after the first hub turn (#564). A busy Jcode now takes an `important` message into its running turn (#572), and `ahub doctor` lists the extra MCP servers the hub-owned Jcode loads from its config files. See [Jcode operations](operations.md#optional-jcode-peer).
42
+
43
+ The Switchyard sidecar is retired (#553). An existing `routing.toml` keeps working: its `[targets]` and `[routes]` tables are ignored and reported as retired settings, and a route without the `hub/` prefix (such as `sy/coding`) uses the fixed model with one warning. Migrate with the table in [in-process model routes](operations.md#in-process-model-routes); the hub never rewrites the file.
34
44
 
35
- Restart Claude sessions after upgrading: each keeps the plugin server it started with until restarted, so a restart loads the 0.12.39 plugin.
45
+ Run `ahub init` once per project to refresh the hub block in `AGENTS.md`: it adds the line that tells agents to report a pushed PR with `hub_task_done` and end the turn instead of waiting for CI (#572). Finishing a task now also says when the project root checkout is on a branch other than its default.
46
+
47
+ `hub_task_decline` takes `capacity: true` for "no room right now", which does not exclude the owner for good (#531). Task overlap compares only tasks in the same repository; set `refs.repo` on `hub_task_propose` or `hub_task_accept` for work in another repository (#476). Pi and the local worker report a reply stopped by the hop cap as not delivered (#550).
48
+
49
+ Live-test delegated CLI commands and terminal-recovery launch records need the upgraded daemon (`cli_audit_delegated`, `terminal_owner`): against an older running hub the first refuses with an upgrade remedy and the second leaves the launch unrecorded, so upgrade running hubs with `ahub upgrade` (#410, #482).
50
+
51
+ Upgrading from 0.12.38 or earlier, the 0.12.39 notes below also apply.
52
+
53
+ ## Upgrading to 0.12.39
36
54
 
37
55
  `ahub jcode` now opens Jcode's native TUI on the hub-owned ACP session by default (#464). The TUI opens after Jcode's first hub turn saves the session; until then the launcher prints one waiting line, and Ctrl-C ends only the wait. Hub-made starts (the dashboard's Start control, the conductor's `hub_peer_start`) follow the start mode too, so they open a terminal for Jcode through a terminal provider. To keep the old headless default, run `ahub settings set peers.jcode.start_mode headless`, or pass `--mode headless`. See [Jcode operations](operations.md#optional-jcode-peer).
38
56
 
@@ -52,7 +70,7 @@ The restarted Claude planner gets the release-train tools (`hub_release_new`, `h
52
70
 
53
71
  Jcode must be 0.98.0 or later, below 1.0.0; 0.94.0 is no longer accepted, so `ahub doctor` fails its Jcode row for an older install and the peer does not start (#529). Every owned jcode process runs with `JCODE_NO_TELEMETRY=1`.
54
72
 
55
- Since 0.12.37 the hub pins `@staix/agent-mem` 0.4.0 and opens a split memory home through its shards (#534). Before anyone runs `amem migrate --shards` on a memory home, every hub using that home must run 0.12.37 or later: an older hub does not see the split and keeps writing the old layout. `ahub memory profile-check` is not split-aware yet and reports a mismatch for every correctly bound hub on a split home (#536). See [memory profiles and rollout](memory.md#selected-memory-profile).
73
+ Since 0.12.37 the hub pins `@staix/agent-mem` 0.4.0 and opens a split memory home through its shards (#534). Before anyone runs `amem migrate --shards` on a memory home, every hub using that home must run 0.12.37 or later: an older hub does not see the split and keeps writing the old layout. `ahub memory profile-check` is split-aware and compares each hub with its own project's shard (#536). See [memory profiles and rollout](memory.md#selected-memory-profile).
56
74
 
57
75
  From 0.12.30 or earlier, existing Jcode project MCP entries need one refresh before conductor or dashboard starts. Once per project, the person runs:
58
76
 
@@ -94,7 +112,7 @@ ahub say @kimi "run the tests and report" # one peer
94
112
  { "omniroute": { "urls": ["http://your-gateway:20128/v1"], "api_key_file": "/path/to/key" } }
95
113
  ```
96
114
 
97
- or set `OMNIROUTE_API_KEY`. Name the model in `.agenthub/routing.toml` (`[local] fixed_model`). Then `ahub local`. It works only inside the project, asks before it writes or runs anything (`ahub permit <id> allow`) unless you chose a permission mode for it (`ahub permission`), and everything it executes is sandboxed. Keep `ahub console` open while it works: a request nobody answers expires, and after two in a row its turn ends.
115
+ or set `OMNIROUTE_API_KEY`. The `.agenthub/routing.toml` that `ahub init` writes asks the gateway for the model aliases `fast` and `coding` (the in-process `hub/stage` route, with `[local] fixed_model` as its fallback); serve those aliases or name yours there, and `ahub doctor` lists any alias the gateway does not serve. Then `ahub local`. It works only inside the project, asks before it writes or runs anything (`ahub permit <id> allow`) unless you chose a permission mode for it (`ahub permission`), and everything it executes is sandboxed. Keep `ahub console` open while it works: a request nobody answers expires, and after two in a row its turn ends.
98
116
 
99
117
  ## Commands of a working day
100
118
 
package/docs/security.md CHANGED
@@ -5,7 +5,7 @@ agent-hub connects agents that can each run commands. This page says what the hu
5
5
  ## Trust boundaries
6
6
 
7
7
  - **Other agents' text is untrusted.** Every message that crosses from one peer to another is framed as untrusted input: a channel tag with `meta.source` for Claude started with `ahub claude`; otherwise a fixed header line (`frame()`, `[agent-hub message from ...]`) with an instruction that says it is untrusted, the plugin's server instructions for a tools-only Claude reading `hub_inbox` and a standing instruction for the others. A message body cannot forge the hub's own headers: such lines are quoted (`sanitize`). Replies inherit a hop count capped at 3, so agents cannot ping-pong forever; neither a digest nor a steer can reset it.
8
- - **The control link is loopback plus a secret.** The daemon and the Codex proxy bind 127.0.0.1 only. The control WebSocket requires a per-run token (`.agenthub/state/control-token`, mode 600), and both servers refuse any request that carries an `Origin` header: any web page can open a WebSocket to localhost, and browsers always send `Origin`. External clients cannot claim the console user's id or a hub-managed peer's id.
8
+ - **The control link is loopback plus a secret.** The daemon and the Codex proxy bind 127.0.0.1 only. The control WebSocket requires a per-run token (`.agenthub/state/control-token`, mode 600), and both servers refuse any request that carries an `Origin` header: any web page can open a WebSocket to localhost, and browsers always send `Origin`. The bearer header a Codex TUI sends to the proxy carries only the terminal launch id (#586): it binds terminal ownership and is not a credential. External clients cannot claim the console user's id or a hub-managed peer's id.
9
9
  - **Permission prompts stay on unless an operator selects a permission mode.** `ask` leaves native decisions unchanged. `ask-when-needed` grants scoped Claude file tools or Pi/local writes and edits; Kimi and Codex use their own native policies. `never-ask` requires explicit runtime confirmation or a person's console `y` for a config default at hub start. Until that config confirmation the peer runs in `ask`. Kimi's and `local`'s permission requests are relayed to the console and cancelled after `approvals.timeout_s` (default 120 s) of silence; the macOS notification for a waiting request carries the peer and the tool name only. The one exception is the hub's own tools (`hub_send` and the task tools, matched by exact name): Kimi's requests for them are approved once without a prompt and logged by name, the same trust Codex gets through `approval_mode` in the hub's config. They invoke hub-owned operations rather than arbitrary file or shell tools, and every call passes the hub's own checks. Identity comes from the exact permission title, or from the earlier tool-call title bound to the same call id and resolved against the configured MCP servers when the permission title contains argument JSON (Qwen). Payload text and unrelated display titles never establish identity. For ACP agents, a payload longer than the console shows is marked as cut and never offers a session-wide grant. Pi's write, edit, shell and git write requests are relayed the same way and can be answered `Always allow <tool> until Pi restarts`: later calls of that tool by the same Pi start skip the prompt but stay inside the path guard and sandbox. The grant covers every later call of that tool whatever its payload, is kept in memory only, cannot be given from the dashboard, and each call it allows is logged by tool name. A Pi or `local` request whose call was cancelled or whose turn ended is withdrawn at once: a later answer is refused and grants nothing, an `Always allow` included. One shell command runs without a hub approval: the `!command` a person types in the managed Pi TUI. It stays inside the same sandbox and denylist, and the hub takes it only from the verified owner process of a Pi in TUI mode, while Pi is idle, on a reservation that is used once; a model's own bash call never takes that path (issue #253). `--unattended` turns prompts off, says so loudly, and is never the default.
10
10
  - **A committed config cannot choose launch commands, credential files, data endpoints, permission modes or a wider sandbox.** The machine-local fields (`kimi_cmd`, `permission_modes`, `codex_bin`, `pi.cmd`, `checks`, `mlx.bin`, `mlx.runtimeDir`, `mlx.modelPath`, `omniroute.urls`, `omniroute.access_hosts`, the `omniroute` key files, `memory.worker_url`, `local.read_allow`, `local.bash_network`, `local.network_allow`, `terminal.open`, `instances`) apply only from a config file git confirms nobody committed (`instances` is read only from an untracked `.agenthub/config.local.json`; in `config.json` it is ignored even when untracked): `.agenthub/config.json` or `.agenthub/config.local.json`, matched by file identity so no other spelling the file system accepts slips past, and `.agenthub` itself not a committed symlink or submodule. Without a repository, or when git fails, they keep their defaults; an empty value always means the default. "Untracked" is answered by the repository that contains the project: a checkout copied or extracted into an unrelated repository, or into an ignored directory of one, is trusted like your own files. So a cloned repository cannot choose a launch command, a completion check, a gateway to send a key file to, a memory endpoint, a permission mode, the command that opens a terminal, or a wider sandbox. A command in `checks` runs as you, outside the local worker's sandbox, like a git hook. So do the `setup` and `verify` commands of a benchmark suite (issue #251): `ahub bench run <suite.json>` is a person's command, refused from an agent session, and it resets the work tree for each attempt, so it runs only with `bench.enabled`, at the root of a clean repository that tracks nothing under `.agenthub/`. Nothing in `routing.toml` or in task text is ever run. `routing.toml` and the other shared fields still come from the checkout, and they matter: `routing.toml` picks the models the local worker and the hub's inference use at your gateway and can turn the PII constraint off, and roles and budget shape who does what. Review them in a repository you do not trust. The hub's own git calls outside the sandbox (snapshots, facts, config trust, project discovery, memory recall, the bench runner) all use the single hardened implementation in the exact-pinned first-party `@staix/agent-mem/platform` package, re-exported by `src/hub/git.ts`, which disables the programs a repository's configuration can name — the file-system monitor, hooks, external diff, the pager, and the credential and askpass helpers, with prompts failing closed — so a config that ever does get written cannot run code with the hub's rights. Of those, the monitor, hooks and external diff are reachable by commands the hub issues (the planted-program test exercises all three; a `diff --no-index` reads the repo's config, so that call also passes `--no-ext-diff` and `--no-textconv`); the pager is not (every call pipes stdout). Credential and askpass helpers are disabled for project repositories; memory sync alone may use credentials for its own memory repository, without prompts. The cost: `core.fsmonitor=false` also turns off a person's enabled built-in monitor for the hub's own calls, so the per-turn snapshot's `add -A` scans the whole tree (the snapshot has a 10 s cap). git has no global switch for filter drivers, and of the hub's commands only the snapshot's `add`/`restore` would run one (its driver command lives in the protected `.git/config`).
11
11
  - **Telemetry holds no bodies.** `.agenthub/state/events.jsonl` (issue #40) records envelope ids, routing and sizes, task ids and states, overlapping paths and token counts, and the task id (with a PII flag) that each usage and token record is attributed to. It never records a message body, a task title or detail, and marks private (PII) envelopes and tasks as such. It stays on the machine; `ahub export` only prints it. Two opt-in stores copy numbers from it into your home directory, shared by every project on the machine (directories mode 700, files 600): research records (issue #247, `research.enabled`) in `~/.agenthub/research/` hold per approved task the ids, states, counts, tokens, times and PII flag the events hold, a person's label from a closed list, and a hash in place of the project id; benchmark runs (issue #251) in `~/.agenthub/bench/` hold suite name and hash, arm label, the attached peers' ids, models and permission modes, task ids, outcomes, exit codes, counts, tokens and times, never suite text or a command's output.
@@ -18,7 +18,7 @@ agent-hub connects agents that can each run commands. This page says what the hu
18
18
  - **PII has an enforced path.** A task matching `signals.pii_patterns` goes to `local` or to nobody; its text is absent from other peers' envelopes, the console stream, the log and the board listing; the console user reviews it; `local` answers such a turn to the console only, keeps it out of its history, refuses it when the only gateway is off campus, and may not save notes or spin off tasks during it. Nothing about it is sent to claude-mem, whose observer is a cloud model.
19
19
  - **A model screen can add to the patterns (issue #198).** With `signals.pii_screen = "local"` in `routing.toml` (default `off`; it applies while `constraints.pii = "local_only"`), a newly proposed task is read by a model before the hub does anything else with it: until the verdict it is on no board, in no envelope, before no triage model, and the hub makes no brief or note call for it. The local worker's claude-mem capture leaves its hub tool calls out (issue #230), so a proposal it makes in an ordinary turn cannot reach claude-mem ahead of the verdict that way. The screen runs on the model on this machine (MLX or Ollama, loopback only) or, without one, on the gateway only while `onCampus()` holds, and the gateway call is refused once more right before transport if the gateway is behind Access; task text never goes off campus to be screened. When the on-device generation slot is taken (Pi generating, another screen), the screen goes to the campus gateway instead of waiting, and on campus a device that fails or is still loading after 60% of the deadline hands over to the gateway too; off campus it waits for the slot and the device within the deadline. With the legacy MLX provider the screen may start an `mlx_lm.server`, as Pi's relay does, and it stays running until `ahub models stop`. Its answer is `pii` or `clear` plus a category from a closed list (name, student id, phone, address, grade, health, other). Anything else, a timeout (8 s, one call and one generation slot per item), a missing or off-campus model, or text longer than 6000 UTF-8 bytes (about 2000 Hangul syllables) is `unknown`, and an unknown task is a PII task: this is the hub's one model call that fails closed, so with the screener down every new task waits for `local`. An unknown verdict has the costs of any PII task, even when a second look clears it later: opening it lifts every silent turn-free cohort for good and turns facts off while it is open, and off campus a proposal without a class is refused (triage is not asked for a PII task there), with an error that says so. Off campus with the on-device slot taken, a task stays `unknown` until it is screened again: while it is still proposed and no peer ever owned it, the hub's release timer (every 60 s, one task at a time, the least tried first, at most 10 times per task in a hub run; never text too long to read) asks again, and `clear` lifts its `pii` signal and routes it through the class peers (a named owner is not remembered; a reserved one is offered first, as always). A task `local` took keeps its PII path, also once it is declined back to nobody. A pattern match is PII without a call. The task's history records the source of its `pii` signal (`regex`, `screen` with the category, or `unknown` with the reason), an unknown verdict is a console line naming the task id only, and `events.jsonl` gets a `pii_screen` record with the item kind, label, source, category, reason and latency, never the text. A routing explanation of a draft does not call the screen and says so.
20
20
  - **Free text is screened too.** On an ordinary task, a done summary (with its check output: a match withholds the whole note), a review note, an unmet item or a budget handoff that matches a PII pattern (issue #69) is not saved to claude-mem, and every peer, `local` included, gets a stub naming `ahub task show <id>`; the board keeps the text, `hub.log` notes the withholding by task id only, and `ahub ask` shows such a note only when its model is reached on campus. `hub_remember` refuses a match; what a vendor agent writes to its own memory is not screened. With the model screen on, the same items (and `hub_remember` notes) go through it as well, one call each and only after the call's own checks (owner, state, verdict, a running check, memory enabled) pass: a `pii` or `unknown` verdict withholds the item exactly like a pattern match, and the board marks the history entry (`withheld`) so later views (task lists, completed-change notices, `ahub ask`, a reviewer replaced after a restart) keep withholding it. A budget hand-off is screened once per hand-off, whatever the number of tasks it moves; an escalation's reason is the hub's own text and is not screened. Pattern-only, without the model: the check output tail in a done note (only the summary is screened), a plan given with `hub_task_accept`, a decline reason, a task's refs (paths, branch), and context checkpoint summaries. Not screened at all (an existing gap, not new with the screen): the local worker's claude-mem capture records its file, shell and git tool calls in ordinary turns with their raw arguments and output, and the closing answer of a turn that used tools, without a pattern or model check. Its hub tool calls (`hub_send`, the task tools, the conductor tools) are not captured (issue #230), so a title, detail, done summary, review note or `hub_remember` text it writes reaches claude-mem only through the screened paths above.
21
- - **Secrets stay where they are read.** The gateway key and Cloudflare Access values are read inside the gateway client and go only into request headers: never into logs, errors, envelopes, tool output, memory, or the generated Switchyard config (the key travels by environment variable name). Subscription logins of Claude, Codex and Kimi are never proxied or pooled.
21
+ - **Secrets stay where they are read.** The gateway key and Cloudflare Access values are read inside the gateway client and go only into request headers: never into logs, errors, envelopes, tool output, memory. Subscription logins of Claude, Codex and Kimi are never proxied or pooled.
22
22
  - **The hub's own model calls are fenced.** Digest condensation, task triage and the PII screen read agent-written text as data; their output is capped text framed as untrusted, or a value checked against a closed list. It is never used as a route, a peer id, a tool call or an instruction.
23
23
 
24
24
  ## Native memory authority and profile selection
@@ -33,7 +33,7 @@ The hub binds the selected root/profile at open. Invalid/missing generation meta
33
33
 
34
34
  Before migrating any real home, every hub sharing it must first be on 0.12.30 or later with positive authenticated binding or unbound/quiesced proof from `ahub memory profile-check` in each relevant registry. An unknown or unreadable runtime is not proof of absence, and a matching ready binding still holds a handle that must be closed. Profile-check reports only its named registry inventory. Root paths, selector receipts and malformed-legacy counts are person-local diagnostics, excluded from ordinary peer status and allowed agent CLI reads. Source/fixture checks do not establish native capture, real migration or account/device-sync success.
35
35
 
36
- On a home split by `amem migrate --shards`, the hub opens only its own project's shard, with the user shard as overlay, under the alias-resolved project key (#534, #537); other projects' private notes are not in that store. Each memory entry rechecks the shard identity, and an alias that later remaps the key latches the binding unavailable. A split that is not active refuses and is never read as an empty or legacy store. A hub on 0.12.36 or earlier does not detect a split and keeps writing the old layout, so every hub on the home must run 0.12.37 or later, or be stopped, before splitting. `ahub memory profile-check` is not split-aware yet (#536): on a split home its mismatch reports are not evidence either way.
36
+ On a home split by `amem migrate --shards`, the hub opens only its own project's shard, with the user shard as overlay, under the alias-resolved project key (#534, #537); other projects' private notes are not in that store. Each memory entry rechecks the shard identity, and an alias that later remaps the key latches the binding unavailable. A split that is not active refuses and is never read as an empty or legacy store. A hub on 0.12.36 or earlier does not detect a split and keeps writing the old layout, so every hub on the home must run 0.12.37 or later, or be stopped, before splitting. `ahub memory profile-check` is split-aware (#536): it compares each running hub with its own project's shard and reports an inactive split with the resume remedy.
37
37
 
38
38
  ## What is not defended
39
39
 
@@ -95,9 +95,21 @@ the existing console authority for console-class operations and conductor-class
95
95
  settings, roster and lifecycle operations. It does not restrict effects to a
96
96
  scratch project: the agent must stay within the authorized commands and targets.
97
97
  Malformed or conflicting caller markers still refuse. The original peer and
98
- closed command label are recorded with outcome `delegated` before execution;
99
- if that audit cannot be written, the command refuses. The current daemon emits
100
- `agent_cli` with `delegated: true` and `refused: false`. Allowed-class commands retain the peer identity and capability checks, including
98
+ closed command label are audited before execution. Ordinary delegation requires
99
+ the authenticated welcome feature `cli_audit_delegated` before publishing that
100
+ outcome. The negotiated connection is reused, helper connections are pinned to
101
+ its authenticated instance, and local dispatch checks that instance. Replacement
102
+ control connections refuse. Before command effects, a console-only acknowledgement
103
+ must confirm the exact UUID was retained by a successful event append in that
104
+ instance; an unavailable acknowledgement refuses. Later local work therefore does
105
+ not depend on a future consumer draining the spool. An older or unavailable consumer gets a compatible `refused` row and the
106
+ command refuses with an upgrade remedy. An unwritable outbox refuses before the
107
+ connection; an event append failure refuses before command effects. Upgrade/recovery instead records compatible `run` spool rows and
108
+ keeps authoritative ids-only `delegatedAudits` in its operation receipt before
109
+ runtime effects, including resume through an older retained coordinator. Local bootstrap commands with no daemon manifest use `run` too. Stored attribution
110
+ never grants authority; the current invocation still needs its own validated peer
111
+ and flag. The current daemon emits `agent_cli` with `delegated: true` and
112
+ `refused: false` for the negotiated outcome. Allowed-class commands retain the peer identity and capability checks, including
101
113
  messages, proposals and reviews. Peer starts, pause and resume may delegate
102
114
  operational console authority; task assign/escalate under the flag refuse. `--as-user`, person task labels,
103
115
  person-authored planning/release/memory/goal writes, SDLC record, benchmark run,
@@ -240,7 +252,7 @@ never-ask still requires the person's confirmation. qwen_cmd is machine-local an
240
252
  never editable through settings. Legacy benchmark adapters remain unmanaged by
241
253
  this production table. Startup diagnostics withhold native host/payload details.
242
254
 
243
- Jcode is optional, source/version-fenced and runs native tools without a hub sandbox or hub permission-mode mapping. .jcode joins the scoped-grant exclusion, including lexical/resolved aliases. Only a person writes its unique project MCP entry; foreign/override/symlink entries refuse. The hub owns a private daemon/socket and ACP group, never the global daemon, and copies no credentials. It accepts only a plain jcode release from 0.98.0 up to, not including, 1.0.0 (#529), and runs every owned jcode process (probe, serve, ACP and TUI) with `JCODE_NO_TELEMETRY=1`, because jcode's telemetry can report the provider account's email. Native diagnostics withhold payloads/hosts. Unproved conversation loads refuse; no fresh replacement is called a restore. Native TUI launch exposes its private socket/session tuple only to the authenticated console, bound to the same live owned socket and session after read-only native persistence proof. Tools/status do not receive it. The TUI has no single-writer guard: person input may interleave with ACP. Native policy remains unmanaged and closing the frontend does not stop ACP.
255
+ Jcode is optional, source/version-fenced and runs native tools without a hub sandbox or hub permission-mode mapping. .jcode joins the scoped-grant exclusion, including lexical/resolved aliases. Only a person writes its unique project MCP entry; foreign/override/symlink entries refuse. The hub owns a private daemon/socket and ACP group, never the global daemon, and copies no credentials. It accepts only a plain jcode release from 0.98.0 up to, not including, 1.0.0 (#529), and runs every owned jcode process (probe, serve, ACP and TUI) with `JCODE_NO_TELEMETRY=1`, because jcode's telemetry can report the provider account's email. Native diagnostics withhold payloads/hosts. Unproved conversation loads refuse; no fresh replacement is called a restore. A finished task's advisory about the project root checkout names the branch only after the hub's whole public-text screen (`Tasks.publicMetadataText`: a PII pattern, a secret shape and a project-private path) passes it and it is a plain ref name; otherwise a generic sentence carries no part of it (#572). Native TUI launch exposes its private socket/session tuple only to the authenticated console, bound to the same live owned socket and session after read-only native persistence proof. Tools/status do not receive it. The TUI has no single-writer guard: person input may interleave with ACP. Native policy remains unmanaged and closing the frontend does not stop ACP. An `important` message reaches a busy Jcode as a soft interrupt written to the hub's own private server socket for the owned session (#572); the hub never cancels a turn to deliver, and a steer that was sent but not acknowledged, or whose turn ended first, is held as `needs_review`, never replayed. Jcode has no switch to choose its MCP servers, so the owned serve and ACP processes load every server named in `<JCODE_HOME>/mcp.json` and the project's `.jcode/mcp.json`, `.mcp.json` and `.claude/mcp.json`, besides the hub's own; `ahub doctor` lists their names only. Point `jcode_home` at a separate home, or remove a server from those files, to keep it out.
244
256
 
245
257
  Antigravity (`agy`, #485) is optional, cloud and off roster by default. It runs native tools under its own unmanaged permissions, without a hub sandbox or hub permission-mode mapping; headless, resume, unattended and native permission flags are refused. Its roles are capped at implementer and verifier, so it never plans, reviews or receives PII work. Only the person's `ahub agy` launcher writes its three plugin files in `.agents/plugins/agent-hub/` (`plugin.json`, `mcp_config.json`, `hooks.json`): linked or foreign entries refuse before any write, cleanup removes only that launch's matching files, and it never writes project-level `.agents/hooks.json` or the person's `~/.gemini` settings. Conductor starts are refused. Hook authority (`native_hook_launch`) comes only from that launcher, belongs to the console socket that began it and ends when that socket closes; hook input over 1 MiB is refused, and observations are bound to the daemon, launch, native session and project.
246
258
 
@@ -355,7 +367,7 @@ The review brief (#520) withholds private changes whole (both sides of a move) a
355
367
 
356
368
  ## Bounded task specs and isolated checks (#418)
357
369
 
358
- `hub_task_spec` reads only the GitHub issues named by a current owner's or reviewer's task in this project's origin repository, including the validated canonical GitHub intake source when its title/body has no issue token. Bare `#N` references count only in the title; detail references require `owner/repo#N` or `https://github.com/owner/repo/issues/N`. Bare detail numbers denote hub tasks, and arbitrary file paths are not parsed as issue references. A qualified reference cannot begin after a slash or a name character, so `src/hub/tasks.ts#12` is a file path. Routing uses this same parser. Without a GitHub project identity, bare title numbers do not require spec access, but explicit qualified/URL references, `refs.repo` and imported GitHub identity still fail closed. The daemon resolves authenticated and container executables outside the project, pins their absolute paths and uses fixed arguments; repository-writable PATH shims are refused. Runner Git resolves its host executable through a restricted PATH. For specs, the model receives no credential or arbitrary authenticated URL capability. Foreign/transferred issues, more than three references, bodies over 64 KiB and aggregate results over 80 KiB fail explicitly. Spec fetches share a 25-second budget inside the control client's 30-second cap; each gh call remains bounded to 15 seconds. Current PII, secret and private-path policy withholds the complete spec rather than returning a partial contract. PII turns and PII tasks refuse these tools. The independent `spec_read` and `check_run` capabilities apply when a peer has a narrowed capability list. Arguments and results are excluded from capture. Proven deterministic pre-effect guards return PreEffectToolRefusal so Pi settles the exact refusal receipt; generic transport/storage failures and failures after durable receipt/output changes remain uncertain, never reclassified from error text.
370
+ `hub_task_spec` reads only the GitHub issues named by a current owner's or reviewer's task in this project's origin repository, including the validated canonical GitHub intake source when its title/body has no issue token. Bare `#N` references count only in the title; detail references require `owner/repo#N` or `https://github.com/owner/repo/issues/N`. Bare detail numbers denote hub tasks, and arbitrary file paths are not parsed as issue references. A qualified reference cannot begin after a slash or a name character, so `src/hub/tasks.ts#12` is a file path. Routing uses this same parser. Without a GitHub project identity, bare title numbers do not require spec access, but explicit qualified/URL references, `refs.repo` and imported GitHub identity still fail closed. `refs.repo`, which `hub_task_propose` and `hub_task_accept` take since #476 to scope overlap per repository, is metadata only: it grants no file access or execution root, and a spec read for a task naming another repository refuses. The daemon resolves authenticated and container executables outside the project, pins their absolute paths and uses fixed arguments; repository-writable PATH shims are refused. Runner Git resolves its host executable through a restricted PATH. For specs, the model receives no credential or arbitrary authenticated URL capability. Foreign/transferred issues, more than three references, bodies over 64 KiB and aggregate results over 80 KiB fail explicitly. Spec fetches share a 25-second budget inside the control client's 30-second cap; each gh call remains bounded to 15 seconds. Current PII, secret and private-path policy withholds the complete spec rather than returning a partial contract. PII turns and PII tasks refuse these tools. The independent `spec_read` and `check_run` capabilities apply when a peer has a narrowed capability list. Arguments and results are excluded from capture. Proven deterministic pre-effect guards return PreEffectToolRefusal so Pi settles the exact refusal receipt; generic transport/storage failures and failures after durable receipt/output changes remain uncertain, never reclassified from error text.
359
371
 
360
372
  `hub_check_start` accepts only a current owner's in-progress task, its recorded branch/commit and either `typecheck` or one to 32 existing `test/*.test.ts` paths. `checks.runner = "container"` is a person opt-in in an untracked config file; a tracked config cannot grant it. Missing service/image, an unverified internal network, stale task/owner/native generation, unavailable dependencies or an invalid path fails closed. There is no host-shell, network or Seatbelt fallback.
361
373
 
package/docs/smoke.md CHANGED
@@ -626,9 +626,9 @@ In the project directory, one terminal each:
626
626
 
627
627
  Needs a model gateway in `omniroute.urls` (for the owner: the campus gateway over VPN, or the Access-protected public URL with its two header files) and a key: `OMNIROUTE_API_KEY`, or `omniroute.api_key_file` in `.agenthub/config.json`.
628
628
 
629
- 1. `ahub doctor`: `omniroute` healthy, `omniroute key` present, `switchyard` installed or not.
629
+ 1. `ahub doctor`: `omniroute` healthy, `omniroute key` present, every routing model alias served; no Switchyard row.
630
630
  2. In a scratch git repo: `ahub up`, `ahub local`, `ahub tail`, then `ahub say @local "fix the typos in <file>, run git diff --stat and report"`. `ahub tail` shows a permission request for the edit; `ahub permit <id> allow`. The file changes, the answer is a short conclusion, `ahub status` shows the served model in the local row's `MODEL` column and `vllm` in the backend table's `PROVIDER` column.
631
- 3. Same with `AGENTHUB_SWITCHYARD_BIN` (or `switchyard-server` on PATH) set before `ahub up`: `ahub status` shows the served model in `MODEL`, `switchyard` in the backend's `PROVIDER` column, and a `switchyard` detail with `127.0.0.1:<port>`; `lsof -nP -iTCP -sTCP:LISTEN | grep switchy` shows loopback only; `.agenthub/state/switchyard.toml` is mode 600 and holds no key; after `ahub kill` the file and the process are gone.
631
+ 3. In an isolated scratch project, use `[local] route = "hub/stage"` with served `fast` and `coding` aliases. Confirm the in-process route event and gateway provider. Try a legacy `sy/` selection: it uses `fixed_model` with one warning, starts no sidecar, and leaves the routing file unchanged. The older Switchyard receipts below describe the retired integration.
632
632
  4. claude-mem: `sqlite3 -readonly ~/.claude-mem/claude-mem.db "select agent_id, agent_type, project, title from observations order by id desc limit 3"` shows `local | local-worker` rows a minute or two later (claude-mem's observer runs asynchronously).
633
633
 
634
634
  ## M4: task board