@staix/agent-hub 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -2
  3. package/docs/agent-notes/adapters.md +2 -1
  4. package/docs/agent-notes/budget.md +1 -1
  5. package/docs/agent-notes/bus.md +4 -1
  6. package/docs/agent-notes/daemon.md +3 -2
  7. package/docs/agent-notes/local-worker.md +1 -1
  8. package/docs/agent-notes/models.md +2 -1
  9. package/docs/agent-notes/tasks.md +1 -0
  10. package/docs/agent-notes/tests.md +1 -1
  11. package/docs/dogfood.md +7 -1
  12. package/docs/operations.md +147 -6
  13. package/docs/profiles.md +28 -3
  14. package/docs/quickstart.md +5 -1
  15. package/docs/security.md +7 -1
  16. package/docs/specs/2026-09-19-agent-hub-design.md +66 -4
  17. package/docs/verification/2026-10-30-0.23.0.md +70 -0
  18. package/docs/verification/2026-10-30-adapter-operational-settings.md +261 -0
  19. package/docs/verification/2026-10-30-model-operational-settings.md +211 -0
  20. package/docs/verification/2026-10-30-operational-settings-closure.md +1217 -0
  21. package/docs/verification/2026-10-30-ui-console-operational-settings.md +1845 -0
  22. package/docs/verified.json +15 -15
  23. package/package.json +1 -1
  24. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  25. package/plugins/agent-hub/ROLE-NOTICE.md +1 -1
  26. package/plugins/agent-hub/server.js +559 -115
  27. package/src/adapters/acp.ts +48 -2
  28. package/src/adapters/claude-channel.ts +6 -4
  29. package/src/adapters/codex-appserver.ts +19 -0
  30. package/src/adapters/jcode.ts +1 -1
  31. package/src/adapters/local-worker.ts +3 -3
  32. package/src/adapters/native-capabilities.ts +50 -0
  33. package/src/adapters/registry-types.ts +3 -10
  34. package/src/cli/capabilities-output.ts +30 -0
  35. package/src/cli/console-state.ts +8 -5
  36. package/src/cli/console.ts +27 -4
  37. package/src/cli/help.ts +2 -1
  38. package/src/cli/identity.ts +1 -0
  39. package/src/cli/main.ts +8 -0
  40. package/src/hub/board.ts +8 -2
  41. package/src/hub/bus.ts +100 -8
  42. package/src/hub/capabilities.ts +215 -0
  43. package/src/hub/config-apply.ts +6 -3
  44. package/src/hub/config-trust.ts +1 -0
  45. package/src/hub/control-client.ts +41 -8
  46. package/src/hub/daemon.ts +376 -20
  47. package/src/hub/deferred-hub.ts +2 -2
  48. package/src/hub/delivery-hints.ts +2 -2
  49. package/src/hub/delivery-journal.ts +2 -4
  50. package/src/hub/envelope.ts +12 -2
  51. package/src/hub/events.ts +2 -0
  52. package/src/hub/hub-tools.ts +16 -1
  53. package/src/hub/inference.ts +10 -5
  54. package/src/hub/invocations.ts +316 -0
  55. package/src/hub/operational-settings.ts +102 -1
  56. package/src/hub/peers.ts +3 -0
  57. package/src/hub/restart.ts +2 -2
  58. package/src/hub/room-invocations.ts +145 -0
  59. package/src/hub/settings.ts +1 -1
  60. package/src/hub/shipped-packs.ts +5 -2
  61. package/src/hub/shipped-roles.ts +13 -0
  62. package/src/hub/tasks.ts +14 -0
  63. package/src/memory/capture.ts +2 -2
  64. package/src/models/relay.ts +20 -9
  65. package/src/rooms/application.ts +67 -2
  66. package/src/rooms/boundary.ts +43 -11
  67. package/src/rooms/service.ts +40 -2
  68. package/templates/checks/analysis.mjs +59 -0
  69. package/templates/checks/fixtures/analysis-expected.json +1 -0
  70. package/templates/checks/fixtures/analysis-input.json +1 -0
  71. package/templates/checks/fixtures/analysis.mjs +7 -0
  72. package/templates/checks/fixtures/ops.json +4 -0
  73. package/templates/checks/fixtures/writing-rules.json +4 -0
  74. package/templates/checks/fixtures/writing.json +5 -0
  75. package/templates/checks/ops.mjs +40 -0
  76. package/templates/checks/writing.mjs +40 -0
  77. package/templates/packs/analysis.pack.json +14 -0
  78. package/templates/packs/ops.pack.json +14 -0
  79. package/templates/packs/writing-pack.pack.json +14 -0
  80. package/templates/roles/NOTICE.md +1 -1
  81. package/templates/roles/ops/meeting-actions-owner.md +26 -0
  82. package/templates/roles/ops/project-manager.md +26 -0
  83. package/templates/roles/writing/editor.md +26 -0
  84. package/templates/roles/writing/style-checker.md +26 -0
  85. package/templates/roles/writing/terminology-checker.md +26 -0
  86. package/templates/roles/writing/translator.md +26 -0
@@ -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.22.0
20
+ bun add -g github:STAIxBWLB/agent-hub#v0.23.0
21
21
  ahub setup
22
22
  ```
23
23
 
@@ -30,6 +30,10 @@ 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.23.0
34
+
35
+ Nothing changes until you set something. Thirteen more operational numbers became settings in `config.local.json`, each keeping its old value when unset: three `adapter_timing.*` (at the next Kimi, ACP or Jcode launch, #982), four `model_relay.*` and `inference.backoff_min` (at the next daemon restart, #984) and five `console.*` (at the next console launch, #987). Capability sharing is new and opt-in. `ahub capabilities` (person only) lists the skills an attached Kimi 2.3.2, Qwen 0.26.1 or Codex 0.166.0 offers; nothing is offered to other agents until you pin an entry's digest under `invocation.share` in `.agenthub/config.local.json` (#985). Agents then call a shared `read` capability with `hub_invoke`. A `write` or `external` one needs an open task of the caller and your allow-once approval on every call, and one with no class is never invoked (#986, #991; see [invoking a shared capability](operations.md#invoking-a-shared-capability-445-c2c3)). Across a room, only a read capability both persons pinned crosses (#992). `ahub init --profile writing-pack|analysis|ops` applies the new shipped domain packs (#993). The `@staix/agent-mem` pin stays 0.13.0. Upgrade running hubs with `ahub upgrade` (an older hub refuses `hub_invoke`) and restart Claude sessions so they load the 0.23.0 plugin.
36
+
33
37
  ## Upgrading to 0.22.0
34
38
 
35
39
  Nothing changes until you set something. Ten operational numbers became settings: five bus and delivery limits (`bus.*`, #975) and five upgrade-recovery waits (`recovery.*`, #979). Unset, each keeps the value it had. `ahub learn list|accept|reject` (#514b, person only) is your verdict on the recurring-unmet-criterion cards. The macOS PTY bridge no longer hangs under load or outlives its caller (#560). The `@staix/agent-mem` pin stays 0.13.0. Upgrade running hubs with `ahub upgrade` and restart Claude sessions so they load the 0.22.0 plugin.
package/docs/security.md CHANGED
@@ -7,7 +7,7 @@ agent-hub connects agents that can each run commands. This page says what the hu
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
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, and a headless Claude implementer's proven shell commands through the host wrapper (#845, below); 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. A call the always-ask policy classes (below) is never approved this way, whatever its title. 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. Configured ACP kinds (#451) never get this exception: a request whose title spells a hub tool exactly still goes to the person approval path, and only a mapped native mode changes what the agent itself asks. 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; a classed call from a hub-owned ACP peer, the local worker or Pi still waits for a person under it.
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` (still accepted, ignored since 0.16.0), `local.read_allow`, `local.bash_network`, `local.network_allow`, `local.doc_read`, `local.web_fetch`, `terminal.open`, `tasks.auto_pull`, `tasks.queue_backpressure`, `context.max_assignment_used`, `schedules`, `folder_triggers`, `corpus.paths`, `instances`, `action_policy`, `rooms`) apply only from a config file git confirms nobody committed (`instances`, `action_policy` and `rooms` are read only from an untracked `.agenthub/config.local.json`; in `config.json` they are 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, a folder the hub reads, work proposed on a timer, a host a model may fetch from, 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 Git calls of one burst share a 250 ms budget, each call at most 10 s; a boundary that runs out is recorded as unknown, never as a stale baseline) (#576). 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`).
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` (still accepted, ignored since 0.16.0), `local.read_allow`, `local.bash_network`, `local.network_allow`, `local.doc_read`, `local.web_fetch`, `terminal.open`, `tasks.auto_pull`, `tasks.queue_backpressure`, `context.max_assignment_used`, `schedules`, `folder_triggers`, `corpus.paths`, `invocation`, `instances`, `action_policy`, `rooms`) apply only from a config file git confirms nobody committed (`instances`, `action_policy` and `rooms` are read only from an untracked `.agenthub/config.local.json`; in `config.json` they are 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, a folder the hub reads, work proposed on a timer, a host a model may fetch from, which of your agents' skills other peers may call, 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 Git calls of one burst share a 250 ms budget, each call at most 10 s; a boundary that runs out is recorded as unknown, never as a stale baseline) (#576). 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. Budget pauses, resumes and quota resets carry the peer, window, times and the hub's own reason text, and a person's release authorization its commit, version and docs digest. The usage roll-up (#683) folds the usage, token and limit records into `hub.db` and joins issue, PR and release from the board when read; `ahub usage` and the console's Usage panel answer only the console and a configured planner, and a planner not during a private turn. Kimi's token records (#285) are counted from its own session wire log under its home: only the four token integers, the model label and the row time are copied, and the record id is an opaque hash, never a path, session id or text. The log 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.
12
12
  - **Snapshots stay in your repository.** Per-turn snapshots (issue #33) are git objects in the project's own object store, written through a temporary index; nothing is referenced, pushed or copied elsewhere, and `git gc` prunes them. They hold what the work tree held, including untracked files that are not ignored, so keep secrets in ignored files. They carry the repository's own permissions, and nothing caps their disk use but `git gc`. A turn of a peer holding an open PII task is not snapshotted; a PII file left in the project is snapshotted by later turns like any other file. `ahub undo` restores only files whose current content is exactly what the turn left.
13
13
  - **The edit hook reads, never decides.** `ahub check-path --hook` (issue #32) reads hub.db and returns context for Claude and a line for you; it sets no permission decision, so your permission rules stay in charge. It names other owners' task ids, titles and states, which then reach Claude's model; PII tasks are left out.
@@ -765,6 +765,12 @@ Room encryption authenticates a hub, not local role authority. Remote ids contai
765
765
  `@` and cannot connect to local control, own tasks, review, plan, conduct, start a
766
766
  native runtime or become a recovery session. Incoming chat is explicitly directed
767
767
  to a current exposed local native peer, framed as untrusted, and capped at status.
768
+ Since X6 (#992), a remote peer may also ask an exposed local peer to run one
769
+ capability that both persons pinned as `read`: the target's own `invocation.share`
770
+ entry and the source's exact `peer@label` entry. Task and approval refs, and write,
771
+ external or destructive requests, never cross. The target re-applies its schema,
772
+ digest, class and privacy gates before any native request, and an unknown handoff or
773
+ result is `needs_review`, never replayed.
768
774
  The same live sender-exposure check applies to restored inboxes. A removed
769
775
  sender's registered pending application is quarantined before its plaintext is
770
776
  retired, so it remains available for person disposition.
@@ -1170,7 +1170,18 @@ local hub work available. Fake-hub/broker tests are not a live TLS/proxy or X7 r
1170
1170
 
1171
1171
  This implements frozen D3/D5 from issue #446, comment 6242775004, with the
1172
1172
  approved X4 refinements and R1 clarifications. X5 metadata views are described
1173
- below; X6 invocations and X7 live two-machine receipts remain separate.
1173
+ below; X7 live two-machine receipts remain separate.
1174
+
1175
+ ### X6 as built (#446, task 187)
1176
+
1177
+ - Cross-hub invocations are read-only and require consent by pinning on both hubs. The source person pins the exact `peer@label`, capability id, digest and class `read` in the existing machine-local `invocation.share`; the target independently requires its current local share pin, effective read class, native schema and every #445 D4 gate. There is no inheritance between peers, instances or labels.
1178
+ - Discovery stays manual: the target person's existing `ahub capabilities` provides the digest out of band. X6 adds no remote discovery RPC or implicit sharing from room exposure alone.
1179
+ - Both sides validate arguments: bounded flat scalar shape at source, actual capability schema at target. Task, approval and other local refs never cross. Write, external and destructive invocations and approval/permission messages remain excluded.
1180
+ - Requests and results use closed, bounded encrypted application envelopes and opaque host provenance at the single room boundary. Generic hub/digest or raw agent invocation envelopes remain refused. A private or uncleared request is refused, never converted into an executable stub; an uncleared result is withheld without its body.
1181
+ - Target admission is bound to the signed room/member and current local capability. Durable invocation identity and result correlation include room, member fingerprint, peer ids, request id and digest. Dedupe never dispatches the same invocation twice; a mismatched or forged result cannot settle another call.
1182
+ - Local and remote nested calls retain the invocation turn identity and refuse depth 2 before another dispatch. Uncertain handoff, lost acknowledgement/result or restored uncertainty stays `needs_review`, never a silent retry. Signed application admission is distinct from a capability result.
1183
+ - Automated proof uses two fake hubs with the real built-in broker and crypto. Native account execution and live two-Mac X7 receipts remain separate and unverified.
1184
+
1174
1185
 
1175
1186
  ### Egress and authority
1176
1187
 
@@ -6308,7 +6319,9 @@ wire/storage/protocol contracts remain constants.
6308
6319
 
6309
6320
  ## Capability invocation (#445)
6310
6321
 
6311
- Plan of record: the #445 C0 freeze (issue comment of 2026-10-17, owner decisions O1-O5 of 2026-10-18, go-ahead 2026-10-25). Phases C1-C4 implement it in order and amend this section when they differ. Nothing below is implemented yet; the [C0 receipt](../verification/2026-10-30-445-capability-probes.md) records what the native sources actually expose.
6322
+ X6 remote reads require an exact source `peer@label` capability/digest/`read` pin plus the target's independent current local read pin and schema/D4 admission. The target person's `ahub capabilities` supplies the digest out of band; discovery remains manual, with no remote listing RPC. Room exposure alone grants no invocation, and no task or approval refs cross. See "X6 as built" under #446.
6323
+
6324
+ Plan of record: the #445 C0 freeze (issue comment of 2026-10-17, owner decisions O1-O5 of 2026-10-18, go-ahead 2026-10-25). Phases C1-C4 implement it in order and amend this section when they differ. C1 (discovery) and C2 (read invocations) are implemented, see "C1 as built" and "C2 as built" below; C3 adds the write/external approval and audit path below; live receipts (C4) remain unverified. The [C0 receipt](../verification/2026-10-30-445-capability-probes.md) records what the native sources actually expose.
6312
6325
 
6313
6326
  Delegated execution, never shared execution: a peer publishes capabilities, another peer asks for one, and the target runs it in its own turn with its own permissions and sandbox. The hub never calls a native tool itself (Codex `mcpServer/tool/call` and `command/exec` are excluded) and never hands out credentials or MCP connections.
6314
6327
 
@@ -6316,9 +6329,9 @@ Delegated execution, never shared execution: a peer publishes capabilities, anot
6316
6329
 
6317
6330
  - **D1 manifest.** An adapter-definition field (`registry-types.ts`), one discovery path per adapter. Each `Capability` has `id` (`${kind}:${name}`, unique per peer id), `kind` (`command|skill|mcp_tool`), `name` (`/^[A-Za-z0-9_.:-]{1,64}$/`), a `sanitize()`d description of at most 300 characters, an optional flat `inputSchema` (absent means `{ input: string }`), the publisher's optional `sideEffect` (`read|write|external|destructive`), `source` (`acp|codex-skills|published`), `invoke` (`native|prompt`) and a `digest` over the canonical fields. Sources: ACP `available_commands_update` (`invoke: native`, the prompt `/name input`); Codex `skills/list` (`invoke: native`, the `turn/start` item `{ type: "skill", name, path }`); `hub_capabilities_publish` for Claude and others and Codex MCP tools (`invoke: prompt`). No cost hint.
6318
6331
  - **D2 opt-in and discovery.** `invocation.share` in `.agenthub/config.local.json` only (`invocation` is machine-local and never editable from settings): a positive allowlist keyed by peer id, each entry pinned by digest with the person's `class`. A changed digest unlists the entry until re-pinned; an instance id never inherits its kind's entries. `hub_capabilities { peer? }` lists shared entries only (id, schema, effective class, the target's permission mode); unshared ones appear only in the person's `ahub capabilities`.
6319
- - **D3 authority.** Callers hold the new `invoke` hub capability; the conductor gets nothing extra. Effective class is the higher of the declaration and the person's `class` (O1); a side that names no class contributes nothing, and only when neither names one is the class `destructive` (O6). Gates: `read` needs no task and no approval; `write` and `external` need an open task whose owner or reviewer is the caller plus person approval on every call through `requestPermission` with a `personOnly` flag that bypasses unattended auto-allow (O2); `destructive` is refused (O3). Every hub-side refusal is a `PreEffectToolRefusal` with a precise reason before any envelope. A callee refuses with `[REFUSED] <reason>` (outcome `refused`); a native rejection is `failed`. Each invocation is a row in an `invocations` table in `hub.db` (ids, digest, class, task, approval id, args sha256, outcome, times, result bytes) plus an ids-only event; a task-scoped one adds history through `Tasks.recordInvocation`. Bodies are never logged.
6332
+ - **D3 authority.** Callers hold the new `invoke` hub capability; the conductor gets nothing extra. Effective class is the higher of the declaration and the person's `class` (O1); a side that names no class contributes nothing, and only when neither names one is the class `destructive` (O6). Gates: `read` needs no task and no approval; `write` and `external` need an open task whose owner or reviewer is the caller plus person approval on every call through `requestPermission` with a `personOnly` flag that refuses unattended mode (O2). The person-only prompt shows caller, target, capability, effective class, task id and argument digest, plus already PII-screened arguments sanitized and capped at 200 characters per value with a cut marker. Argument previews stay on person approval surfaces only, never logs, events, history or peer-visible views; `destructive` is refused (O3). Every hub-side refusal is a `PreEffectToolRefusal` with a precise reason before any envelope. A callee refuses with `[REFUSED] <reason>` (outcome `refused`); a native rejection is `failed`. Each invocation is a row in an `invocations` table in `hub.db` (ids, digest, class, task, approval id, args sha256, outcome, times, result bytes) plus an ids-only event; a task-scoped one adds history through `Tasks.recordInvocation`. Bodies are never logged.
6320
6333
  - **D4 PII.** `hub_invoke` is refused during a PII turn, for a PII task, and when the Tasks PII screen finds or cannot rule out PII in free-text args. The result is bounded to 8 KiB, `sanitize()`d, given a hub-written untrusted header, addressed to the caller only and never captured to memory.
6321
- - **D5 transport.** New envelope kinds `invocation` and `invocation_result`, journalled, delivered alone and never condensed, priority `status`. `hub_invoke` returns `{ invocation, state: "queued" }` at once. Deadline `invocation.timeout_s` (default 600): still queued means `expired`; delivered without a result means `timeout` for `read` and `needs_review` otherwise. No caller cancel; the person uses `ahub queue resolve`. Offline, paused, held, off-roster and pull-only targets are refused (O4). Depth 1: no `hub_invoke` inside an invocation turn. `invocation` joins `DAEMON_FEATURES`; upgrade and restart refuse while invocation rows are open on a target without it.
6334
+ - **D5 transport.** New envelope kinds `invocation` and `invocation_result`, journalled, delivered alone and never condensed, priority `status`. `hub_invoke` returns `{ invocation, state: "queued" }` after admission and any required person approval. Deadline `invocation.timeout_s` (default 600): still queued means `expired`; delivered without a result means `timeout` for `read` and `needs_review` otherwise. No caller cancel; the person uses `ahub queue resolve`. Offline, paused, held, off-roster and pull-only targets are refused (O4). Depth 1: no `hub_invoke` inside an invocation turn. `invocation` joins `DAEMON_FEATURES`; upgrade and restart refuse while invocation rows are open on a target without it.
6322
6335
  - **D6 failures.** An uncertain `write` or `external` outcome is `needs_review` and never retried; a `read` may be redelivered once, on a `failed_safe` receipt only.
6323
6336
  - **O5.** `invoke: prompt` capabilities are listed and invocable, labelled as model-mediated requests.
6324
6337
  - **O6 (2026-10-30).** `destructive` applies only when neither the publisher nor the person's share entry names a class. It relaxes the freeze's "undeclared reads as `destructive`", which with O1 would have refused every native capability, since no native listing declares a class.
@@ -6335,3 +6348,52 @@ Probed on Kimi 2.3.2, Qwen 0.26.1 and Codex 0.166.0 in isolated, credential-free
6335
6348
  - **Native rejection.** Qwen rejects a command outside its ACP set with `-32603`; Kimi ends the turn normally with an "Unknown ACP command" text chunk (source reading). C2 maps Qwen's error code and matches Kimi's text to `failed`.
6336
6349
  - **Codex.** `skills/list` answers with no thread and no auth. The `skill` input item is in the stable schema; a live skill turn is unverified. `mcpServer/tool/call` has no approval gate in the 0.166.0 source, which confirms its exclusion.
6337
6350
  - **Unverified until C4.** Executing a skill through `/name input` on Kimi or Qwen and a Codex `skill` turn (each needs an account), and listings under a real login.
6351
+
6352
+ ### C1 as built (#445, task 178)
6353
+
6354
+ C1 lists capabilities and nothing else: no `hub_invoke`, no `invoke` capability, no envelope kinds, no `invocations` table. These points are where the build fixes or differs from the freeze above.
6355
+
6356
+ - **Where.** `src/hub/capabilities.ts` holds the pure rules (digest, sanitizer, flat schema, publish validation, share listing). The two native parsers and their version pins are vendor-specific, so they live in `src/adapters/native-capabilities.ts`, where the #451 registry guard allows vendor literals. `AcpPeer` and `CodexPeer` implement a new optional `PeerAdapter.capabilityManifest()`; the daemon reads it per call, never a cached or probed copy. The per-definition `capabilities.manifest` field of D1 is not used: the port on the adapter is.
6357
+ - **No class is declared natively.** `Capability.sideEffect` is optional and absent for every native listing, so the digest carries `null` for it. `effectiveClass` is the higher of the declared and the person's class, a side that names none contributes nothing, and only when neither names one is the class `destructive` (O6). A value that is not one of the four reads as `destructive`. The `exec` class is removed from the manifest type.
6358
+ - **Pins.** `NATIVE_PINS` is the one place the probed versions are written: Kimi 2.3.2 (`Kimi Code CLI`), Qwen 0.26.1 (`qwen-code`), Codex 0.166.0. A listing on any other version is empty. The repository's Codex pin is 0.154.0, so a pinned Codex lists nothing until a receipt covers it; this is intended and shown in `ahub help capabilities`.
6359
+ - **ACP lists belong to a session.** The `available_commands_update` of the current session is kept whatever the turn state. One that arrives before `session/new` is answered waits and counts only when it names the session that was bound; one for another session is dropped. A restart clears the list. An offline peer lists nothing.
6360
+ - **Codex.** `skills/list { cwds: [project], forceReload: false }` goes through the attached app-server connection with a hub id, so the TUI never sees the answer. Enabled skills only; the short description is used when there is one. An unattached, offline or unanswered peer rejects, which the daemon shows as no list.
6361
+ - **Config.** `invocation` is a machine-local block validated by the closed schema (`invocation.share` only in C1; `timeout_s` arrives with C2) and applies live. `hub_capabilities` reads the share from the running config on every call.
6362
+ - **Tools.** `hub_capabilities { peer? }` is a read tool, exposed to read-only projects; it needs no task and no approval. `hub_capabilities_publish { capabilities }` is the source for a session the hub cannot read natively: a peer whose adapter implements `capabilityManifest` is refused, every field is checked, an unknown field refuses the whole call, the invoke form is forced to `prompt`, the list replaces the last one and is bound to the publishing connection (the daemon's connection generation), and it is dropped when the peer goes offline. Both calls are refused during a PII turn and for a peer holding PII, and neither is captured to memory (`CAPABILITY_TOOLS`).
6363
+ - **The person's view.** `ahub capabilities` (op `capabilities_list`, Console only, no `CONDUCTOR_TASK_OPS` entry) shows every found entry with its state (`listed`, `unshared` or `digest-changed`) and the class that would apply, for every peer, shared or not. The audit label is `capabilities` with no argument echoed.
6364
+ - **Not decided here.** Whether a Codex skill can be run through a `skill` turn item, and how a Kimi or Qwen `/name input` behaves under a real login, stay C0's unverified list and move to C2 and C4.
6365
+
6366
+ ### C2 as built (#445, task 179)
6367
+
6368
+ C2 runs `read` invocations end to end. `write` and `external` stay refused (the person-approval path is C3) and `destructive` is refused for good (O3). These points are where the build fixes or differs from the freeze.
6369
+
6370
+ - **Where.** `src/hub/invocations.ts` holds the pure gates (`invocationRefusal`), the argument check (`checkArgs`), the two texts the hub writes (`requestText`, `renderInvocationResult`), `refusedBy` and the `InvocationLedger`. The daemon's `hub_invoke` branch sits beside the C1 capability tools in `taskOpBody`. Adapters are unchanged.
6371
+ - **Every invocation is a model-mediated request in C2.** The freeze's `invoke: native` form (the target's own `/name input` path, a Codex `skill` turn item) is not implemented: it needs a live account to verify and is C4. The target is handed one hub-written envelope naming the caller and the capability, and answers it as an ordinary turn under its own permissions and sandbox. `invoke: native` capabilities are therefore invocable only as that request, and the request says so.
6372
+ - **Transport.** The request is an envelope from the hub to the target alone (`kind: invocation`, `priority: status`, `refs.invocation` = its own id). The callee's answer is taken over inside the bus, before anything is built from it (`Bus.onAnswer`): it is recognised by what the peer was handed (a delivery holding an `invocation` request) when it names that request, or names no parent while that invocation's own turn remains open, never by its recipient address. Once the turn ends, an unnamed later message is ordinary traffic and is published; an explicit late or repeated reply naming the request remains captured and dropped. Important messages are not steered into the invocation's open turn, so they cannot change its reply parent. A headless agent whose final answer is normally shared with every sender therefore cannot leak that invocation's answer. A taken answer is published nowhere, stored nowhere and emitted in no event, so no tap, queue, digest, recall preface, journal row or memory path sees it, and a late or repeated answer is dropped rather than broadcast. The daemon settles the row and publishes ONE `invocation_result` from the hub to the caller only, with a hub-written first line, the callee's text sanitized (a forged hub header is quoted) and cut to 8 KiB by code point. An explicit `hub_send` with `reply_to` the request is the same answer. A reply starting `[REFUSED]` is `refused`. The id of the request, the ledger row and the result are one id. Both kinds are in `KINDS` (`envelope.ts`), which the journal, the restart reader, the deferred-notice check and the held-delivery hints all read.
6373
+ - **Alone, never condensed.** `Bus.take` and `Bus.takeBudget` end a batch at an invocation or result envelope, and never put one beside another. The daemon's tap handles these envelopes first and returns, so no log line carries a body and no memory citation sees one.
6374
+ - **Gates, all before any envelope, one reason each.** Order: PII context, depth, own capability, off roster, offline, paused, held, pull-only, no shared capability (an unshared entry and a missing one read the same, so an agent learns nothing about what the person withheld), digest changed, destructive, write or external (C3), task not yours or PII, per-caller and per-target open caps (4 and 8). Then the arguments against the capability's flat schema (a native form refuses a newline), then the Tasks pattern check and, when the screen is on, the model screen on free-text arguments, with unknown refused. Every fact is read again after that await and the gates run again. Depth is the target holding a `delivered` row: the caller of a nested invocation is the target of an open one.
6375
+ - **Result privacy.** The callee's answer goes through the pattern check, the secret check, the path policy and, when `signals.pii_screen` is on, the model screen, with unknown treated as PII, and it is checked again after that await. The full answer is screened, rather than only its shortened refusal reason, before any reason is forwarded. If the callee's turn is private (`turns.get(target).private`, `contextPii`) or the scoped task is PII, or any check does not clear it, an answer starting `[REFUSED]` retains outcome `refused` and returns only the fixed reason `<target> declined; the reason was withheld`; no callee text is kept. An ordinary unsafe answer retains outcome `withheld` and its existing fixed text, with no body kept anywhere. Unknown or a screen failure follows the same refusal-versus-ordinary distinction; a clean refusal retains its screened reason.
6376
+ - **Pause and budget.** A target paused by the person, the conductor or the budget, or whose quota window is exhausted, is refused at call time. A pause that starts while the request is queued leaves it queued under its deadline. The caller is charged the ordinary per-sender and per-pair limits as for a message to the target (the request itself is a hub envelope, which no limit applies to), and every invocation is `priority: status`.
6377
+ - **Deadline and receipts.** `invocation.timeout_s` (10 to 86400, default 600). A 5 s sweep (`invocationSweepMs` is a test seam) withdraws a request whose ledger row is `queued` and that is still waiting in the target's delivery queue at its deadline (`expired`, no effect). A ledger row can still say `queued` after its request was handed over but before the adapter reports acceptance; if the request is no longer in that queue, it may already be running and receives `timeout`, as does a `delivered` read with no result. The bus marks a row `delivered` from `onDelivered` (actual transport admission) and the daemon settles it `failed` on `undeliverable`. A row settles once, so a late answer after a timeout changes nothing. The freeze's one redelivery of a read after a `failed_safe` receipt is the bus's own retry (it requeues the original, at most `bus.max_attempts` times) rather than a second mechanism.
6378
+ - **Record.** The `invocations` table in `hub.db` holds ids, digest, class, task, a digest of the arguments, outcome, times and result bytes, never an argument or a result. An ids-only `invocation` event is written at queue and at settle. `hub_invoke` joins the capture skip list. The task history entry (`Tasks.recordInvocation`) is C3, with the first call that can change something.
6379
+ - **Feature flag.** `invocation` is in `DAEMON_FEATURES`. The Claude plugin refuses `hub_invoke` with a clear message against a hub without it, and recovery refuses a target without the feature while any invocation row is open. `PROTOCOL` stays 16.
6380
+ - **Not in C2.** The `invoke` hub capability is a name in `capabilities.<peer>` and defaults on, as every other (an unlisted peer has all of them). Per-class gates beyond read, the approval path, `personOnly`, the PII task refusals for write, and every live receipt are C3 and C4.
6381
+
6382
+
6383
+
6384
+ ### C3 as built (#445, task 180)
6385
+
6386
+ - Write/external admission requires an open non-PII task whose owner or reviewer is the caller. Unattended refuses before prompting. Destructive remains refused. Person approval uses allow-once/deny only; the existing native forceAsk policy is unchanged.
6387
+ - The person-only prompt names caller, target, capability digest, effective class, task and arguments digest. It also shows screened, sanitized arguments, at most 200 Unicode characters per value, marked when cut. The prompt is available through the authenticated console/dashboard permission path; no argument preview is logged, emitted as an event, placed in task history or returned in a peer-visible permission view.
6388
+ - Admission rechecks session generations, current capability/share/digest/class, task scope/privacy, turn privacy, roster, pause/hold, depth, open caps and shutdown after asynchronous screens/approval. A changed binding refuses the call; an approval never follows a changed task or capability.
6389
+ - Control requests for `hub_invoke` cover the supported 3600-second approval ceiling plus admission/transport margin. MCP cancellation and client abandonment cancel only that connection's pending request; closing its control connection aborts the pending permission. Current admission checks the abort fence after every await, so a late allow cannot dispatch a disconnected or abandoned call. An already admitted native turn is never cancelled through this path.
6390
+ - The existing ledger approval column stores the actual person approval id. Task-scoped calls record queued metadata through host-only `Tasks.recordInvocation` before publication, and terminal metadata after settle-once. History carries ids, digests, class, approval and outcome only; it changes no task state and emits no task notification. A failed queued audit prevents delivery.
6391
+ - Only a `read` may automatically redeliver, at most once and only after `failed_safe`, independent of `bus.max_attempts`. Write/external safe refusal ends failed without replay; an ambiguous receipt, thrown handoff, restored uncertain delivery or delivered deadline is `needs_review` and holds the journal. Recipient retry cannot replay invocation requests; the person's existing queue resolution remains explicit. A queued request never handed over still expires without effect.
6392
+ - Capability execution remains model-mediated under the callee's own sandbox and permissions. Native forms, live-account execution and C4 receipts are not supplied by this change.
6393
+
6394
+
6395
+ ## Amendment: shipped writing, analysis and ops packs (#513b)
6396
+
6397
+ The writing domain pack is named `writing-pack` (base `writing`), preserving the existing rule that a pack cannot shadow a profile. `analysis` uses `research` and the existing analyst role; `ops` uses `general`. The catalog adds four original writing roles (editor, translator, style-checker, terminology-checker) and two original ops roles (project-manager, meeting-actions-owner). Their guidance grants no authority, and pack loading does not assign peers automatically.
6398
+
6399
+ Each pack supplies criterion ids in its rubric and exactly one advisory check entry. Standalone examples in `templates/checks/` validate writing JSON metadata/literal terminology/mechanical style, rerun an explicitly selected analysis program twice against declared input/expected bytes, or validate meeting-action owner/due/status records. They are shipped with synthetic fixtures, not executed by init or by parsing pack data. The existing person-owned `checks` policy is still the only way to select a task command. Inputs are bounded to 1 MiB, analysis runs to 5 seconds and 1 MiB stdout each, and errors never echo artifact contents. Checks emit criterion ids and a deterministic final result; semantic quality, meeting coverage and undeclared analysis dependencies remain reviewer judgments. `docs/profiles.md` records exact invocations, formats and limits. The role inventory lint compares every template group with the bundled registry rather than assuming the first eleven definitions. No live adoption or native model ingestion is claimed.
@@ -0,0 +1,70 @@
1
+ # 0.23.0 release verification
2
+
3
+ This record covers release preparation and source review. It claims no installation, no running-hub upgrade to 0.23.0 and no live TUI launch.
4
+
5
+ ## Candidate
6
+
7
+ Version 0.23.0, prepared from main 2dbd4853 (11 commits since v0.22.0, 8 of them `feat`; bump rule: minor). The release commits carry the version, the CHANGELOG section, the bundle and the documentation stamps.
8
+
9
+ ## Reviews
10
+
11
+ Pull requests named by the commits since v0.22.0: #993, #992, #991, #990, #989, #986, #987, #567, #988, #985, #984, #982.
12
+
13
+ Every one merged with exact-head CI green and no unresolved review thread. The conductor (claude) read each hub approval in `hub.db` and compared its head with the merged head:
14
+
15
+ - Merged at the approved head: #993 (task 322, ae6900e6), #990 (task 572, 58ba2e2b), #988 (task 567, 104d5eb5).
16
+ - Merged after the conductor re-reviewed a follow-up diff on top of the approved head, because the hub refuses a second approval of an approved task:
17
+ - #992 (task 187): approved d1bcf22c, merged e492e359. The follow-up sorts the remote argument keys, a connector P2.
18
+ - #991 (task 180): approved 4b54d96f, merged 17c3df2a. The follow-up gives `hub_invoke` an approval-sized control timeout with cancellation, a connector P2.
19
+ - Merged after a rebase whose `git range-diff` against the approved head shows no code change:
20
+ - #987 (task 448): approved 6bae3153, merged 8452d254; every commit is identical.
21
+ - #986 (task 179): approved 08096575, merged 5920c228; one commit message differs.
22
+ - #984 (task 447): approved eaeeb7b2, merged 1f8e3d7a. The H5 commits dropped out after #982 merged, and the H6 commit is identical.
23
+ - #982 (task 446): approved 473d0d63, merged 90643007; one `docs/operations.md` conflict hunk was resolved in the release rebase.
24
+ - #985 (task 178): approved 5e2fccba, merged f21fe4fb. The rebase added one docs-only commit (the `acpManifest` comment and the parser location in the spec) and rebuilt the bundle.
25
+ - #989 is docs only (the #489 disposition, Fixes #489), with no hub task; claude reviewed it on the PR before the merge.
26
+
27
+ Release merge: #994 merged release/0.23.0 at 18947c14 with a merge commit, c3e322f4, so the stamp commit ce8da3ec is an ancestor of main. It was given a custom subject (`Merge release 0.23.0 (#994)`). `scripts/release-tag.ts` accepts only the default `Merge pull request #N from <owner>/release/0.23.0` subject, so it refused that commit. This note is carried by a second release PR merged with the default subject, and the tag goes on that merge commit; no history was rewritten and no guard was changed. The connector's P1 on #994 (stamps not in the ancestry) did not apply: it reviewed a squashed view (3c7b0aa) that is not in the branch history.
28
+
29
+ ## Native acceptance receipts
30
+
31
+ Receipts added under `docs/verification/` since v0.22.0: `docs/verification/2026-10-30-adapter-operational-settings.md`, `docs/verification/2026-10-30-model-operational-settings.md`, `docs/verification/2026-10-30-operational-settings-closure.md`, `docs/verification/2026-10-30-ui-console-operational-settings.md`.
32
+
33
+ This list is read from the repository, never written by hand. Native acceptance and the person's release consent are separate records: consent is `ahub sdlc authorize-release 0.23.0`, person only, and is not recorded in this file. The tag run refuses without it.
34
+
35
+ ## Document review
36
+
37
+ `node scripts/check-docs.mjs --require-fresh` reported 15 stale documents. Each was checked against its source commits since its stamp (cb942e06, or aeeaeb04 and 43867c65 for local-worker.md, models.md and benchmarks.md). The check looked for statements those commits made untrue and for rules an agent editing that area now needs. Every proposed fix was confirmed against the code before it was applied in ce8da3ec, and all 15 were then restamped at ce8da3ec.
38
+ - README.md (9): the report link now names this report. docs/quickstart.md (9): the install pin and a new "Upgrading to 0.23.0" note.
39
+ - docs/operations.md (10): the capabilities listing no longer says invoking is a later slice. The #489 line records the #989 closure. The relay cooldown names the `model_relay.*` settings (the streak resets after twice the cap, `src/models/relay.ts:251`). The `role list` groups and the `init --profile` text name the shipped packs.
40
+ - docs/security.md (9): `invocation` joins the machine-local field list, and the X4 boundary section states what X6 lets cross.
41
+ - src/cli/help.ts (1): the `ahub init` line names the shipped packs. Its watched paths do not include `src/hub/shipped-packs.ts`, so freshness alone would not have caught this.
42
+ - Agent notes:
43
+ - bus.md: invocation kinds go alone and are never condensed or steered into; the `onAnswer` capture order; no ordinary replay; add a kind only to `KINDS`; `remoteEnvelope` for provenance.
44
+ - daemon.md: the `invocation` recovery feature floor, next-use and restart settings captured once by their consumer, and the `hub_invoke` socket/`rid` abort binding.
45
+ - adapters.md: `adapter_timing.*` passed at construction, and native capability listings.
46
+ - budget.md: the invocation exception to the quota-pause retry rule.
47
+ - local-worker.md: remote turns are `remoteEnvelope`.
48
+ - models.md: `model_relay.*` and `inference.backoff_min` are restart-bound.
49
+ - tasks.md: the `Tasks.recordInvocation` audit rule.
50
+ - tests.md, benchmarks.md and agent-notes/README.md: no statement changed (#990 had already updated the CI shard lines).
51
+ - The CHANGELOG gained the missing #982, #984, #987, #988, #991, #992 and #993 entries, and the C1 and C2 entries no longer say invocation is a later slice.
52
+
53
+ ## Checks
54
+
55
+ In the release worktree with its own frozen install (`@staix/agent-mem` 0.13.0):
56
+ - `node scripts/check-docs.mjs --require-fresh` printed `docs: OK (0 stale documents)`.
57
+ - `bun x tsc --noEmit` was clean.
58
+ - `bun scripts/build.mjs --check` exited 0.
59
+ - `node scripts/check-package.mjs` printed `package: OK (673 files, @staix/agent-hub@0.23.0)`.
60
+
61
+ The release PR's exact-head CI is the full gate on both OSes.
62
+
63
+ ## Known gaps, not blocking
64
+
65
+ - No new live receipt in this release. #445 C4 (Claude invoking a Kimi command and a Codex skill on a real task, with a person approving) needs this release installed and is task 181. #446 X7 (a live room run on two Macs) is task 188. Still open: #912 AC1, #793 AC6 (task 468: the hub must be restarted with the remote runner), #418 AC8 and #332 AC6.
66
+ - Capability invocation (C1-C3, X6) is proved with fixture peers and two in-process daemons with the real broker and crypto; no native account executed a shared skill.
67
+ - The thirteen new settings were exercised through fixtures and real daemons in tests, never edited live against a running hub.
68
+ - The running hub on this machine is 0.20.0; no `ahub upgrade` to 0.23.0 has run.
69
+ - The Bun pin stays 1.4.2: 1.4.4 is under two weeks old until 2026-11-05 (task 565).
70
+ - The full `scripts/check.sh` was not run locally; the release PR's CI runs it on both OSes.