@staix/agent-hub 0.19.0 → 0.21.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 (184) hide show
  1. package/CHANGELOG.md +70 -3
  2. package/README.md +2 -2
  3. package/docs/agent-notes/README.md +11 -0
  4. package/docs/agent-notes/adapters.md +10 -5
  5. package/docs/agent-notes/benchmarks.md +27 -0
  6. package/docs/agent-notes/budget.md +2 -1
  7. package/docs/agent-notes/bus.md +3 -3
  8. package/docs/agent-notes/daemon.md +20 -7
  9. package/docs/agent-notes/local-worker.md +5 -0
  10. package/docs/agent-notes/models.md +2 -0
  11. package/docs/agent-notes/tasks.md +20 -10
  12. package/docs/agent-notes/tests.md +13 -5
  13. package/docs/bench.md +1 -1
  14. package/docs/data-policy.md +2 -0
  15. package/docs/dogfood.md +41 -5
  16. package/docs/events.md +2 -1
  17. package/docs/memory.md +9 -3
  18. package/docs/operations.md +598 -63
  19. package/docs/profiles.md +13 -1
  20. package/docs/quickstart.md +18 -9
  21. package/docs/research.md +7 -4
  22. package/docs/sdlc.md +66 -10
  23. package/docs/security.md +397 -35
  24. package/docs/smoke.md +127 -0
  25. package/docs/specs/2026-09-19-agent-hub-design.md +1149 -19
  26. package/docs/verification/2026-10-27-0.19.0.md +5 -5
  27. package/docs/verification/2026-10-27-0.20.0.md +75 -0
  28. package/docs/verification/2026-10-28-claude-2.1.311-2.1.312-bash-permission.json +635 -0
  29. package/docs/verification/2026-10-28-claude-2.1.311-2.1.312-permission-transport.json +403 -0
  30. package/docs/verification/2026-10-28-claude-implementer-permission-triage.md +123 -0
  31. package/docs/verification/2026-10-28-jcode-0.99.1-usage.md +41 -0
  32. package/docs/verification/2026-10-28-realpath-name-audit.md +222 -0
  33. package/docs/verification/2026-10-28-routing-f0bb8ad0.md +1000 -0
  34. package/docs/verification/2026-10-29-0.21.0.md +61 -0
  35. package/docs/verification/2026-10-29-model-effort-capabilities.md +123 -0
  36. package/docs/verification/2026-10-29-modelrouting-b152ddb9.md +953 -0
  37. package/docs/verification/claude-implementer-permission-triage.ts +160 -0
  38. package/docs/verification/claude-permission-transport-native.py +71 -14
  39. package/docs/verification/profiles/YYYY-MM-DD-research.md +1 -1
  40. package/docs/verified.json +26 -18
  41. package/package.json +2 -2
  42. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  43. package/plugins/agent-hub/server.js +1116 -284
  44. package/src/adapters/acp.ts +87 -3
  45. package/src/adapters/channel-peer.ts +11 -5
  46. package/src/adapters/claude-channel.ts +13 -6
  47. package/src/adapters/claude-implementer.ts +61 -7
  48. package/src/adapters/codex-appserver.ts +88 -5
  49. package/src/adapters/conformance/enrollment.ts +4 -4
  50. package/src/adapters/conformance/factories.ts +2 -1
  51. package/src/adapters/conformance/fakes/jcode-agent.ts +6 -2
  52. package/src/adapters/conformance/kit.ts +10 -6
  53. package/src/adapters/definitions/builtin-metadata.ts +3 -114
  54. package/src/adapters/definitions/native-config-defaults.ts +1 -1
  55. package/src/adapters/definitions/native-config-policy.ts +3 -2
  56. package/src/adapters/definitions/native-context.ts +12 -0
  57. package/src/adapters/external-metadata.ts +48 -0
  58. package/src/adapters/external-modules.ts +63 -3
  59. package/src/adapters/jcode.ts +6 -2
  60. package/src/adapters/local-worker.ts +42 -25
  61. package/src/adapters/native-proof-ports.ts +10 -0
  62. package/src/adapters/pi.ts +7 -0
  63. package/src/adapters/registry-types.ts +4 -0
  64. package/src/adapters/reviewer-print.ts +1 -0
  65. package/src/broker/protocol.ts +203 -0
  66. package/src/broker/server.ts +314 -0
  67. package/src/broker/store.ts +548 -0
  68. package/src/cli/bench.ts +3 -2
  69. package/src/cli/broker.ts +58 -0
  70. package/src/cli/conductor-recovery.ts +142 -0
  71. package/src/cli/configured-acp-entry.ts +26 -6
  72. package/src/cli/console-state.ts +59 -17
  73. package/src/cli/console.ts +17 -2
  74. package/src/cli/facts-hook.ts +3 -1
  75. package/src/cli/help.ts +28 -16
  76. package/src/cli/identity.ts +23 -10
  77. package/src/cli/init.ts +34 -7
  78. package/src/cli/intake-output.ts +33 -0
  79. package/src/cli/main.ts +314 -71
  80. package/src/cli/output.ts +41 -1
  81. package/src/cli/recovery-package.ts +18 -1
  82. package/src/cli/role-holders.ts +35 -0
  83. package/src/cli/room.ts +304 -0
  84. package/src/cli/say-input.ts +16 -0
  85. package/src/cli/schedule-output.ts +49 -0
  86. package/src/cli/sdlc-output.ts +44 -6
  87. package/src/cli/smoke-pty-bridge.ts +33 -0
  88. package/src/cli/smoke-pty.ts +145 -0
  89. package/src/cli/smoke-run.ts +139 -0
  90. package/src/cli/smoke-target.ts +130 -0
  91. package/src/cli/source-output.ts +29 -0
  92. package/src/cli/task-output.ts +3 -2
  93. package/src/cli/upgrade-runtime.ts +98 -16
  94. package/src/cli/upgrade.ts +66 -7
  95. package/src/hub/ask.ts +76 -12
  96. package/src/hub/board.ts +132 -22
  97. package/src/hub/bus.ts +399 -56
  98. package/src/hub/child-process.ts +2 -0
  99. package/src/hub/ci-watch.ts +162 -0
  100. package/src/hub/claude-failure-stage.ts +85 -0
  101. package/src/hub/claude-launch-proof.ts +67 -2
  102. package/src/hub/claude-shell-wrapper.ts +11 -5
  103. package/src/hub/conductor.ts +17 -4
  104. package/src/hub/config-apply.ts +32 -5
  105. package/src/hub/config-reload.ts +7 -5
  106. package/src/hub/config-trust.ts +8 -1
  107. package/src/hub/context-window.ts +33 -3
  108. package/src/hub/control-client.ts +14 -2
  109. package/src/hub/corpus-config.ts +34 -0
  110. package/src/hub/corpus-evidence.ts +184 -0
  111. package/src/hub/corpus.ts +373 -0
  112. package/src/hub/cost-governance.ts +26 -0
  113. package/src/hub/daemon.ts +1228 -242
  114. package/src/hub/deliverable-files.ts +8 -2
  115. package/src/hub/delivery-journal.ts +123 -17
  116. package/src/hub/dogfood-profile.ts +32 -5
  117. package/src/hub/dogfood-read.ts +6 -2
  118. package/src/hub/envelope.ts +11 -0
  119. package/src/hub/eval-gate.ts +131 -0
  120. package/src/hub/events.ts +1 -1
  121. package/src/hub/external-instance.ts +69 -0
  122. package/src/hub/facts.ts +15 -5
  123. package/src/hub/folder-trigger.ts +223 -0
  124. package/src/hub/hub-tools.ts +2 -1
  125. package/src/hub/inference.ts +20 -5
  126. package/src/hub/instance-config.ts +69 -3
  127. package/src/hub/instance-execution.ts +6 -2
  128. package/src/hub/instances.ts +32 -4
  129. package/src/hub/intake.ts +125 -0
  130. package/src/hub/learning.ts +85 -0
  131. package/src/hub/lifecycle.ts +7 -3
  132. package/src/hub/limits.ts +12 -5
  133. package/src/hub/native-permission-request.ts +77 -0
  134. package/src/hub/operational-authority.ts +63 -5
  135. package/src/hub/operational-settings.ts +76 -0
  136. package/src/hub/operational-view.ts +1 -1
  137. package/src/hub/packs.ts +132 -0
  138. package/src/hub/path-policy.ts +7 -0
  139. package/src/hub/peer-doing.ts +130 -0
  140. package/src/hub/peers.ts +46 -0
  141. package/src/hub/queue-backpressure.ts +58 -0
  142. package/src/hub/read-tools.ts +2 -0
  143. package/src/hub/recovery-authority.ts +151 -0
  144. package/src/hub/recovery-policy.ts +220 -0
  145. package/src/hub/research.ts +24 -14
  146. package/src/hub/restart.ts +7 -0
  147. package/src/hub/routing.ts +54 -11
  148. package/src/hub/schedules.ts +266 -0
  149. package/src/hub/sdlc-merge.ts +38 -14
  150. package/src/hub/sdlc-metrics.ts +32 -10
  151. package/src/hub/sdlc-queue.ts +7 -1
  152. package/src/hub/sdlc-release.ts +1 -1
  153. package/src/hub/sdlc.ts +37 -7
  154. package/src/hub/security-baseline.ts +38 -0
  155. package/src/hub/settings.ts +7 -1
  156. package/src/hub/shipped-packs.ts +8 -0
  157. package/src/hub/snapshots.ts +72 -6
  158. package/src/hub/source-read.ts +160 -0
  159. package/src/hub/sources.ts +311 -0
  160. package/src/hub/task-shell-default.ts +3 -2
  161. package/src/hub/tasks.ts +1047 -115
  162. package/src/hub/workflow.ts +1 -1
  163. package/src/local/deny.ts +21 -0
  164. package/src/local/doc-read.ts +69 -0
  165. package/src/local/proxy.ts +4 -13
  166. package/src/local/sandbox.ts +3 -1
  167. package/src/local/tools.ts +37 -3
  168. package/src/local/web-fetch-config.ts +43 -0
  169. package/src/local/web-fetch.ts +183 -0
  170. package/src/memory/brief.ts +85 -13
  171. package/src/memory/capture.ts +1 -1
  172. package/src/rooms/application.ts +114 -0
  173. package/src/rooms/boundary.ts +103 -0
  174. package/src/rooms/commands.ts +294 -0
  175. package/src/rooms/crypto.ts +371 -0
  176. package/src/rooms/link.ts +584 -0
  177. package/src/rooms/policy.ts +82 -0
  178. package/src/rooms/remote-peer.ts +80 -0
  179. package/src/rooms/service.ts +426 -0
  180. package/src/rooms/status.ts +146 -0
  181. package/src/rooms/store.ts +1210 -0
  182. package/src/rooms/wire.ts +465 -0
  183. package/src/ui/index.html +91 -4
  184. package/templates/packs/example.pack.json +13 -0
@@ -125,9 +125,11 @@ the same retirement decision.
125
125
  relayed to the hub console. Loopback is not enough on its own (any web page can open a
126
126
  WebSocket to 127.0.0.1), so the control WS requires a per-run token and both hub
127
127
  servers refuse requests that carry an `Origin` header (amended in M1).
128
- 9. Single machine, one hub daemon per project directory. Cross-machine broker is out.
129
- (Amended in #258: hub memory is replicated between a person's machines through a git
130
- remote; that is memory replication, not a broker, and the bus stays single-machine.)
128
+ 9. One hub daemon per project directory, with a local bus. Amended by #446:
129
+ opt-in encrypted rooms may connect hubs through an untrusted broker; the room
130
+ boundary does not federate their boards, roles, approvals or memory. X1 defines
131
+ crypto and wire primitives only. X2 supplies a standalone broker; the hub link
132
+ and local delivery integration remain X3/X4. Memory replication through git (#258) remains a separate mechanism.
131
133
  10. Shared memory reuses claude-mem as the single store; the hub never runs its own
132
134
  memory database. (Amended in #258: the hub owns a long-term store and its graph,
133
135
  `docs/memory.md`; claude-mem stays the optional capture and session-recall source.) Native capture stays where it exists (Claude hooks, Codex plugin,
@@ -706,7 +708,8 @@ M6 internal inference, packaging
706
708
 
707
709
  ## Out of scope
708
710
 
709
- - Cross-machine broker or rooms, Windows. The web UI was outside the original six milestones; issue #6 adds the local dashboard described below.
711
+ - Windows. Cross-machine rooms are an opt-in extension under #446, described below. The web UI was outside the original six milestones; issue #6 adds the local dashboard described below.
712
+ - Room v1 excludes hosted multi-tenant service, multi-owner rooms, forward secrecy, traffic-metadata hiding, approval or permission-request relay, and cross-machine instances. Shared boards, remote roles and remote task ownership remain excluded; memory crosses only through the separate agent-mem git sync. No hub server is exposed.
710
713
  - OmniRoute daemon on the Mac; pooling subscription OAuth through any gateway.
711
714
  - Gemini CLI and OpenCode adapters beyond the ACP fallback note.
712
715
  - Switchyard `auto` route until v0.3.0 ships.
@@ -715,6 +718,724 @@ M6 internal inference, packaging
715
718
  - A hub-owned memory store, cmem.ai cloud sync, automatic corpus building (a project
716
719
  corpus for `ahub ask` is an optional M6 item), and any change to the vault LEARN loop.
717
720
 
721
+ ## Encrypted rooms: X1 crypto and wire contract (#446)
722
+
723
+ This amends decision 9 and the original cross-machine exclusion. The [frozen
724
+ D1-D5 design](https://github.com/STAIxBWLB/agent-hub/issues/446#issuecomment-6242775004),
725
+ [person's O1-O4 answers](https://github.com/STAIxBWLB/agent-hub/issues/446#issuecomment-6242885199)
726
+ and [implementation go-ahead](https://github.com/STAIxBWLB/agent-hub/issues/446#issuecomment-6391889660)
727
+ are binding: a self-hosted zero-dependency broker, person-only owner-hub invitations
728
+ and revocations, no task notices in v1, and remote priority capped at `status`.
729
+ X1 adds source modules, with no listeners, CLI, daemon startup imports or room delivery.
730
+ X2 supplies the broker; X3 supplies durable state/link/enrollment integration; X4
731
+ supplies message boundary enforcement; X5-X7 supply status, invocation and the
732
+ person's two-machine live receipt. None of those later proofs follows from X1 tests.
733
+
734
+ ### Algorithms and local identity
735
+
736
+ `src/rooms/crypto.ts` uses built-in WebCrypto only: Ed25519 signatures, X25519 key
737
+ agreement, HKDF-SHA-256, AES-256-GCM with a 128-bit tag, and HMAC-SHA-256. Definitions
738
+ follow [WebCrypto](https://www.w3.org/TR/webcrypto-2/), [RFC 7748](https://www.rfc-editor.org/rfc/rfc7748)
739
+ and [RFC 5869](https://www.rfc-editor.org/rfc/rfc5869). `ROOM_MIN_BUN_VERSION` is
740
+ `1.4.2`, also the repository's Bun floor. `assertRoomCryptoReady()` caches a
741
+ fail-closed version/capability self-test, including known-answer and negative
742
+ checks for every primitive. Every asynchronous crypto/wire operation awaits it;
743
+ X3 must also await it before opening any room link. A failure disables rooms,
744
+ without changing local hub operation. Supporting a version requires both the
745
+ version floor and the actual self-test, not a version-string assertion alone.
746
+
747
+ A hub has separate signing and agreement key pairs. Its fingerprint is lowercase
748
+ SHA-256 hex of its raw Ed25519 public key. Both public keys and the fingerprint
749
+ are authenticated together by joins, wraps and rosters. Private runtime handles
750
+ are opaque WeakMap-backed identities; explicit `exportHubIdentityForStorage` and
751
+ `exportEpochKeyForStorage` are machine-local persistence boundaries, never wire
752
+ fields or diagnostics. X3 must store them in a private 0700 directory and 0600
753
+ files. A same-user process can steal those files; pinning is not a sandbox.
754
+
755
+ A random 256-bit epoch root is wrapped separately for each member using a fresh
756
+ X25519 ephemeral pair, 32-byte HKDF salt and 12-byte GCM nonce. HKDF info is SHA-256 of the full
757
+ versioned, domain-separated derivation transcript, keeping provider input bounded
758
+ without dropping context; GCM AAD and signatures still bind the full transcript. The owner's
759
+ signature binds the full owner/recipient public-key tuples, room/epoch/membership
760
+ context, ephemeral key, salt, nonce and ciphertext. Zero shared secrets refuse.
761
+ Every roster change, including label or exposure changes, advances the epoch and
762
+ creates a fresh root. A removed member receives no new wrap. This has no forward
763
+ secrecy against later theft of a member's long-term agreement key and recorded wraps.
764
+
765
+ ### Wire v1 and privacy
766
+
767
+ `src/rooms/wire.ts` defines strict canonical UTF-8 JSON: recursively sorted ASCII
768
+ object keys, exact known fields, safe integer timestamps, positive canonical
769
+ uint64 decimal strings for epochs/sequences, and unpadded canonical base64url.
770
+ Decoders reject unknown/duplicate fields, whitespace or alternative encodings,
771
+ invalid UTF-8, unsorted/duplicate collections and exceeded ceilings. Canonical
772
+ bytes are the signed/hashed transcript; signatures use separate versioned domains
773
+ for frames, rosters, wraps and joins. IDs/labels are bounded ASCII identifiers,
774
+ never arbitrary display text. Errors contain fixed codes, no input or key material.
775
+
776
+ | Record or collection | v1 ceiling |
777
+ | --- | --- |
778
+ | Plain frame payload | 65,536 bytes |
779
+ | Encoded frame | 131,072 bytes |
780
+ | Encoded roster | 524,288 bytes |
781
+ | Invite or join | 16,384 bytes |
782
+ | Members, frame recipients, exposed local peer ids per member | 64 each |
783
+ | Local label or exposed peer id | 32 characters |
784
+ | Broker URL | 2,048 characters |
785
+
786
+ These are protocol ceilings, not measured capacity claims. Raising one requires a
787
+ versioned contract review. Broker URLs are canonical `wss:` without credentials or
788
+ fragments; `ws:` is permitted only for literal `127.0.0.1` or `[::1]` testing.
789
+
790
+ The signed roster has public room/epoch/owner, member fingerprints and public keys,
791
+ wraps, and an encrypted private roster. The encrypted portion contains the matching
792
+ member tuples, unique local labels and exposed peer ids. This resolves the freeze's
793
+ signed-roster requirement without exposing labels or peer ids to the broker. All
794
+ public and encrypted components are covered by the owner signature. A receiver
795
+ pins room, owner fingerprint and its persisted minimum epoch. Initial enrollment
796
+ uses `minEpoch="0"`; positive values name an installed epoch, whose exact digest
797
+ is mandatory when accepting that same epoch again. Missing or mismatched digests
798
+ refuse even an otherwise valid owner-signed retry. It verifies the signature, its exact key tuple, wrap,
799
+ private roster and identical membership before accepting a candidate. X3 must
800
+ atomically install that candidate against its persisted pin. Replaying an old
801
+ candidate must never reset the minimum epoch or a grace deadline.
802
+
803
+ A frame's public header is `{v,type,room,epoch,sender,recipients,seq}` plus nonce,
804
+ ciphertext and signature. Recipients are hub fingerprints, not local peer ids.
805
+ The body is opaque encrypted bytes, including any eventual local addressing,
806
+ kind, trace and text. Frame AAD is exactly UTF-8 `room|epoch|sender|seq`. A
807
+ sender-specific AES key is HKDF-derived from the epoch root and room/epoch/sender/
808
+ roster digest context. Every encryption attempt draws a fresh random 96-bit nonce
809
+ from WebCrypto, independent of the sequence counter. A counter rollback therefore
810
+ does not deterministically reuse a GCM nonce; per-sender subkeys also separate
811
+ senders. Random-nonce collision bounds and a functioning runtime RNG remain
812
+ cryptographic assumptions. The sender's
813
+ signature additionally binds recipients, nonce, ciphertext and all header fields.
814
+ X1 encryption authenticates bytes; X4 must still screen what those bytes may mean
815
+ before any local bus delivery, including untrusted framing and the `status` ceiling.
816
+
817
+ ### Required durable host ports and epoch settlement
818
+
819
+ There is no production in-memory fallback. `sealFrame` requires `SequencePort`:
820
+ `reserve` durably consumes a strictly increasing room/sender counter that survives
821
+ restart and epoch rotation, checking the installed roster digest. `isCurrent` is
822
+ a final synchronous fence after crypto; X3 must additionally fence durable outbox
823
+ publication. Failed attempts consume sequence numbers; each encryption attempt draws a fresh random nonce. Counter durability still protects replay/order state, rather than determining encryption nonces.
824
+
825
+ `openFrame` verifies current sender/recipient membership, signature, epoch, nonce
826
+ length and GCM authentication before invoking `ReplayPort.accept`. In one transaction that
827
+ port must recheck current and selected roster digests, current membership and its
828
+ own local clock/grace deadline, compare the room/sender high-water mark, and commit
829
+ accepted digest plus inbox delivery before release. Only `accepted` returns
830
+ plaintext. `duplicate` means an identical previously accepted frame digest and
831
+ returns no plaintext; an unknown lower sequence is `stale`, never a completed ACK.
832
+ D5 receipts may be resent only for that stored identical digest. Gaps are recorded,
833
+ not treated as proof that missing messages arrived. Callback failure refuses.
834
+
835
+ Only the immediately preceding epoch may have a persisted 60-second local grace
836
+ deadline. X3 must derive and persist that deadline as the local successful rotation
837
+ installation time plus exactly 60,000 ms, never from caller or remote expiry input. Both sender and receiver must still be current members with unchanged
838
+ keys; revocation therefore refuses immediately, including during grace. The port
839
+ must refuse after expiry even if crypto crossed the deadline, and restarts or
840
+ replayed rosters must not extend it. X1's optional `previous` and `nowMs` parameters
841
+ are trusted-host inputs/test clocks, not remote authority. No caller can substitute
842
+ for X3's durable atomic installation, clock and publication fences.
843
+
844
+ An out-of-band invite carries room, broker, pinned owner fingerprint, expiry,
845
+ random invite id and random 256-bit one-time secret. Never send it to the broker or
846
+ log it. A join exposes only its member public-key tuple and encrypted private
847
+ label/exposure data. HMAC binds the full invitation claims and signed request;
848
+ the member signature binds the same claims and request. Before consuming the
849
+ invite, a trial X25519 agreement rejects unusable/low-order member public keys.
850
+ This checks key usability, without claiming a separate private-key possession
851
+ proof. A broker cannot replace keys or repurpose a request under a changed invitation. Owner expiry is exact;
852
+ a joiner's preflight allows at most 10 minutes of clock skew, which grants no
853
+ owner-side extension. `EnrollmentPort.consume` must compare the original stored
854
+ invite digest and expiry, consume once, and persist identity/request outcome with
855
+ membership publication. Only the exact same request may be a duplicate. Owner-hub
856
+ person authorization and OOB secret handling remain X3 integration obligations.
857
+
858
+ ### Acceptance and limits
859
+
860
+ Real built-in crypto tests cover vectors, private/public binding, wrap tampering,
861
+ wrong recipient/context, revoked old-key failure, domain separation and sanitized
862
+ errors. Wire fixtures cover canonical bounds, owner pins, metadata hiding,
863
+ unsigned/tampered frames, replay/concurrent duplicates, stale sequences, epoch
864
+ rotation, current-member grace, invite substitution/expiry/one-time consumption
865
+ and refused state ports. Memory fake ports prove API ordering only, not durability
866
+ or restart safety. Full gate and exact-head CI are separate from native/account or
867
+ two-machine receipts. No room transport, real invite, remote delivery, production
868
+ storage permission, crash-restart replay or X7 receipt is verified by this slice.
869
+
870
+ ## Encrypted rooms: X2 blind broker (#446)
871
+
872
+ The frozen O1 is self-hosted, zero-dependency `ahub broker serve`. X2 adds a separate
873
+ Bun HTTP/WebSocket process. The hub daemon, local bus, board, roles, approvals and
874
+ memory are untouched. X3 supplies the hub link, person-only room operations and
875
+ one-time enrollment; X4 supplies framing/privacy/priority enforcement. X2 auth
876
+ proves a hub signing identity against membership, not a remote person's consent.
877
+ There is no invitation-secret or pre-enrollment endpoint here: X3 must supply that
878
+ control path before an invited hub opens its member data connection.
879
+
880
+ ### Process and public verification
881
+
882
+ The command is `ahub broker serve [--hostname <host>] [--port <0..65535>]
883
+ [--public-url <ws[s]://host/>] [--owner <fingerprint>]... [--state-dir <dir>]`. Agent markers and `--live-test`
884
+ delegation refuse before importing the command. Help/parser/audit labels agree;
885
+ audit labels never contain path operands. Default bind is `127.0.0.1:8788`; port 0
886
+ asks the OS for an ephemeral fixture port. Default state is `broker/` beneath the
887
+ selected hub state directory, with an explicit machine-local override available.
888
+
889
+ The reference listener uses HTTP/WS behind operator-managed TLS termination.
890
+ Public audience is a canonical root URL without credentials, query or fragment;
891
+ `ws:` is loopback-only, and a non-loopback bind requires an explicit WSS audience.
892
+ Proxy `/v1/rooms/<room>` as binary WebSocket and preserve `Origin`. Every HTTP
893
+ request/upgrade carrying any Origin header, even empty, refuses. `GET /health`
894
+ returns fixed version/status only. Audience comes from startup config, never Host.
895
+ No proxy, certificate or firewall is configured by this command, and fixture
896
+ success is not a live TLS receipt.
897
+
898
+ `--owner` is a repeatable exact signing-fingerprint allowlist for first-roster
899
+ admission. Public WSS audiences (including loopback behind a proxy) and non-loopback
900
+ binds require a nonempty allowlist before storage/listener effects. Other owners
901
+ cannot consume a room slot or squat a room id. Existing room membership remains
902
+ pinned and is not revoked merely by changing this creation allowlist. Loopback with
903
+ no public audience may omit it for development; that open mode is a ceiling, not a
904
+ public service configuration. Do not expose open mode through a proxy.
905
+
906
+ `startBroker` awaits X1's minimum-Bun/capability self-test before database/listener
907
+ effects. `verifyPublicRoster` and `verifyPublicFrame` share X1's canonical signature
908
+ and pin checks. Their immutable WeakMap-issued products contain public ids, keys,
909
+ epoch/digest metadata only; byte accessors copy their storage, Buffer included.
910
+ They carry no epoch root, private identity, plaintext labels/exposures or body.
911
+ The broker never unwraps a key or opens ciphertext.
912
+
913
+ ### Socket protocol
914
+
915
+ Input is binary canonical UTF-8 JSON: control cap 1 MiB, challenge cap 8 KiB, with
916
+ X1's smaller roster/frame caps still enforced. Unknown fields, alternate encoding,
917
+ text messages and invalid signatures refuse with fixed codes, no reflected input.
918
+
919
+ 1. `/v1/rooms/<room>` emits `{v:1,type:"challenge",room,broker,nonce,expiresAt,rosterDigest}`.
920
+ Nonce is 32 random bytes; deadline is local issue time + 10 seconds. Digest is
921
+ current pinned roster digest, or null for a new room.
922
+ 2. The hub replies `{v:1,type:"auth",member,roster,signature}`. New rooms require
923
+ base64url owner-signed roster bytes and the owner's private-key proof. Existing
924
+ rooms require `roster:null` and a signer in the current pinned membership.
925
+ A non-owner cannot bootstrap a former member's old roster into a fresh room.
926
+ 3. The Ed25519 transcript is canonical bytes of
927
+ `["agent-hub.rooms.broker.v1","auth",challenge,member,initialRosterDigestOrNull]`.
928
+ The existing digest is already in the challenge; the final array item is null
929
+ for existing rooms. The client builder requires trusted expected broker URL and
930
+ room. Thus audience, room, socket nonce, deadline, digest and member are bound.
931
+ The server consumes its one-use challenge before await, then rechecks expiry,
932
+ current membership/digest and socket generation before effects.
933
+ 4. The broker sends current signed roster bytes and
934
+ `{v:1,type:"ready",room,member,epoch,digest}`. The client verifies the roster and
935
+ sends `{v:1,type:"roster_ready",digest}` before queue delivery. This ACK is flow
936
+ control, not proof of epoch-key possession. New rosters require a fresh ACK;
937
+ stale ACKs cannot restore authorization. One socket per room/hub is current;
938
+ replacement closes the older socket and fences its awaited work.
939
+
940
+ Only an authenticated owner publishes newer X1 rosters. Owner fingerprint stays
941
+ pinned; older epochs and same-epoch digest changes refuse. Exact replays are
942
+ idempotent. An adjacent predecessor alone has grace: persisted successful local
943
+ installation time + 60,000 ms. Epoch gaps get no predecessor grace. Replays and
944
+ restart never extend the deadline. Removed/key-changed members disconnect; their
945
+ sender floors and inappropriate queued recipients are purged. During grace only
946
+ still-current sender/receiver keys authorize old frames.
947
+
948
+ ### Bounded ciphertext queue
949
+
950
+ A raw X1 frame must match its authenticated socket's sender. After signature awaits,
951
+ the latest socket generation/current membership is rechecked. Store admission
952
+ again checks selected current/previous digest, grace clock, replay floor and caps
953
+ inside a synchronous transaction before writes. No unauthenticated frame persists
954
+ or fans out. Control and frame processing is serialized per socket, so ACKs may
955
+ wait behind an outgoing signature check without racing authentication state.
956
+
957
+ | V1 ceiling | Value |
958
+ | --- | --- |
959
+ | Rooms | 64 |
960
+ | Open sockets / concurrent handlers | 128 / 16 |
961
+ | Pending input per socket | 8 packets, 2 MiB |
962
+ | Pending input globally | 32 MiB |
963
+ | Queued packets per room / globally | 200 / 1,000 |
964
+ | Queued packet bytes per room / globally | 4 MiB / 32 MiB |
965
+ | Queue TTL | 24 hours |
966
+ | Per-socket native backpressure limit | 1 MiB plus the final message allowance |
967
+ | Roster ACK deadline | 30 seconds |
968
+
969
+ Storage holds original sealed wire bytes plus public recipient fingerprints. It
970
+ commits before `{type:"stored",digest,seq,duplicate}`. Sender sequence/digest floors
971
+ survive queue ACKs/restart: the last accepted digest or an identical retained older
972
+ packet is idempotent without requeueing; conflicting or
973
+ unknown lower sequences refuse. FIFO uses SQLite admission row order, never lexical
974
+ sequence strings or clock ties. Full queues refuse new packets without evicting
975
+ accepted ones. Prior-epoch grace may expire a packet before its ordinary TTL.
976
+
977
+ One packet per recipient socket is in flight. `{v:1,type:"received",digest}` removes
978
+ only that identity from the packet's remaining recipients, then returns
979
+ `{type:"queue_ack",digest}` and pumps the next packet. Unknown ACKs change nothing.
980
+ These are transport/storage ACKs, not D5's signed recipient application receipt;
981
+ X3 must handle completion and uncertainty in its journal. Disconnect/backpressure
982
+ retains only bounded ciphertext. Revocation, replacement, expiry and shutdown fence
983
+ pending work before storage or fan-out.
984
+
985
+ The versioned built-in SQLite store uses DELETE journal and EXTRA synchronous
986
+ commits, a private 0700 directory and 0600 database. Exclusive mode retains kernel
987
+ ownership until close; second processes fail closed, and crash releases ownership
988
+ without stale-lock unlink. Existing empty, malformed or unsupported state refuses,
989
+ never silently initializes new pins. Restore validates signed rosters/frames,
990
+ metadata and floors before delivery; expired grace is normal cleanup. See
991
+ [SQLite locking mode](https://www.sqlite.org/pragma.html#pragma_locking_mode) and
992
+ [exclusive access](https://www.sqlite.org/atomiccommit.html#exclusive_access_mode).
993
+ Native buffering includes the [Bun final-message allowance](https://bun.com/docs/runtime/http/websockets).
994
+
995
+ Schema v2 stores each queued packet's epoch as indexed metadata and has an expiry
996
+ index. Known v1 state migrates in place, retaining pins, floors, ciphertext and
997
+ admission order. Ordinary current lookups are pure; next/ACK use targeted metadata
998
+ expiry and only read the selected ciphertext, while timer/admission sweeps expire
999
+ indexed rows and bounded room pins without parsing every packet. Rotation inspects
1000
+ only the changed room's authenticated metadata. Newly admitted packets obey the
1001
+ 4 MiB per-room share; existing valid larger queues are grandfathered and block new
1002
+ admission until ACK/expiry reduces them, never evicted to retrofit the share.
1003
+
1004
+ Existing populated directories are accepted only as private recognized broker
1005
+ state; unrelated files or non-0700 mode refuse without changing contents or mode.
1006
+ Empty directories may be tightened to 0700. Offline operations require an existing
1007
+ database and never create a new store to repair a misspelled path.
1008
+
1009
+ `ahub broker room drop <room> --yes [--state-dir <dir>]` is person-only, including
1010
+ under `--live-test`, and requires the broker stopped: the same exclusive database
1011
+ lock refuses an online attempt. It transactionally forgets only the named room's
1012
+ pins, sender floors and queued packets, freeing its slot; other rooms stay intact.
1013
+ This is an explicit broker-side room reset, not member revocation, secure erase,
1014
+ or an application-delivery receipt. Clients retain their own pins/replay state.
1015
+ Remove an unwanted owner from the creation allowlist before restarting, otherwise
1016
+ that still-approved owner may bootstrap the dropped id again. Use a fresh room
1017
+ lifecycle rather than deleting the whole database to regain capacity.
1018
+
1019
+ Logs/stats contain fixed event/error codes, public ids, counts and byte totals.
1020
+ Canary fixtures separate the encrypting client from the broker process and inspect
1021
+ transmitted bytes, retained ciphertext, database and logs. Only clients open the
1022
+ body. This is an input/retained-state invariant, not a full runtime heap-dump
1023
+ receipt or a claim that malicious input cannot deliberately contain plaintext.
1024
+ Traffic analysis, availability attacks, malicious owners, same-user compromise,
1025
+ hosted multi-tenant accounts and forward secrecy remain frozen exclusions.
1026
+ X3/X4 integration and live TLS/account/X7 two-machine receipts remain separate.
1027
+
1028
+
1029
+ ## Encrypted rooms: X3 outbound membership and presence (#446)
1030
+
1031
+ X3 adds a machine-local `RoomStore`, an outbound `RoomLink` and person-only room
1032
+ administration. The broker remains the X2 blind relay. No listener, remote bus
1033
+ peer, chat egress, task sharing, permission relay or cloud/model call is added.
1034
+ X4 still owns envelope screening, remote identity delivery, signed application
1035
+ receipts and the application journal's completion/uncertainty lifecycle. X5 owns
1036
+ room dashboard integration; X7 owns the person's two-machine live receipt.
1037
+
1038
+ ### Person commands and out-of-band enrollment
1039
+
1040
+ The person runs these commands in an ordinary terminal, with a running local hub:
1041
+
1042
+ - `ahub room create <name> --broker <ws[s]://host/> [--label <id>]`
1043
+ - `ahub room invite <name>`
1044
+ - `ahub room join <token> [--name <name>] [--label <id>]`
1045
+ - `ahub room invite <name> --accept <request>`
1046
+ - `ahub room join <name> --roster <signed-roster>`
1047
+ - `ahub room leave <name>`
1048
+ - `ahub room revoke <name> <fingerprint>`
1049
+ - `ahub room expose <name> [<peer>...]`
1050
+ - `ahub room list [--json]` and `ahub room status [<name>] [--json]`
1051
+
1052
+ Names, labels and exposed ids are bounded ASCII identifiers. The label defaults
1053
+ to `hub-<fp8>`, never a hostname. Broker audiences are canonical root WSS URLs;
1054
+ plain WS is allowed only for literal loopback. Read forms publish only local room
1055
+ names, room ids, owner/member fingerprints, epochs and link states. Agent shells
1056
+ may read them. All mutations refuse agent markers and `--live-test`, including
1057
+ conductor delegation. Daemon mutations require a console connection, and every
1058
+ operation, unknown or refused included, answers its `rid`.
1059
+
1060
+ A mutation first prepares a connection-bound, one-use, 30-second ticket. The CLI
1061
+ prints both owner and member fingerprints before executing it. The person must
1062
+ compare them and explicitly confirm: a TTY asks y/N (default No), and a non-TTY
1063
+ requires `--confirm <member-fingerprint-prefix>` with 16..64 matching lowercase hex
1064
+ characters. No, EOF, missing or mismatched confirmation executes nothing. The
1065
+ daemon also matches confirmation to the ticket's member fingerprint. The ticket binds
1066
+ the captured action and arguments; execute accepts no replacement action/input.
1067
+ Socket ownership, shutdown and recovery holds are rechecked after crypto awaits
1068
+ and inside the final state transaction. Preview does not install a room or consume
1069
+ an invitation. Creating a private local identity is not room enrollment.
1070
+
1071
+ The [X3 operand ruling](https://github.com/STAIxBWLB/agent-hub/issues/446) keeps
1072
+ pre-enrollment out of the broker:
1073
+
1074
+ - The owner creates the room and prints an invitation with `invite <name>`. Only
1075
+ this invitation contains the one-time secret. Share it privately out of band,
1076
+ never with the broker, repository, shared logs or an agent.
1077
+ - The joining person runs `join <token>` and returns the printed signed/HMAC'd
1078
+ request to the owner out of band. It contains neither the invitation secret nor
1079
+ any private key. The pending invitation pins room, broker and owner locally.
1080
+ - The owner runs `invite <name> --accept <request>`. HMAC, signature, member keys,
1081
+ expiry and the invitation's original digest are verified before one transaction
1082
+ consumes it and installs the next signed roster. The committed roster is the
1083
+ recoverable publication; the outbound link sends those same persisted bytes.
1084
+ An identical accepted retry does not consume or rotate again. Changed requests
1085
+ refuse. Owner expiry is exact; joiner preflight allows the X1 ten-minute skew.
1086
+ - The joining person imports the returned roster with `join <name> --roster ...`.
1087
+ Owner authentication comes from its own pending invitation, never from the
1088
+ broker or the offered roster itself. Exact own signing/agreement keys must be
1089
+ included. Subsequent explicit roster imports use the retained owner/epoch pin.
1090
+ - A retry after saving a pending request returns the identical saved request when
1091
+ token, label and exposure selection agree, including after a lost CLI reply.
1092
+ A changed pending selection refuses. `leave` cancels a pending enrollment so the
1093
+ person can retry without editing private state. Expired pending entries are
1094
+ bounded and pruned on new admission.
1095
+
1096
+ A non-owner's `expose` prints a signed exposure request, bound to its installed
1097
+ room, owner and roster digest. The member sends it out of band; the owner accepts
1098
+ it with the same `invite --accept` form and returns the next signed roster. It
1099
+ cannot change another member's exposure or be replayed after rotation. A later
1100
+ slice may fetch an already signed roster from the broker and remove the manual
1101
+ `--roster` step; this slice does not add that endpoint or trust the broker to enroll.
1102
+
1103
+ `rooms.<name>.expose` lives only in an untracked `.agenthub/config.local.json`.
1104
+ Shared/committed configuration and generic settings cannot enable it. The live
1105
+ local policy and the verified roster are intersected before any presence is sent;
1106
+ loss of config trust removes exposure. Writable local policy is checked before
1107
+ the owner's own exposure rotation, not only when saving the policy afterward. Empty exposure is the default. Room keys,
1108
+ broker binding, pending invites and membership are private machine-local state,
1109
+ not project config or synced memory. A hub stop closes links and retains membership.
1110
+ `leave` disconnects locally and retains owner/epoch/replay pins; an owner uses
1111
+ `revoke` to remove a former member from the signed room and rekey other members.
1112
+ A person may explicitly import a pinned signed roster to reactivate a left room;
1113
+ automatic link traffic cannot reactivate it.
1114
+
1115
+ ### Durable state and outbound link
1116
+
1117
+ `<hub-state>/rooms/rooms.db` is schema version 1, SQLite DELETE/EXTRA with exclusive
1118
+ process ownership and a 1 ms busy bound. Directory mode is 0700 and database mode
1119
+ 0600. Symlinks, hardlinked files, unrelated contents, unknown schema, corrupt or
1120
+ unverifiable existing pins refuse, never initialize a new identity over old state.
1121
+ Private keys remain opaque at runtime; only X1's explicit local storage codec is
1122
+ used. This protects against other users, not a hostile same-user process.
1123
+
1124
+ Epoch installation compares the captured current digest in a transaction, refuses
1125
+ rollback and same-epoch digest changes, and preserves only the adjacent predecessor
1126
+ for local successful install time + exactly 60,000 ms. Restart/replayed rosters do
1127
+ not extend grace. A persisted local clock high-water refuses rollback. Sequence
1128
+ reservation commits before sealing and never resets on rotation. Replay atomically
1129
+ checks current membership/keys, installed digests, local grace clock and sender
1130
+ high-water, and persists the accepted digest plus inbox before returning plaintext.
1131
+ Identical retained receipts may be duplicate; unseen lower sequences stay stale.
1132
+ Gaps are recorded. Inbox bodies are removed after validated presence observation;
1133
+ bounded digest receipts remain for duplicate handling.
1134
+
1135
+ Limits are 64 rooms, 128 invitations, 64 pending joins, 200 pending inbox records
1136
+ per room, 16 MiB aggregate inbox bytes, 1,000 retained digest receipts per room,
1137
+ 256 retained historical sender floors per room (new senders refuse at the cap),
1138
+ and host outbox ports capped at 200 records per room/16 MiB aggregate. Outbox broker
1139
+ storage is only `accepted`, never application `completed`. X3 presence is ephemeral
1140
+ and uses durable sequence/replay without filling the application outbox. Signed
1141
+ application completion, nack and needs-review expiry are activated with X4 delivery.
1142
+
1143
+ Every link awaits the crypto self-test before dialing out. Authentication binds
1144
+ its configured audience, room, broker nonce and membership digest. Current
1145
+ owner/epoch pins are verified before roster ACK; an owner may republish a locally
1146
+ newer roster after offline rotation without downgrading its pin. Non-owners never
1147
+ bootstrap an empty broker room. Input is binary canonical JSON, serialized with
1148
+ at most 8 pending packets/2 MiB per link, 1 MiB packet/backpressure limits and a
1149
+ 30-second handshake bound. Disconnects use jittered reconnect capped at five
1150
+ minutes and do not block local work. Stop, leave, replacement and rotation fence
1151
+ all awaited work by link generation.
1152
+
1153
+ Presence is an encrypted, signed frame containing only exposed peer ids and closed
1154
+ idle/busy/paused/offline states. It is sent on connection and coalesced peer-state
1155
+ changes, never titles, bodies, private task context or credentials. Unknown payload
1156
+ types and unexposed ids refuse before replay commit. Broker ACKs are transport
1157
+ facts only. A canonical room/recipient-bound frame that is refused or stale is
1158
+ recorded in a durable metadata-only drop ledger before transport retirement ACK.
1159
+ This does not accept plaintext, advance a replay floor or assert application
1160
+ completion. Fixed refusal codes, digest and local timestamp are retained, capped
1161
+ at 1,000 diagnostics per room; eviction removes only old diagnostic rows, never
1162
+ cryptographic replay state. Wrong/missing session or membership authority and a
1163
+ failed durable record produce no ACK. Noncanonical/foreign-room packets still
1164
+ refuse without a transport ACK. Later valid packets are not blocked behind a
1165
+ retired refused packet. A failed room state or crypto initialization disables rooms and leaves
1166
+ local hub work available. Fake-hub/broker tests are not a live TLS/proxy or X7 receipt.
1167
+
1168
+
1169
+ ## Encrypted rooms: X4 screened chat and application receipts (#446)
1170
+
1171
+ This implements frozen D3/D5 from issue #446, comment 6242775004, with the
1172
+ approved X4 refinements and R1 clarifications. X5 metadata views are described
1173
+ below; X6 invocations and X7 live two-machine receipts remain separate.
1174
+
1175
+ ### Egress and authority
1176
+
1177
+ Each signed-roster remote id is a host-owned `RemotePeer`, outside the native
1178
+ adapter catalog. Its `deliver` calls `roomBoundary` once to build the outbound
1179
+ view. Only explicitly addressed `chat` and `status` from an exposed local peer
1180
+ or the local console may cross. The boundary refuses hub/digest/remote senders,
1181
+ workflow kinds, private envelopes, a private sender turn, and PII or unknown task
1182
+ references. Private-turn/task provenance is captured at enqueue and retained
1183
+ while offline. An unknown authority check refuses; it never becomes a grant.
1184
+
1185
+ The screen reuses memory promotion's secrets, PII patterns and private-path
1186
+ rules. When the model screen is enabled, the existing device/campus-only screen
1187
+ owns model admission. Failure or an unknown verdict sends a fixed ids-only
1188
+ withheld stub. Refs are removed. Immutable host proofs recheck current roster,
1189
+ exposure, privacy and policy after awaits and before publication. No room payload selects callbacks, native effects or provider credentials. The
1190
+ host's existing fixed-model configuration owns model choice.
1191
+
1192
+ X4 accepts at most **one remote target per send**. More than one is refused before
1193
+ any local or remote effect, naming the targets and directing separate sends.
1194
+ One remote target plus local targets retains local fan-out. Multi-remote support
1195
+ needs target-derived stable wire ids and per-delivery cap accounting. Implicit
1196
+ broadcasts never include remote peers. Remote delivery has one original, no
1197
+ preface, no condensation and no memory observation. Incoming remote bodies are
1198
+ also excluded from condensation/memory; emitted events and logs contain ids and
1199
+ fixed stubs. Native/role/board/control/recovery actor paths refuse ids containing
1200
+ `@`; a remote console sender is `user@label`, never the local `user` authority.
1201
+
1202
+ The private roster binds labels to fingerprints. Ambiguous global `peer@label`
1203
+ addresses across rooms are inert. Queue recipient bindings survive active and
1204
+ uncertain journal rows; neither a restart nor a person retry may substitute a
1205
+ new fingerprint reusing the label. Unknown legacy bindings become uncertainty,
1206
+ not newly inferred identity. Native peer delivery requires a current exposed
1207
+ local native target. On an X4-only receiver, a reply to `user@label`
1208
+ receives a signed `failed_safe/person_inbox_unsupported` NACK with the fixed
1209
+ reason "person inbox not supported yet (X5)"; no person queue or admission is
1210
+ invented. X5b below adds durable person admission; X5 metadata views alone do not.
1211
+
1212
+ ### Encrypted application wire
1213
+
1214
+ X1 frames carry canonical version-1 `chat` or `receipt` payloads. Chat carries
1215
+ id, trace, hop, local from/to ids, chat/status kind, capped priority, body and the
1216
+ sender-claimed timestamp. Receipt carries the exact original frame digest,
1217
+ application id, destination peer, closed outcome and fixed reason (including the explicit unsupported-person-inbox NACK). Unknown
1218
+ fields or kinds refuse. Only the expected recipient hub's authenticated receipt
1219
+ can settle an outgoing application. Receipt frames produce no application
1220
+ receipt of their own. Incoming ids are fully validated before framing, bodies
1221
+ use `sanitize`, priority is at most status, and sender/hub rate admission shares
1222
+ one atomic limiter decision.
1223
+
1224
+ The first presence on a new link session may carry the optional encrypted,
1225
+ signed boolean `requestReply: true`; absence means false. An accepted request
1226
+ elicits one ordinary presence without that request, never a reply loop. This
1227
+ refreshes discovery after a restart while the other hub's peers remain idle,
1228
+ without a periodic heartbeat. A transient disconnect retains the last signed
1229
+ peer states, but delivery remains gated on the current authenticated link and
1230
+ roster. A roster change invalidates the cached states.
1231
+
1232
+ Ciphertext publication requires the ready socket's verified roster to match the
1233
+ local installed roster. While a verified owner upgrades an older broker roster,
1234
+ one canonical same-room, own-recipient ciphertext may wait inside the existing
1235
+ packet/byte bounds while roster controls continue. It is not decrypted, replayed
1236
+ or ACKed before ready for the current roster. Generic premature or second
1237
+ deferred frames refuse.
1238
+
1239
+ ### D5 settlement and bounded retention
1240
+
1241
+ `completed` means the remote hub durably admitted **one screened chat to its
1242
+ exposed local peer's queue**. It does not mean native or model execution. Person
1243
+ views say **completed (admitted)**. Broker `stored` means accepted ciphertext
1244
+ only; a transport retirement ACK is not application completion.
1245
+
1246
+ Schema3 of the private `rooms.db` migrates schema1/2 without replacing identity,
1247
+ pins, counters, floors or retained traffic. A receiver commits its crypto inbox,
1248
+ then application `pending -> dispatching` before publishing into the separate
1249
+ Bus journal. A crash or failure in that cross-DB publication window becomes
1250
+ `needs_review`, with no automatic republish or fabricated receipt. Sender
1251
+ `dispatching`/`accepted` rows become `needs_review` at restart. A same-process
1252
+ reconnect drains never-handed offline queues once. A late signed receipt is kept
1253
+ as evidence on `needs_review` and shown with the person's disposition view; it
1254
+ never auto-settles. Known safe refusals retain the Bus's existing poison policy.
1255
+
1256
+ Signed seq/floors are the permanent primary replay protection. Duplicate seqs
1257
+ are retired without a second admission; a known identical completed application
1258
+ can resend its saved outcome. The extra `(sender, env.id) -> content hash` guard
1259
+ refuses changed content while retained. Identical uncertain duplicates never
1260
+ republish. Tombstones retain 30 days, at most 10,000 per room; at capacity the
1261
+ oldest settled rows are evicted first. Pending, dispatching and unresolved
1262
+ `needs_review` are never evicted. An all-unsettled cap refuses with the room and
1263
+ count. Outside retention or after settled eviction, there is no historical
1264
+ app-id dedupe claim; an old signed frame is still refused by its seq floor.
1265
+
1266
+ The trace high-water mark is kept for the tombstone retention (30 days / 10,000
1267
+ per room, settled rows evicted first); outside it there is no historical trace
1268
+ claim, and MAX_HOP 3 still bounds every trace. Before local admission a hop below
1269
+ the retained maximum for that trace refuses; equal or higher hops remain allowed.
1270
+ There is no second trace ledger. Sender timestamps do not order messages or
1271
+ supply host expiry provenance; gaps are accepted and reported as signed frame
1272
+ sequence counts, never waited on. They do not prove which chat bodies were lost.
1273
+
1274
+ Offline Bus queues count toward the 200-per-room ceiling before transport starts;
1275
+ `hub_send` refuses beyond it. Original host deadlines survive handoff and restart.
1276
+ Unknown legacy deadlines fail closed to `needs_review`. Machine-local
1277
+ `rooms.ack_timeout_s` defaults to 600 (1..86,400); `rooms.queue_ttl_h` defaults to
1278
+ 24 (1..168). A single deadline-driven host timer handles ack/queue expiry. No
1279
+ uncertain work is silently retried. Explicit terminal-history retries without
1280
+ recipient proof refuse; the person can author a fresh message deliberately.
1281
+ An expired or missing original enqueue deadline also refuses a person retry
1282
+ before any disposition is recorded; a retry never invents a fresh TTL.
1283
+
1284
+ The ACK deadline starts at durable application handoff, before a broker `stored`
1285
+ reply, and is never extended by that reply. An unstored frame therefore becomes
1286
+ `needs_review` within the same ACK timeout instead of holding the peer for the
1287
+ queue TTL. Known pre-admission capacity or journal-unavailable refusals retain
1288
+ their pending plaintext for the next online/disposition retry without preventing
1289
+ later room packets and receipts from being processed. Status reads apply no
1290
+ disposition. Settled chat plaintext rows are pruned independently of presence
1291
+ traffic; permanent floors and retained application outcomes survive that pruning.
1292
+
1293
+ The crypto inbox has a separate durable reserve of at most eight receipt rows
1294
+ per room, each canonical receipt payload at most 1,024 bytes, with at most
1295
+ 64 * 8 * 1,024 additional bytes globally. Before taking a reserved slot, the
1296
+ authenticated sender and receipt frame/application/peer must match this hub's
1297
+ outstanding `dispatching`, `accepted` or `needs_review` outgoing row. Junk or
1298
+ non-matching receipts cannot use the reserve. Normal chat remains bounded to
1299
+ 200 pending rows and 16 MiB; presence caps, signatures and floors do not change.
1300
+ A matched receipt settles and frees its slot immediately. The durable
1301
+ `receipt_reserve` marker preserves its admission across a crash after settlement;
1302
+ open validates the canonical payload, size and reserve bounds and refuses an
1303
+ over-cap store as corrupt. Migration marks every legacy row ordinary and never
1304
+ infers reserved authority from its JSON.
1305
+ At the physical cap only, an authenticated current-member receipt with no
1306
+ matching outstanding row is durably recorded as the existing metadata-only
1307
+ `invalid_frame` refusal and transport-ACKed, without advancing the floor or
1308
+ sending a signed receipt. Below the cap its normal handling is unchanged.
1309
+ A matching receipt with all eight reserved slots occupied remains unACKed until
1310
+ a slot frees; it is never retired as junk.
1311
+
1312
+ At the ordinary physical cap, a fresh fully authenticated chat frame is durably
1313
+ refused with metadata-only `invalid_frame` and transport-ACKed, without a floor
1314
+ advance, admission or signed receipt. Its plaintext is not retained in the full
1315
+ inbox. The sender's application stays unsettled and expires to `needs_review`;
1316
+ matching receipts behind the refused chat can still reach the reserve. Known
1317
+ exact-duplicate and stale sequence handling precede this fresh-frame cap check.
1318
+
1319
+ At the ordinary physical cap, a fully validated signed current-member presence
1320
+ is recorded as metadata-only `invalid_presence` and transport-ACKed. It consumes
1321
+ no reserve slot, advances no floor and produces no `onPresence` observation.
1322
+ Runtime and other store errors remain unACKed. After room/global capacity has
1323
+ space for the retired presence again, one coalesced `requestReply` presence
1324
+ refetches the current remote state per drop-below-cap transition. There is no
1325
+ periodic capacity poll or permanent reliance on a dropped state observation.
1326
+
1327
+ ### Person disposition
1328
+
1329
+ `ahub room resolve <name> --frame <digest> --action accept|discard` uses the
1330
+ existing console-only preview ticket and fingerprint confirmation. Agent markers
1331
+ and `--live-test` refuse it. It records only local `disposed` metadata: the
1332
+ person's action, time and actor. Accept means the person vouches that the local
1333
+ peer got the chat; discard means drop it. Neither re-admits or republishes.
1334
+ Views say **disposed (accept|discard, by person)**, never completed. A disposed
1335
+ row is settled for retention/cap eviction, but never creates or resends a signed
1336
+ completed/failed_safe receipt. A retained duplicate remains non-publishable.
1337
+ Outgoing person queue dispositions likewise do not manufacture remote proof;
1338
+ late signed proof remains evidence-only.
1339
+
1340
+ Explicit room status is metadata-only and bounded: up to 200 outgoing rows
1341
+ prioritizing unresolved traffic, up to 200 incoming rows with uncertainty first,
1342
+ counts/truncation and remote addresses/states. Native turns, task roles and
1343
+ recovery sessions are never invented for remote transports. Real TLS/proxy,
1344
+ account and two-machine X7 proof remain unverified until their own live legs.
1345
+
1346
+ ## Encrypted rooms: X5 metadata views (#446)
1347
+
1348
+ The console's Rooms panel (`8`) and the dashboard's Agents > Rooms section are
1349
+ read-only views of the same canonical room metadata: local room name/id, epoch,
1350
+ owner/self fingerprints, member count, link state and exposed remote peer
1351
+ ids/states. Missing room metadata from an older hub and an empty room list hide
1352
+ the views; unavailable metadata is explicit rather than retaining old readings.
1353
+ Status reads create no room storage or keys.
1354
+
1355
+ Relay counters describe only this daemon session, starting at daemon startup and
1356
+ resetting on restart. Outbound counts successful handoffs; inbound counts
1357
+ confirmed local admissions. Withheld counts are a subset of outbound handoffs,
1358
+ not an additional total. Missing or unreadable values stay `null`/unknown, never
1359
+ zero. These counters do not prove application completion or lifetime traffic.
1360
+
1361
+ The projection contains no message body/title, reason, broker URL, key material
1362
+ or delivery history. This projection never reads a person inbox body; X5b below
1363
+ provides that separate person-only CLI. Native/account and X7 two-machine
1364
+ receipts remain separate live verification.
1365
+
1366
+ ## Encrypted rooms: X5b person inbox (#446)
1367
+
1368
+ Authenticated eligible chat to local `user` is admitted to a durable person
1369
+ inbox, separately from native peer delivery. The signed
1370
+ `completed/inbox_admitted` receipt, displayed as **completed (inbox)**, means
1371
+ durable inbox admission only. It does not mean the person read or acted on it.
1372
+ Admission, duplicate identity and receipt commit together before publication;
1373
+ duplicate frames cannot admit or count the message again. Done, discard and
1374
+ expiry never change that receipt or manufacture another outcome.
1375
+
1376
+ The RPC gate admits only the console role. As with X3 room mutations, the CLI
1377
+ refuses agents, conductors and `--live-test` before connecting. A console RPC
1378
+ connection carries no delegated/live-test marker, so that flag is not independently
1379
+ classified at the RPC gate. Dashboard, status, logs, memory, models and observers
1380
+ receive no inbox body:
1381
+
1382
+ ```sh
1383
+ ahub room inbox [<name>] [--json]
1384
+ ahub room inbox show <id>
1385
+ ahub room inbox done <id>
1386
+ ahub room inbox discard <id>
1387
+ ```
1388
+
1389
+ One operand always names a room, including `show`, `done` and `discard`; only
1390
+ an action followed by an exact local inbox UUID performs that action. Only the
1391
+ metadata list accepts `--json`; none of these forms accepts `--confirm` or adds a
1392
+ fingerprint prompt. Show returns one untrusted framed body through `sanitize`
1393
+ and terminal control stripping, and never marks it done. Done/discard are local
1394
+ metadata changes and immediately purge the body. Expired or disposed entries
1395
+ show metadata stubs only: no archive or read-after-done path exists.
1396
+
1397
+ A person done/discard commits and replies before firing a background retry, so
1398
+ a retry failure or socket drop during that work cannot change its acknowledgement.
1399
+ It and an actual TTL purge create one capacity-release opportunity for existing
1400
+ X4 deferred traffic. The disposed or expired entry gets
1401
+ no new outcome or receipt; unrelated authenticated pending chats may resume.
1402
+ Idle expiry ticks never retry or poll retained plaintext.
1403
+ Cleanup performed during show or admission retains a committed purge signal until
1404
+ the host consumes it once; rollback restores the prior signal.
1405
+
1406
+ Limits are 100 unread messages per room, 64 KiB decoded body per message and
1407
+ 4 MiB retained canonical payload bytes globally (the stored BLOB length, including
1408
+ JSON envelope overhead and escaping). Metadata `bytes` remains decoded UTF-8 body
1409
+ size. The existing 64 KiB entire
1410
+ application-wire cap is unchanged, so envelope overhead makes the actual
1411
+ sendable body smaller. Schema 4 records the first durable local crypto arrival;
1412
+ the inbox TTL is 30 days from that time, not the remote timestamp, admission
1413
+ retry or restart. Legacy pending person traffic with unknown local arrival age
1414
+ receives fixed `failed_safe/inbox_age_unavailable`; native peer handling is
1415
+ unchanged. Other fixed refusal reasons are `inbox_full`, `inbox_oversize` and
1416
+ `inbox_expired`.
1417
+
1418
+ Retain up to 1,000 settled metadata stubs per room for 30 days after settlement,
1419
+ evicting oldest settled stubs first. Unread messages and their matching admission
1420
+ identities remain protected, as do unsettled identities under the shared
1421
+ 10,000-per-room application cap. Inbound session counts increment once at
1422
+ admission, never on duplicate delivery, show or disposition. One 30-second
1423
+ loaded-state daemon hook expires bodies even when all rooms were left; it opens
1424
+ no empty store and creates no keys.
1425
+
1426
+ Lists return complete metadata, with at most 1,100 rows (100 unread plus 1,000
1427
+ stubs for one filtered room). A larger global list explicitly refuses with
1428
+ `inbox_list_limit` and guidance to choose a room, never silently truncates.
1429
+ ponytail: this complete-list ceiling keeps the first slice bounded; pagination
1430
+ is the follow-up for cross-room inventories. Inbox reads and dispositions refuse
1431
+ during recovery or a Bus hold in this slice.
1432
+
1433
+ Bodies are plaintext in the private local store under its existing directory/file
1434
+ modes; this adds neither at-rest encryption nor secure-erasure guarantees.
1435
+ Older X4 codecs reject the new receipt reasons and leave their sender outcome
1436
+ uncertain; no mixed-version success is claimed. Native/account and live
1437
+ two-machine receipts remain separate verification.
1438
+
718
1439
  ## Risks
719
1440
 
720
1441
  | Risk | Mitigation |
@@ -1570,6 +2291,30 @@ Release 0.12.16 uses control protocol 14. Recovery sources 9 through 13 remain
1570
2291
  supported; protocol 13 identifies releases 0.12.4 through 0.12.15, while
1571
2292
  0.12.16 uses protocol 14.
1572
2293
 
2294
+ ## New-work context admission (issue #802)
2295
+
2296
+ `context.max_assignment_used` defaults to 0 (disabled), is a machine-local
2297
+ finite fraction in `[0, 1]`, and is independent of `context.gate`. Only an
2298
+ untracked local source and a confirmed person reload (or deliberate restart)
2299
+ may enable it. A known occupancy greater than or equal to the configured
2300
+ nonzero threshold excludes a candidate from a new automatic owner offer.
2301
+
2302
+ `assign()` remains pure: its host inputs include the threshold, freshness
2303
+ limit, clock, attachment, current native session and a copy of that peer's
2304
+ native reading. Validate timestamp, session equality and finite occupancy
2305
+ before comparing. Missing, invalid, stale, future, mismatched or unsupported
2306
+ readings are unknown, never zero or evidence of headroom. Unknown does not add
2307
+ an eligibility refusal; the ordinary gates still apply. Traces show the same
2308
+ context decision as actual assignment without exposing session identifiers.
2309
+
2310
+ Only new automatic owner offers use the guard; reviewer admission is unchanged,
2311
+ including owners of review-class work. A name or reservation is not an override.
2312
+ A host-authorized explicit person/conductor nomination or self-claim can bypass
2313
+ only this context guard; all other privacy, roster, quota, capability and manual
2314
+ assignment constraints remain. Re-evaluate at the existing post-await owner
2315
+ admission boundary. No reading triggers pause, compaction, replacement, task
2316
+ reassignment or a new retry timer. Checkpoint pressure semantics stay unchanged.
2317
+
1573
2318
  ## Operator console and conductor (issues #190, #191, #193, #194, #195)
1574
2319
 
1575
2320
  `ahub console` owns one authenticated console connection. It shares tail's
@@ -2414,7 +3159,7 @@ acts on the stopped state directory.
2414
3159
  - Recording never blocks or fails the task flow: the writer runs after the board event, and a failure is one
2415
3160
  hub.log line per run. ponytail: it reads the whole events file for each record; an incremental per-task index
2416
3161
  if that file grows large.
2417
- - Agents may read the measures and export; backfill and labels are a person's (identity gate).
3162
+ - Agents may read the measures and export. `research backfill` is the person's or the configured conductor's, and an agent labels only its own task, where a person's label always wins (#933 amendment; identity gate).
2418
3163
 
2419
3164
  ## Amendment: proven Pi tool refusals and idle exit recovery (issues #254, #255)
2420
3165
 
@@ -2446,7 +3191,7 @@ acts on the stopped state directory.
2446
3191
  ponytail: a board state to withdraw an open task would let the run continue. The fingerprint holds what the hub's
2447
3192
  status reports (a requested model for Pi, permission modes once #242 lands); ponytail: read each peer's launch
2448
3193
  settings for the rest.
2449
- - The dashboard snapshot carries run summaries and per-arm measures only. `bench run` is a person's (identity gate,
3194
+ - The dashboard snapshot carries run summaries and per-arm measures only. `bench run` is the person's or the configured conductor's (#933 amendment; identity gate,
2450
3195
  wherever `--json` stands); reading results is not. Schema and commands: [bench](../bench.md).
2451
3196
 
2452
3197
 
@@ -2750,6 +3495,7 @@ the queue.
2750
3495
  - Schemas, CLI classification, Tasks, daemon/native contracts, plugin instructions, both local-worker tool-building paths, check-name allowlists and init use the same resolved data. No-override dev contracts/schemas/instructions remain byte-identical. Old accept schemas support existing task fields.
2751
3496
  - Flow authority remains the separately validated `roles` map; executable checks remain trusted `checks`. Example checks are display data and do not execute. Managed instruction content keeps surrounding text and quotes forged headers.
2752
3497
  - Acceptance criteria with ids (#507b) reuse the #481 S8 `criteria` checklist, the only criteria model: `sendReview` lists the recorded criteria (`- <id>: <result>`, `(optional)` for a non-required one) ahead of the profile's checklist lines in every profile, and `Tasks.review` records on the verdict the ids that the reviewer's `unmet` items name, through the pure `unmetCriteriaIds` (an item names a criterion only by starting with its exact recorded id, followed by the end of the item or a delimiter; each id once, in first-named order). The ids come from the board's checklist at the verdict, never from the caller, and are stored as `HistoryEntry.unmetCriteria` (validated: at most 32 distinct bounded ids, on a saved review verdict only) and shown in the public history. The note text is unchanged. A PII task lists and records none; a task with no checklist, a verdict with no unmet items and an item with no matching id add nothing, so every existing request and verdict is byte-identical. #513 packing and #514 learning refer to these ids.
3498
+ - Recurring unmet criteria (#514a, `src/hub/learning.ts`, `Tasks.proposeCriterionCandidates`) reuse the #481 S3 recurrence path: after a saved verdict that named criteria, the pure `groupUnmetCriteria` groups the ids reviews recorded as `unmetCriteria` in the last 30 days by profile, profile class (the alias when the task has one) and criterion, over rows that are not PII and whose text the candidate screen allows. A group that this verdict named, that has at least 3 tasks forming at least 2 separate pieces of work (`workUnits`) and no card yet (`Criterion candidate key: <profile>:<class>:<id>` in a hub-filed backlog card's detail, the same dedupe as `Review finding key:`) files ONE backlog `plan` card for a person: ids and counts only, plus the `profile_overrides.review_checklist` fragment as text (`overridesDiff`). Nothing is applied: `profile_overrides` is not machine-local (it is read from the tracked `.agenthub/config.json` and the untracked `config.local.json`, layers merged, and is in neither `MACHINE_LOCAL` list), so the person chooses the layer and merges the line into their own checklist, which replaces as a unit. `selectGroups` applies those filters before the per-pass cap of 64 cards, so groups that are settled (already carded, unnamed, under threshold) never use up the cap and starve a new one. A withheld verdict, an evidence-only verdict (#852), a private turn and a PII task file nothing and count for nothing. Per-profile `profile_overrides` application, thresholds as config and decay are not part of this step.
2753
3499
  - Override maps are bounded to 32 class entries and existing base classes; plan selections use existing fields. Role text is 2000 characters, agents 16000, examples 16 strings of 2000 characters, review checklist 16 lines of 500 characters (#507). Native ingestion/retention/measurement remains unverified; configuration changes apply after daemon/MCP restart.
2754
3500
 
2755
3501
 
@@ -4145,10 +4891,10 @@ These rules are settled here and land with hub tasks #284-#286; until then the S
4145
4891
 
4146
4892
  - `ahub sdlc metrics [--since]` and the planner tool `hub_sdlc_metrics` are a pure projection of board history (`src/hub/sdlc-metrics.ts`: no clock, no I/O, no model). `Tasks.sdlcMetrics` is the one read, for a planner or the person, refused during a private turn; private tasks are counted as withheld and read for nothing else. Anything the board cannot derive is `null`/`unknown` and counted, never 0.
4147
4893
  - Scope cuts, each deliberate:
4148
- - **No PR readback.** The report reads only the board. Time to merge and same-head CI reruns need a merged PR and its check runs, which `ahub sdlc costs` already reads under its own bounds (16 PRs, 20 s, public tasks only); a second reader of the same GitHub surface would duplicate those bounds and their privacy revocation checks. Both read `unknown` here, and the pure functions accept them as optional inputs so a later slice can supply them.
4894
+ - **PR readback is the merge's, not the report's (#442).** The report still reads only the board. The guarded merge reads the merged PR once, after it observes the merge, and records `cost: { mergedAt, ciRuns, ciReruns, reviews }` on the task's merge record (`readPrCostHistory` under an 8 s bound, host-written, never a tool input, never part of the merge decision). The report then reads those recorded figures, so it keeps the board as its only source and makes no GitHub call: a second reader of the same surface would duplicate `ahub sdlc costs`' bounds and privacy checks, which this does not. A task whose merge was not read stays `unknown`, never 0. The cut that remains: a PR merged by hand or before this record has no figures.
4149
4895
  - **S10's `costBandProposals` is not consumed.** It returns per-PR upper-limit breaches over complete merged-PR rows, a different data set (review rounds, CI runs and reruns per merged PR) from board-history metrics, and it takes absolute limits rather than a rolling baseline. S10 left the baseline, configuration and automatic card scheduler to S7 and delivered the pure breach data only; S7 builds the scheduler for its own metrics and leaves the PR cost metrics' bands for the slice that also reads PRs, so one configuration and one dedupe rule cover both.
4150
- - **No security-scan baseline.** The last S7 line (a scheduled security scan with a recorded baseline feeding the propose tier) is not built. No scan source exists in the repository to baseline, and inventing one would claim a security control nothing enforces.
4151
- - **No release metrics, no release bands, no gates-run record.** S11 recorded release cut and shipment times and left the bands and proposals over them to S7 (release interval, lead time from approval to release, and a band on the hotfix rate that proposes a planning card). S7 here reads task history only, so none of those is built: release history is a separate table with its own bounds and privacy rules, and a hotfix rate needs shipped releases to have a baseline at all. The per-release "gates run" record is S11's explicit deferral to S6/S7 and stays deferred: a cut record is not gate-execution evidence, and recording gates needs the S6 authorization path. These are the next slice, and `[bands]` has no release metric names until then, so a typo cannot silently configure one.
4896
+ - **Security-scan baseline is an opt-in read of GitHub's own alerts (#442).** No scan source existed in the repository, and inventing a scanner would claim a control nothing enforces, so the hub runs none. `[security_scan]` (no default source) names which of Dependabot, secret scanning and code scanning to read; the daemon reads their open-alert counts on the configured interval, records counts only in `security_scans`, and `security_open_alerts` is a band over the totals, so a breach is the existing one-card-per-episode path. A source GitHub will not read makes the total unknown and unknown is not a point, never a zero. The cut that remains: no scan runs unless the person configures one, and a repository that exposes no alert source records nothing.
4897
+ - **Release metrics and the `hotfix_rate` band are built; the gates-run record is not (#442).** The release report shows release interval, lead time from final approval to release and hotfix rate from `releaseMetrics`, with unknown named and never 0, and `hotfix_rate` is a band with one point per shipped release that has recorded train history (a legacy release is left out, never a 0). The per-release gates-run record stays S11's explicit deferral: a cut record is not gate-execution evidence, and recording gates needs the S6 authorization path.
4152
4898
  - **`first_pass_check` is the hub's completion check, not PR CI.** It reads `check passed` and `check failed` at `hub_task_done`, so it is `unknown` wherever completion checks are off (the default). The issue's "first-pass CI success" is the PR-based figure and is not available without the readback above.
4153
4899
  - **Stale-head re-reviews need head evidence.** A review round is stale when its verdict is bound to a different head than its request (`reviewCost.head` on the done against `reviewFindings.head` on the verdict). A task without both is `unknown`. A `done` after an `approved` is not a signal: the board's move table has no way out of `approved`.
4154
4900
  - Control bands are optional `[bands.<metric>]` tables of `sdlc.toml`, validated by `bandsConfig` with field-level paths. The newest value is compared with the mean and population standard deviation of the preceding `window` values, on the bad side only, with a per-metric deviation floor, a `min_samples` floor, a `min_delta` floor that every tier including the drift rule honours, and a drift rule (`drift_run` newest values all over one deviation on the bad side is at least `diagnose`).
@@ -4156,6 +4902,14 @@ These rules are settled here and land with hub tasks #284-#286; until then the S
4156
4902
  - A breach is an episode: the run of non-ok values for one metric. `BandResult.episode` is the task that started the run, derived from history alone, so a restart finds the same key. `Tasks.proposeFromBands` is the only writer. `log` is one hub notice, recorded in the board's `band_notices` table so it is announced once. `diagnose` is one read-only `triage` backlog task and `propose` one `plan` backlog card, keyed `<metric>:#<episode>:<tier>` in the card. A metric with an open band card writes nothing more, so a value growing from diagnose to propose inside one episode adds no second card, and a sustained shift is one card. Card text is fixed words, numbers and task ids, screened like the recurring-finding card; the hub opens no GitHub issue.
4157
4903
  - The daemon evaluates every five minutes, only when the board's status revision or the band configuration moved since the last evaluation that ran, and not while it is stopping, in recovery or has a storage error (the same holds as the task sweep). A pass that was held, in mode off or without bands consumes nothing, so a breach seen then is written when the hold clears and bands enabled by a reload on an idle board are evaluated at the next tick. A failed evaluation is one `hub.log` line. Ceiling: one evaluation reads every task with its history; a history window is the upgrade if a board passes several thousand tasks.
4158
4904
  - Bands write cards only in `advise` or `enforce`; in `off` the writer is idle while the report still shows each configured band's tier.
4905
+ - Two bands are not made of tasks (#442): `hotfix_rate` (a point per shipped release) and `security_open_alerts` (a point per recorded scan). `BandResult.subject` says what `lastTask` and `episode` number (`release` or `scan`), the card text names that subject (`release #N`, `security scan #N`) and the key format `<metric>:#<id>:<tier>`, the open-card rule and the episode derivation are unchanged. The band signature that gates the five-minute evaluation includes the shipped-release count and the newest scan id, since neither moves the task board's revision.
4906
+
4907
+ ### S5 eval gate amendment (#481, hub task #201)
4908
+
4909
+ - Scope: the gate mechanism only. The eval suite and its smoke subset (#266, #251) do not exist, and choosing one is a person decision, so the hub chooses, creates and defaults to none: `eval_check` has no default and an unconfigured gate reports `eval check not configured: ... (suite choice pending the person)` as a note in advise and enforce, never a silent pass and never a blocker, so an enforce setup gains no refusal until the person names a suite.
4910
+ - `config_paths` (default AGENTS.md, `docs/agent-notes/**`, `templates/**`, `.agenthub/roles/**`, routing.toml and `src/hub/hub-tools.ts`) marks a configuration PR from complete PR file metadata (`src/hub/eval-gate.ts`). An incomplete inventory is unknown, never a code-only PR. A code-only PR is unaffected and off keeps its output byte for byte.
4911
+ - The configured `eval_check` joins the required checks wherever they are judged (`sdlcStatus`, the queue and `decideMerge`, each reading). The pass rate and suite version come from one exact-head completed check run of that name, parsed only from the `eval_pass_rate=` and `eval_suite=` markers in its title and summary, and are recorded on the approval note. With a check named, gaps (unknown coverage, a pass with no reported rate or suite) are shown in advise and block in enforce.
4912
+ - Cuts: no suite, no case from an S7 incident, no tag or release gating, and no per-repository `eval_check` table (a repository takes the top-level key). The marker contract is the hub's, not any suite's; the person's chosen suite must emit it, or the gap stays visible.
4159
4913
 
4160
4914
  ## Antigravity optional implementer peer (#485 P1)
4161
4915
 
@@ -4339,7 +5093,7 @@ The owner gave the #516 track its go-ahead on 2026-10-22. This section closes th
4339
5093
 
4340
5094
  ### Sources (#508)
4341
5095
 
4342
- - `ahub source add <path|url>` is person-only, refused in agent shells like the other console commands. The id is `src:` plus the first 16 hex digits of the snapshot's SHA-256, so adding the same bytes twice returns the existing id.
5096
+ - `ahub source add <path|url>` is the person's or, since #933, the configured conductor's (admitted and audited by the hub); other agent shells are refused like the other console commands. The id is `src:` plus the first 16 hex digits of the snapshot's SHA-256, so adding the same bytes twice returns the existing id.
4343
5097
  - The snapshot lives in the hub state directory under `sources/<id>/`: `meta.json` (`{id, kind, origin, sha256, bytes, added_at, private}`), the raw bytes, and the projected text. The raw cap is the text-projection input cap (`MAX_PROJECTION_INPUT`, 16 MiB), and the text comes from the existing text_projection converters with their caps and sandbox. Larger inputs are refused with the cap in the reason; nothing is truncated silently.
4344
5098
  - A URL is fetched once, by the person's CLI process, over https only, following at most 5 redirects, within the same 16 MiB cap. No agent, sandbox or hub tool fetches a URL. Because the person issues the fetch, a PII project does not refuse it; the snapshot is screened like any file.
4345
5099
  - A path under `pii.paths`, or a snapshot whose text the PII screen flags, is stored with `private: true`.
@@ -4347,6 +5101,28 @@ The owner gave the #516 track its go-ahead on 2026-10-22. This section closes th
4347
5101
  - `hub_source_read {id, range?}` takes `range: {offset, length}` in characters but bounds the response in bytes: after `sanitize()` and the PII screen, the text is at most 32 KiB of UTF-8, cut at a character boundary with an explicit cut marker, and the reply returns the `next` character offset. It is excluded from capture.
4348
5102
  - The plan's `sources` field is validated against the registry: an unknown id is refused at propose, accept and plan. Overlap notices name registry ids, never origins.
4349
5103
 
5104
+ #### Sources reader implementation (#508 AC3, hub task #333)
5105
+
5106
+ - `hub_source_read` is a read tool on the common task-tool surface. Offsets and lengths count Unicode code points, starting at zero; both range fields are required when a range is supplied. Length is positive, offset nonnegative, and both are safe integers. The reply contains `id`, `kind`, `private`, `text` and `next`; it never includes an origin. A withheld private reply contains only `id`, `kind` and `private: true`.
5107
+ - The reader screens the whole stored projection before selecting a range. It reapplies current `pii.paths` to every file origin and current `pii_patterns` to the complete text. With local model screening enabled under `local_only`, it uses the existing bounded host PII screen. A timeout, unavailable model, invalid pattern or oversized input is private. The existing 6000-byte model-screen ceiling applies to the full source; this slice does not classify excerpts as a substitute for the full source.
5108
+ - Host classification upgrades `meta.json` under the registry's registration lock without adding an origin, so the origin cap cannot prevent an upgrade. A public registration can never undo that classification. A screen failure is withheld, never exposed as a clear result.
5109
+ - Private text requires the daemon's in-process builtin on-prem worker, a current PII task turn, and a positive on-campus check. A control WebSocket claiming the local peer is insufficient. Authority, policy and record changes across awaited screens or campus checks fail closed. Already-private text is not sent to the model screen.
5110
+ - Pagination sanitizes the chosen original characters, includes an explicit cut marker when characters remain, and caps that text including the marker at 32 KiB of UTF-8. `next` counts consumed original characters, so sanitation expansion cannot skip text. Tool calls and their results are excluded from local and native memory capture; pending-delivery hints are separate from the source JSON.
5111
+
5112
+ #### Sources (#508) implementation notes (hub task #329: `ahub source add|list`, `src/hub/sources.ts`)
5113
+
5114
+ Where the code differs from or fixes a detail the section above leaves open:
5115
+
5116
+ - **Directory name.** The snapshot directory is `sources/src-<16 hex>/`, not `sources/src:<16 hex>/`: a colon is not a safe path segment on every filesystem. The id stays `src:<16 hex>` everywhere it is shown, stored in `meta.json` and accepted as input; only the folder drops the colon. A reader maps an id to its folder with `id.replace(":", "-")` and refuses anything that is not exactly `src:` plus 16 lowercase hex digits, so an id can never name a path.
5117
+ - **`meta.json` shape.** The spec's `{id, kind, origin, sha256, bytes, added_at, private}` describes the first registration and is kept exactly. `origins` is added: every registration of those bytes (`{kind, origin, added_at, private}`, at most 256, an exact repeat adds nothing) so "keeps every origin" is data, not a promise. A distinct origin beyond 256 is explicitly refused before any mutation, including a proposed private upgrade; that attempted registration did not occur, and the existing record stays unchanged. Exact repeats remain idempotent at capacity. `private` is the OR over `origins`; a `meta.json` whose flag is lower than one of its origins is damaged and reads as no record, so a hand edit or a stale writer can never downgrade a source.
5118
+ - **What "the PII screen" is on this path.** `ahub source add` runs in the person's CLI process, which has no daemon and no model gateway, so it applies the deterministic screens only: `pii.paths` for a file, and `signals.pii_patterns` (the same regular expressions the hub applies to task text) to the snapshot's text. The model screen (`signals.pii_screen = "local"`) needs the daemon's on-campus check and is not run here. A pattern that cannot be evaluated counts as flagged (`text could not be screened`), never as clear. A snapshot only the model screen would flag registers public in the CLI; the daemon-side reader (`hub_source_read`, AC3, implemented in task #333) screens it again before exposure and upgrades the record, which the monotonic rule allows. The reasons are fixed words (`path under pii.paths`, `text matches signals.pii_patterns`, `text could not be screened`); the matched text is never printed.
5119
+ - **Text.** The text is the existing converters' projection (`TextProjections.projectBytes`, with their caps and sandbox) where one handles the extension; otherwise a plain file that is valid UTF-8 without NUL bytes is its own text, and anything else has no `text` file and is not guessed at. A converter that fails leaves the plain-text fallback, never an unscreened empty record.
5120
+ - **URL.** Redirects are followed by hand so each hop is re-checked as `https:` without credentials; a relative `Location` resolves against the current URL; the sixth redirect is refused without a seventh request. The body is read through a running byte cap, so a response that lies about or omits its length is cut off at 16 MiB and never buffered whole, and a declared `content-length` over the cap is refused before the body is read. The registered origin is the final URL.
5121
+ - **Authority.** `source list` is `console` in `classifyPeerCommand` by falling through (an unlisted command is the person's), and `source add` is `conductor` (#933: see the amendment "the console class keeps trust, money, attribution and terminals"); a test pins every form, and `cliCommandLabel` names `source add` or `source list` in the audit and in a refusal, never the path or URL. Both registration and listing also refuse `--live-test` delegation under agent markers, through the existing person-attributed-content gate, before any source is read, fetched or written.
5122
+ - **One registry lock, and caps that match.** Registrations are serialized across processes by `sources/.lock`, published whole by `link(2)` of a temp file that already holds the owner's pid and process signature, so it never exists without an owner. Only its own holder removes it, when the registration ends. It is never taken over: a lock whose owner is proven gone makes the registration fail closed, naming the file for the person to delete, because "check, then unlink" is not atomic and two recoverers could each delete the other's fresh lock and mutate together. A stale-holder refusal rechecks the same inode and owner bytes after the process inspection: a normally released or replaced lock retries within the existing deadline instead of reporting an old observation as stale. No contender removes any lock. A registration that cannot get a live holder's lock within ten seconds fails naming the lock. Reader and writer share one cap per file (`MAX_META_BYTES` 2 MiB, `MAX_SOURCE_TEXT` equal to the raw cap), so what `add` accepts can always be read back. A directory with a `meta.json` that cannot be read is damaged, not absent: `add` refuses it and leaves it as it is, because rebuilding it could drop a private flag.
5123
+ - **Registry capacity and enumeration.** The 10,000-source cap counts valid metadata records, never lock files, contender temporary files, crash partials or damaged entries. Lists validate records before applying the cap; an already overfull on-disk registry refuses explicitly instead of silently cutting rows. New registrations enforce capacity under the registry lock, including when their target directory is a crash partial. Existing-source privacy/provenance updates remain possible at source capacity, subject to the separate origin cap.
5124
+ - **Not in the CLI slice.** The reader is implemented separately in task #333 above. The plan `sources` validation, overlap notices and the dashboard list (AC4 and the dashboard half of AC5) remain separate tasks; this one is the CLI half of AC1, AC2 and AC5.
5125
+
4350
5126
  ### Intake drafts (#511)
4351
5127
 
4352
5128
  - A draft is a backlog card carrying `draft: true`. Backlog work is never offered, `hub_backlog_pull` refuses a draft, and the draft's children inherit the mark, so AC3 holds by construction.
@@ -4354,7 +5130,19 @@ The owner gave the #516 track its go-ahead on 2026-10-22. This section closes th
4354
5130
  - With a planner, the hub sends it a `task` envelope of class `plan` that names the draft id. The request text goes through `publicView`. The planner answers with `hub_backlog_split` on the draft, giving each child a profile class, its sources, dependencies (`after`/`afterParts`) and acceptance criteria in the #507 criteria-id format.
4355
5131
  - Without a planner, the hub model (`inference.ts`) proposes the same split as JSON. The hub validates it against the `hub_backlog_split` schema and writes it; invalid output leaves the parent draft unsplit, with the reason.
4356
5132
  - PII in the request never goes to a planner peer, because PII turns refuse `hub_backlog_split` (`taskOpBody`). The hub's on-prem model proposes the split instead, under the existing triage screen, and the hub writes it itself through `Tasks` as private (PII) drafts, which show as stubs everywhere. Without an on-prem model, the draft stays unsplit for the person.
4357
- - `ahub intake approve <id>` (person-only) clears `draft` on the parent and its children, which then follow the normal backlog pull. `ahub intake discard <id>` cancels them through `Tasks.cancel`.
5133
+ - `ahub intake approve <id>` (the person's, and since #933 the configured conductor's) clears `draft` on the parent and its children, which then follow the normal backlog pull. `ahub intake discard <id>` cancels them through `Tasks.cancel`.
5134
+
5135
+ #### Intake drafts implementation notes (hub task #334: `ahub intake`, `src/hub/intake.ts`)
5136
+
5137
+ - **Where the mark lives.** `draft` is a column on `tasks` (added by `ALTER` on an older database, default 0) and `Task.draft?: true`. Only `Tasks.propose` writes it, and only for the person's own `backlog: true` proposal (`by === USER`); a peer's `hub_task_propose` has no such field. `Board.splitBacklog` gives each child its parent's mark.
5138
+ - **Enforcement is at the board, not only at the callers.** `Board.pullPlanned` refuses a draft, and `Board.update` refuses to move a draft to any state but `backlog` or `cancelled`, so a path that forgot the check still cannot offer one. `Tasks` adds the readable refusals in `pullPlanned` and `moveBacklog`, skips drafts in the reservation auto pull (`topReservedReady`) and keeps them out of the planner's "Backlog items" notice.
5139
+ - **The request.** One parent draft: class `plan`, title `Intake: <first line>`, detail the text (1 to 7000 characters) and, on a last line, `Sources: <ids>`. Each `--source` must be a `src:<16 hex>` id the registry holds, at most 16 and distinct; all are checked before any card is made.
5140
+ - **With a planner**, the hub sends the first attached, rostered planner a `task` envelope (`taskRecipient: planner`) naming `hub_backlog_split` on the draft id at its planning revision, with the text from `publicView`, the sources and the criteria format. If no planner is reachable, or the envelope is not queued, the hub model takes it.
5141
+ - **Without one**, `Inference.split` (interactive clock, 60 s, 3000 tokens) answers JSON. `parseIntakeSplit` takes the bare object, a fenced block or prose around it, checks it against the `parts` property of the running profile's `hub_backlog_split` schema (a small validator over the keywords those schemas use: type, required, additionalProperties, min/max items, lengths, minimum, uniqueItems, enum, pattern) and then the rules `Tasks.splitBacklog` enforces anyway. It also requires every part's detail to carry acceptance criteria in the #507 `<id>: <text>` format and every `plan.sources` id to be one the request named. The hub writes the result through `Tasks.splitBacklog(HUB, ..., { intake: true })`, the only call where `by === HUB` passes the planner check. The hub's exception requires the parent to still be a draft at every validation through the final write: `Tasks.splitBacklog` checks it before and after the awaited screen, and sets `Board.requireDraftForNextSplit` for exactly that write so the board checks it again inside its transaction (an approval clears the mark without moving the planning revision, so the revision alone cannot show it). A person who approves the card while the model answers therefore wins: no child is created, and none is left undrafted. Any failure (no model, no answer, not JSON, schema, criteria, a refusal by `splitBacklog`, a draft that changed meanwhile) leaves the draft whole with a `hub` history entry `intake unsplit` naming the reason; the reason never contains anything the model wrote: it is built only from fixed words, array indexes and the schema's own property names. A property the schema does not list is reported as `parts[i]: an unexpected property`, without its name, so a hostile or private property name cannot reach the draft's history, which is not given the semantic screen the split parts get. A refusal from `splitBacklog` keeps the hub's own fixed refusal text, and any other error becomes `the board refused it`.
5142
+ - **PII.** A request whose draft is private is never sent to a planner. The model call needs `triage.onCampus()`; off campus the draft stays whole with that reason. On campus the hub's split goes through the unchanged `splitBacklog` screening, so the children are private drafts that show as stubs.
5143
+ - **Approve and discard.** `Tasks.approveIntake` accepts `USER` or the admitted conductor (#933 amendment), and `discardIntake` accepts only `USER`. The family is the draft and the still-open draft cards in its `deps`. Approval is one `Board.approveDrafts` transaction (every card must still be an unstarted, ownerless backlog draft, or none changes) that clears the mark and records `draft approved`; the cards then follow the normal pull. Discard calls `Tasks.cancel` on each card.
5144
+ - **The command.** `ahub intake "<text>" [--source <id>...]`, `approve <id>`, `discard <id>`, with the usual `--json` and `--color=`. It is not in `classifyPeerCommand`'s allowed list, so an agent shell is refused (except `approve` for the configured conductor (#933: see the amendment "the console class keeps trust, money, attribution and terminals")), and `liveTestRefusal` covers it. The audit label is `intake`, `intake approve` or `intake discard`, never the text. The request is 90 s at most on the control socket.
5145
+ - **Not in this slice.** The dashboard text box (AC1's second half) and a dashboard drafts view are #511b. Drafts show in `ahub backlog list` with `draft: true`.
4358
5146
 
4359
5147
  ### Schedules and folder triggers (#512)
4360
5148
 
@@ -4362,7 +5150,45 @@ The owner gave the #516 track its go-ahead on 2026-10-22. This section closes th
4362
5150
  - Entry: `{id, every, class, title, detail?, sources?, owner?, queue?: "backlog" | "ready"}`. `every` is `<n>m|h|d` with a 15-minute minimum. `cron` is accepted by the schema but refused with "cron not supported yet; use every", because a cron parser is a new runtime dependency or new code; it is a later slice.
4363
5151
  - Due checks run on their own interval timer, started whenever `schedules` or folder triggers are configured, independent of `task_sweep.enabled` (the task sweep's timer exists only when that option is on, and it defaults to off). The timer runs no task sweep, and it is re-armed on config reload. `queue` defaults to `backlog`. Missed runs coalesce: a schedule proposes at most one task per tick, and none while its previous task is still open. A global pause or hold skips the tick and coalesces it. Last and next run times live in `hub.db`, and status and the dashboard show the next run.
4364
5152
  - Folder trigger: `{id, folder, glob?, class, title, ...}`. `folder` is resolved with `realPath`, must lie inside the project or a declared data path, and is polled on the schedules timer, not watched. Symlinks that leave the folder are skipped. A new file (by `realPath`, size, mtime and SHA-256 recorded in `hub.db`) is registered as a source (#508) and proposes one task that names it. The trigger therefore lands after the source registry.
4365
- - `ahub schedule list|run <id>|pause <id>|resume <id>` is person-only.
5153
+ - `ahub schedule list|run <id>|pause <id>|resume <id>` is person-only for `list`; `run`, `pause` and `resume` are also the configured conductor's since #933 (implementation notes in #512b below).
5154
+ - Folder triggers are their own machine-local key, `folder_triggers` (implementation notes in #512c below).
5155
+
5156
+ #### Schedules (#512a) implementation notes (hub task #330: `schedules`, `src/hub/schedules.ts`)
5157
+
5158
+ Where the code states or fixes a detail the section above leaves open:
5159
+
5160
+ - **Where the timer runs.** `schedulesTimer` in `startDaemon` is its own interval (default one minute, `schedulesEveryMs` in tests), armed at startup and re-armed by `reloadConfig` when a `schedules` leaf changes. It exists only while `schedules` is non-empty, and it never reads `task_sweep.enabled`. The pass is `Tasks.proposeFromSchedules`; the daemon only calls it and writes `status.json` afterwards.
5161
+ - **Period.** A schedule's period starts at its anchor in `schedule_runs` (`hub.db`): the first time the hub saw it, then each run. A schedule never runs at first sight, and a restart or a hold never replays what it missed: a run restarts the period at `now`. Changing `every` takes effect from the same anchor. A schedule removed from the config loses its row.
5162
+ - **Coalescing.** A due schedule proposes at most one task per pass. While the task its previous run made is open, or while the hub is held (stopping, recovering, a storage error), a due pass proposes nothing and counts a skip (`coalesced`); the next run after the cause ends is the one task they fold into.
5163
+ - **Which task is "its previous task".** The open card whose first history entry is by the hub and whose detail starts with `Schedule key: <id>`, read from the board, never from `hub.db`. So a task closed by anyone, or a `hub.db` row that was lost, cannot make the answer wrong, and a task a person or peer wrote to quote the marker does not count.
5164
+ - **Holds are fenced at the write, and stop drains the pass.** `propose` awaits the PII screen and triage, and a hold (stopping, recovery, a storage error, the test seam) can begin during them, so `proposeFromSchedules` hands `propose` a fence that `requestCheck` asks again right before the board write. A run overtaken that way writes nothing and counts as a skip, never as the schedule's error. Normal shutdown clears `schedulesTimer`, waits for a pass in flight (bounded at five seconds, after which the pass is still refused by the fence), and closes `schedulesStore` before the board; a pass that ends during shutdown does not republish `status.json`. A recovery commit waits for a pass in flight (`Tasks.schedulesPending`, in `recoveryReady()` and the blockers as "a scheduled task proposal in flight"), like it waits for completion checks and automatic pulls, so no scheduled run writes after the commit's digest. An ordinary `propose` takes no fence.
5165
+ - **How the task is made.** Through `Tasks.propose` as the hub (backlog for `queue: "backlog"`, a routed proposal for `"ready"`), so the PII screen, triage, class validation, routing and notices all apply. A proposal `Tasks` refuses is recorded as the schedule's `last_error`, shown in status, and retried at the next tick (the period is not restarted by a failure).
5166
+ - **Status.** `schedules` in the daemon status holds, per schedule: id, every, class, queue, next run, last run, the task, a state (`scheduled`, `due`, `waiting`, `held`), skipped runs and the last error. It never holds a title or detail, because those are task text. `ahub status` prints one `schedule <id>` line each.
5167
+ - **Config.** `schedules` is in `MACHINE_LOCAL` and `NEVER_EDITABLE` (a settings write cannot create work), is validated by `schedulesConfig` in the loader and in `config-apply`, and applies live. At most 64 entries, ids of lower-case letters, digits, `-` and `_`, details of at most 7000 characters (so the marker line is never cut off), at most 16 `sources` ids of the `src:<16 hex>` form.
5168
+ - **Not in this slice.** `sources` is carried into the task's detail as `Source: <id>` lines; it is not yet validated against the registry or attached as `plan.sources` (that is the plan-validation slice of #508). Folder triggers and `ahub schedule list|run|pause|resume` stay with their own tasks.
5169
+
5170
+ #### Schedule commands and the dashboard (#512b) implementation notes (hub task #335: `ahub schedule`, `src/cli/schedule-output.ts`)
5171
+
5172
+ - **Person or conductor, three layers** (`run`, `pause` and `resume` also admit the configured conductor, #933: see the amendment "the console class keeps trust, money, attribution and terminals")**.** `schedule` is not in `classifyPeerCommand`'s allowed list, so an agent shell is refused before the command runs, `liveTestRefusal` covers it, and the daemon's console-op gate and `Tasks.runScheduleNow` and `setSchedulePaused` each refuse any actor but the person. The audit label is `schedule <list|run|pause|resume>`, never an id (an id is config text).
5173
+ - **`list`** prints `scheduleViews` as a table: id, every, class, queue, state, next run, last run, task, then the skipped-tick count and the last error as notes. No title or detail appears anywhere: both are task text.
5174
+ - **`run <id>`** proposes the schedule now, whatever its period says, through `runSchedule`, the same code the timer's pass calls, so a manual run can never do what a due one may not: it is refused as `held` while the hub is stopping, recovering or on a storage error, refused as `waiting` while the task its previous run made is open, and otherwise goes through `Tasks.propose` as the hub (the PII screen, triage, routing and notices all apply). It takes the same single pass guard as the timer (`schedulesRunning`), so it never overlaps a pass and recovery's `schedulesPending` wait still covers it; while a pass runs it is refused with "a schedules pass is running". A successful run restarts that schedule's period at now, like a due run. A refused manual run is not a missed period, so it does not feed the coalescing count.
5175
+ - **Pause and resume are ordered against a run in flight.** `ScheduleStore` keeps a per-schedule pause generation in memory (`epoch`, bumped by `setPaused`). A timer run captures it when it is admitted, and the write fence `propose` asks again right before the board write also compares it, so a pause or resume that lands while the run awaits its PII screen cancels that run (outcome `changed`, nothing written, no skipped tick): the acknowledged command is what takes effect. `ScheduleStore.ran` also moves the anchor with `MAX(anchor, now)`, so a pass that finishes after a later resume can never shorten the fresh period. A person's own `run` is outside the epoch: pausing during it does not cancel it. A restart ends every run in flight, so the generation needs no storage.
5176
+ - **A manual run is drained at shutdown.** `schedule_run` is registered in `manualSchedulePasses`, which `stopOnce` waits for (bounded at 5 s, like the timer's `schedulesPass`) before the peers stop and the board and the schedule store close, so a run that created its task and is still awaiting assignment delivery finds its bookkeeping (`ran`, `failed`) written to an open store. A run that arrives once stop has begun is refused by the control layer ("hub is stopping").
5177
+ - **`pause <id>` and `resume <id>`.** A `paused` column on `schedule_runs` (added by `ALTER` on an older database, older rows read as not paused). The timer skips a paused schedule without a run and without counting a skipped tick. `resume` clears the flag and starts a fresh period at now, so a long pause is never replayed as a run the moment it ends, as a restart never is. A paused schedule still runs on `run <id>`: a pause stops the timer, not the person. `pause` of a schedule not yet seen records it, paused.
5178
+ - **View.** `ScheduleState` gains `paused`, which wins over every other state. `ahub status` reads `paused (ahub schedule resume <id>); would be next <time>`.
5179
+ - **Dashboard.** The UI snapshot carries `schedules` (the same views, ids, enums and times only; absent when none is configured, so an older page and an unconfigured project see no change). The Agents view has a read-only Schedules table, built from the existing `usage-table` classes so no style changes, with a next-run column worded by `scheduleNextLabel`: the time for `scheduled`, `Paused (would be <time>)`, `Due now`, `Due, waiting for task #N to close` or `Due, held`, and `unknown` for anything else, never an invented time. The dashboard has no run, pause or resume control: those stay the person's CLI commands.
5180
+ - **Not in this slice.** Folder triggers (#512c) and cron remain refused or absent.
5181
+
5182
+ #### Folder triggers (#512c) implementation notes (hub task #336: `folder_triggers`, `src/hub/folder-trigger.ts`)
5183
+
5184
+ - **Config.** `folder_triggers` is a separate machine-local key (not inside `schedules`, whose entries are time-based), in `MACHINE_LOCAL` and `NEVER_EDITABLE`, validated by the pure `foldersConfig` in the loader and in `config-apply.ts`. Entry: `{id, folder, glob?, class, title, detail?, owner?, queue?}`, at most 16. `glob` is a file-name pattern (letters, digits, `. _ -`, space, `*`, `?`; never a path) that matches whole names; a dot file never matches.
5185
+ - **Where the folder may lie.** `checkTriggerFolder` runs the corpus root check (`checkCorpusRoot`: `realPath`, vault refusal, `local.deny`, `pii.paths`) and then requires the real folder to lie inside the project or a declared `data.readonly` folder, both as real paths, so a link out of either is judged by where it lands. The answer is a reason string, never a throw; a refused folder records `last_error` and proposes nothing.
5186
+ - **Polled, never watched.** The pass runs on the schedules timer (`runSchedulesOnce`), under the same `schedulesRunning` guard as a schedules pass and `ahub schedule run`, so it never overlaps one and a recovery commit waits for it. While the hub is held it reads and records nothing, so the files wait for the next poll.
5187
+ - **Baseline.** A trigger's baseline is the files already in the folder when it starts. They are recorded and propose nothing, so adding a trigger to a full inbox does not flood the board. A folder too big to hash in one poll (below) is baselined over several polls: each records what it read, and the poll that reaches the end of the folder completes the baseline (`folder_poll.baselined`); until then nothing proposes, and a file that arrives meanwhile is part of the baseline. A refused folder never begins the trigger: when it becomes readable, what is in it then is the baseline.
5188
+ - **What is read, and the memory bound.** Regular files directly inside the folder (no recursion), in name order. The listing is never cut (a folder of more than 100000 entries is refused as not an inbox), so a file late in name order is reached once the earlier ones are recorded, however many hidden or unmatched names sort before it. A symlink is never read: one that leaves the folder is skipped as `symlink leaving the folder`, one that stays as `symlink`. A file on the denylist is skipped. The scan keeps no file's bytes: a file already recorded at its real path, size and mtime returns its recorded hash after an `lstat` and is not opened; any other is opened with `O_NOFOLLOW`, checked against its `lstat` (size and inode) and hashed in 64 KiB chunks from that descriptor. At most 200 files and 256 MiB are hashed per scan (the first file always is, so one file near the source cap still goes); the rest wait for the next poll. Registering a file reads that one file again (`readScanned`) and refuses it unless it still hashes to what the scan saw, so at most one file's bytes are in memory and a file that changed in between is tried again as what it became. A file whose bytes change without its size or mtime changing is not noticed (a `ponytail:` note names the upgrade: hash every poll under the same budget).
5189
+ - **What is new.** `folder_seen` in `hub.db` records, per trigger, the real path, size, mtime and SHA-256. A file is new when its hash is not recorded for that trigger, asked again for each file as the pass goes, so two files with the same new bytes that arrive together propose ONE task. A rename and a touch keep the hash, so they propose nothing; the same bytes under another name are the same file (the source registry keeps one record per hash too). Such a file is still recorded where it now is (`FolderStore.observe`: its new path, size and mtime, keeping the task its content already has), so the next scan finds it with `known` and does not hash it again; otherwise a pile of renamed, touched or duplicate files would spend the hash budget on every poll and a genuinely new file behind them would never be reached; a path whose bytes changed has a new hash and is new. A deleted file never proposes.
5190
+ - **What a new file does.** At most 20 per trigger per pass, in name order. For each, the hub screens it exactly as `ahub source add` does and registers it, in `registerScanned` (`screenRegistration`: `pii.paths` and `signals.pii_patterns` over the projected text, then `SourceRegistry.add`, private when flagged), the one function the daemon and the tests share, then proposes ONE task through `Tasks.propose` as the hub, behind the same hold fence as a schedule. The task's detail is `Folder trigger: <id>`, a fixed line, the person's `detail`, and `Source: <src id>`; it never carries the file's name or path (both can be private), so a PII file's task is as public as its id. A file is recorded only after its task exists: a failure or a hold between the two leaves it to be tried again, and later files wait behind it so order is kept. A crash in that window can repeat one task; it never loses a file.
5191
+ - **Not in this slice.** `plan.sources` validation of the task against the registry, a status or dashboard view of triggers, and recursive folders.
4366
5192
 
4367
5193
  ### Deliverables (#509)
4368
5194
 
@@ -4374,12 +5200,57 @@ The owner gave the #516 track its go-ahead on 2026-10-22. This section closes th
4374
5200
  - A corpus path that lies in, is, or contains an Obsidian vault is refused. The check runs on the `realPath` of the configured root, so a symlink into a vault is caught. It looks for a `.obsidian/` directory in every ancestor of that real path (a root inside a vault, such as `/vault/notes` when `/vault/.obsidian` exists), at the root itself, and below it. The index walk also skips no `.obsidian/` it meets; it refuses the root instead. Evidence it cannot read (an unreadable ancestor or subdirectory, or a realPath failure) refuses the path; it never certifies a plain folder. The reason names MCP: a vault is reached only through the Obsidian MCP tools, never read by the hub from disk.
4375
5201
  - AC3 (vault candidates) stays with agent-mem (tasks 112 and 122); the hub adds no vault reader.
4376
5202
 
5203
+ #### Knowledge corpus (#510a) implementation notes (hub task #331: `corpus.paths`, `src/hub/corpus.ts`)
5204
+
5205
+ Where the code states or fixes a detail the section above leaves open:
5206
+
5207
+ - **Config.** `corpus: { paths: string[] }` is validated by `corpusConfig` (`src/hub/corpus-config.ts`, pure, so the channel bundle that reaches the config schema never pulls in file-system code). `corpus.paths` is in `MACHINE_LOCAL` and `corpus` is in `NEVER_EDITABLE`, so only an untracked `config.local.json` sets it and a settings write cannot. At most 16 folders; each is an absolute or project-relative path, and `~` is refused rather than expanded. It applies at startup (the index is bound to its roots then).
5208
+ - **Root check (`checkCorpusRoot`).** Order: resolve the path with `policyRealPath` then `realPath` (a symlink into a vault resolves to the vault), then look for any entry named `.obsidian` at the real root and in every ancestor of it, then the deny rules (`isDenied` with `local.deny`), then `pii.paths` (`matchesPolicy`). Anything that cannot be read, resolved or listed refuses the root with its cause; only a clean read certifies it.
5209
+ - **Walk (`CorpusIndex.refresh`).** Bounded at 4096 folders, 100000 entries, depth 24 and 5000 notes of at most 256 KiB. Any entry named `.obsidian` (a folder or a file, folded spelling too) below the root refuses the whole root, and a tree too deep or too big to be shown free of a vault is refused, never cut short. A folder that `pii.paths`, the deny rules or `local.deny` protect is never listed or read, so a vault anywhere below it cannot be ruled out: **a root that holds a protected folder is refused**, naming the folder, the same fail-closed rule as for unreadable evidence (the owner decision). The person points `corpus.paths` at a folder with no protected folder in it. A protected note (a file) is only skipped and counted as `protected`, since a file cannot hold a vault. Symbolic links and multiply-linked files are skipped and counted, and notes are opened with `O_NOFOLLOW` and checked against the walked identity. A note matching `signals.pii_patterns` (or whose pattern cannot be evaluated) is not indexed.
5210
+ - **Index.** In memory, rebuilt at startup, incremental by device, inode, size and times: an unchanged note is not read again. It stores the id `note:<root label>/<path>`, title, frontmatter, tags, aliases, headings, wikilinks and a SHA-256, never the note's text, and it never writes into the folder. A refusal after a note was indexed drops that root's notes on the next refresh.
5211
+ - **Parser.** `parseNote` reads flat frontmatter (`key: value`, `key: [a, b]`, block lists), ATX headings and `[[target#heading|alias]]` / `![[embed]]` links outside code, with caps on headings, links, tags and frontmatter size, and it reports `truncated`. It is not a YAML parser (zero runtime dependencies); nested maps are ignored.
5212
+ - **Departures from the accepted plan.** `CorpusIndex.resolveLink` returns the matching `NoteRecord[]` (not ids), so a caller gets the title and path without a second lookup. `CorpusContext` has no `io` seam: the tests use real temporary folders, so the walk, the identity check and the vault refusal are exercised for real, and `piiPaths` and `deny` are optional (absent means none). Neither changes what the index accepts or refuses.
5213
+ - **Not in this slice.** No daemon wiring or status line yet, no `ahub ask` or brief citations (AC2), no vault candidates (AC3, agent-mem) and no PII task handling downstream (AC4). The index is a library the next slice calls.
5214
+
5215
+ #### Corpus evidence in ask and task briefs (#510b)
5216
+
5217
+ - The corpus index retains structure, not bodies. Evidence reads reopen selected notes through current vault, deny and `pii.paths` checks, regular-file/no-follow identity and hash checks, and a full-text privacy screen before selecting any excerpt. A policy revision creates a fresh index. A PII question or private task context never queries corpus notes or shared observers.
5218
+ - Retrieval is seeded from screened title, path, tags, aliases and headings. At most 32 candidate bodies are read, six evidence items are returned, each text is at most 768 UTF-8 bytes, and complete corpus evidence including ids fits 4096 bytes. A cut excerpt is marked. This bounded metadata-first slice does not claim a full-text search index.
5219
+ - Evidence ids use `note:<root-label>/<path>#<heading-slug>`. Slugs preserve Unicode letters/numbers and deterministically disambiguate duplicates, including the headingless `top` section. Backslashes, commas, brackets and fragment delimiters inside id parts are escaped with a backslash; Unicode remains literal. Control-bearing or over-1024-byte ids are not published. Models must copy the complete escaped id exactly; invented note ids cause the answer to be discarded.
5220
+ - Corpus rows supplement amem brief rows within the existing item cap. Plan fails retain priority and library ordering remains authoritative. Delivery dedupe, policy/task freshness and private-turn guards apply before retrieval and again before observation or delivery. Corpus rows are not counted as native memory rows.
5221
+ - A PII ask remains evidence-only when campus status is false or unknown. On a positively confirmed campus gateway it uses `onCampusOnly: true` and pins the host's `routing.local.fixed_model` before any gateway await, using the existing repository on-prem rule. A changed socket, caller-private context or policy invalidates the result, as does a change to the PII or note-PII verdict of any gathered task row; unrelated board writes and public history/status changes do not invalidate snapshot evidence; a failed campus-only transport never falls back to a cloud call. No stronger provider trust tier is introduced here.
5222
+
4377
5223
  ### On-prem web reading (#515), owner decision 2026-10-22
4378
5224
 
4379
5225
  - `web_fetch` is refused for the whole turn of a PII task, whatever the host. The outbound URL or query could carry PII to the web, and `pii.paths` and `sanitize()` do not cover outbound requests.
4380
5226
  - Outside PII turns it runs only through the egress proxy, to hosts in a machine-local per-project allowlist, and returns bounded text only.
4381
5227
  - `doc_read` is unaffected by this rule; it keeps its own `pii.paths` and `data.readonly` checks.
4382
5228
 
5229
+ #### On-prem document reading (#515a) implementation notes (hub task #320: `doc_read`, `src/local/doc-read.ts`)
5230
+
5231
+ Where the code states or fixes a detail the section above leaves open. `web_fetch` is a separate task and waits for its owner decision.
5232
+
5233
+ - **Opt-in.** `local.doc_read: true` (machine-local: in `MACHINE_LOCAL`, and never editable from settings) gives the local worker a `doc_read` tool. Off by default: `DOC_READ_SCHEMA` is not in `TOOL_SCHEMAS`, the worker offers it only when the daemon passes a `docRead` port, and a model that names it anyway gets "doc_read is not enabled". Pi shares `TOOL_SCHEMAS` and does not get it.
5234
+ - **What it reads.** The model names `path` (and `offset`, `limit` as for `read`). The path goes through `guardPath` in read mode, the same guard as `read` and the one #418 shares (inside the project after `realPath`, no link out, no secrets denylist, `local.deny`). Then the file must be a regular file with one name and at most 16 MiB. The converter is the person's `text_projection` entry for the extension (PDF, DOCX, HWPX are whatever the person configured), run by `TextProjections` in its seatbelt profile on a private copy of the bytes, bounded by its input, time and output caps and cached by content in the hub state directory. An extension with no entry is refused with the way to configure one. The hub never picks a converter.
5235
+ - **Screens, in order.** `pii.paths` by the document's path (`matchesPolicy`, so globs and folded spellings count): an ordinary turn is refused before anything is converted. After conversion, a text matching `signals.pii_patterns` (or a pattern that cannot be evaluated) is refused the same way. A PII task turn is the case these refusals point to, so it reads both: that is what the on-prem worker is for. Then `sanitize()` quotes any line that forges a hub header, and the output is cut at the shared output cap with how to continue (`offset`).
5236
+ - **`data.readonly`.** A read is not a write, so the policy is not a reason to refuse. What it guards holds: the converter runs read-only in its sandbox, its cache is in the hub state directory, and `doc_read` writes nothing beside the document.
5237
+ - **Never memory evidence.** Converted text is document content (a converter can surface hidden text), so the result is excluded from claude-mem capture and from the research observation on every turn, not only a PII turn.
5238
+ - **Tests.** Fakes stand in for the projections (the converter itself is `text-projection.test.ts`'s subject); one macOS leg runs a real shell converter through `TextProjections`. The live leg on the on-prem worker with a real PDF, DOCX and HWPX converter is a separate person leg.
5239
+ - **Not in this slice.** `web_fetch`, which waits for an owner decision, and an async conversion: it is synchronous in the daemon process, bounded by the converter's 8 s timeout (ponytail comment in `doc-read.ts`).
5240
+
5241
+ #### On-prem web reading (#515b) implementation notes (hub task #332: `web_fetch`, `src/local/web-fetch.ts`)
5242
+
5243
+ Where the code states or fixes a detail the section above leaves open. `doc_read` is unaffected.
5244
+
5245
+ - **Opt-in, per project.** `local.web_fetch: { allow: [hosts] }` in `.agenthub/config.local.json`. The tool exists for the local worker only when `allow` names at least one host: with the default (an empty list) `WEB_FETCH_SCHEMA` is not in the worker's tool list, no proxy is started, and a model that names the tool anyway gets "web_fetch is not enabled". Machine-local (`MACHINE_LOCAL`, so a committed config is ignored), never editable from settings (`NEVER_EDITABLE`), bound when the hub starts (`LOCAL_RESTART_REASONS`). Pi shares `TOOL_SCHEMAS` and does not get it.
5246
+ - **The allowlist.** Each entry is a host name, optionally `:port` (443 unless named): letters, digits, `-` and `.`, at least one dot, never a scheme, path, wildcard, address or bare top-level name. At most 64, lower-cased and de-duplicated. It is checked by the pure `webFetchAllow` (`src/local/web-fetch-config.ts`, imported by the config schema, so it stays free of the disk and process modules the Claude channel bundle must not reach) when the config loads. A name also allows its subdomains, by the same `allowed()` rule the proxy uses.
5247
+ - **PII, first.** `webFetch` returns the same fixed refusal for the whole turn of a PII task before it reads the URL, the allowlist or the proxy, whatever the host: nothing about the request changes the answer or the timing, and no request is made. The worker passes the turn's PII flag into the tool context (`toolContext`), as it does for `doc_read`.
5248
+ - **Only through its own egress proxy.** The daemon starts a second `startEgressProxy` for `web_fetch` whose list is `local.web_fetch.allow` and nothing else (it follows a reload through `allowFn`). It is never the bash proxy: that one carries `network_allow` (package registries and code hosts by default), so listing a host for a model to read opens nothing to a shell command and the reverse. The proxy keeps its own rules: CONNECT only (TLS stays end to end), and a listed name that resolves to a loopback, private or link-local address is refused. With no proxy running the tool makes no request.
5249
+ - **What the model controls.** Only `url`, `offset` and `limit`. The request is a bare GET with a fixed `Accept` and `User-Agent`: no method, header, body or proxy from the model. The URL must be https, at most 2000 characters, with no space or control character, no user name or password, no address literal, and a host (and port) on the list; a fragment is dropped. Redirects are not followed by the client (`redirect: "manual"`): each `Location` is resolved and checked against the same rules before it is requested, at most three.
5250
+ - **What comes back.** Only `text/*`, JSON and XML types are read; anything else is refused without reading the body. The body is read at most 512 KiB deep (the stream is cancelled past it, never drained) within 15 seconds. HTML is reduced to text (scripts, styles, comments, `svg`, `head` and tags go, block tags break lines, a few entities are decoded), then `sanitize()` quotes a line that forges a hub header and the output is cut at the shared output cap with how to continue (`offset`). A failure is a fixed sentence plus at most an error code, never the error text.
5251
+ - **Never memory evidence.** The result and the URL are excluded from claude-mem capture and from the research observation on every turn, like `doc_read`: a page is a third party's text and the URL is the model's own request.
5252
+ - **Not in this slice.** There is no `ahub` command for it: the list is edited in `config.local.json`. A request is not asked of the person (the allowlist is the person's consent, given in advance). `robots.txt`, caching, cookies, authenticated pages and non-text types are not handled.
5253
+
4383
5254
  ## Amendment: roles per SDLC stage (#525)
4384
5255
 
4385
5256
  Written against main `8efdba48` (v0.12.45 + #671). Claims about current code carry `file:line` at that commit. "Probe" means a read-only `bun` run of `validateInstances` / `resolveSettledRoles` against that commit on 2026-10-22. Anything marked unverified was not run or read.
@@ -4524,9 +5395,9 @@ Headless and terminal forms of one kind coexist as separate ids. Neither reviews
4524
5395
  - Reviewer: done (#535, `review_mode: "fresh"`); native continuing `["reviewer"]` instances keep #519's rule. No change.
4525
5396
  - Planner: a Codex instance whose roles hold `planner` and neither `implementer` nor `verifier` is a planner instance. It is accepted today (probe); this amendment adds:
4526
5397
  - no worktree (refused, it plans on the project checkout);
4527
- - its app-server threads start with the read-only sandbox. Unverified: the adapter's thread start sandbox parameter (`src/adapters/codex-appserver.ts` has no `sandbox` match at `8efdba48`); the slice adds it and refuses the start when the thread's reply does not confirm read-only (unknown is never read-only). The attached TUI is the person's and may change it; recorded as a ceiling;
5398
+ - its app-server threads start with the read-only sandbox: the host supplies a `CodexOptions.threadSandbox` value or host-only live getter (`"read-only" | "unavailable" | undefined`), and both `thread/start` and `thread/resume` send `sandbox: "read-only"`. The matching native reply must carry the known top-level `sandbox: { type: "readOnly", networkAccess?: boolean }` policy; missing, malformed or writable policy refuses that thread RPC and closes the unconfirmed link before adoption or hub delivery. The live getter is rechecked at thread admission, reply and hub injection: a live role transition cannot reuse a writable or unconfirmed thread as a planner, and a planner with a declared worktree is unavailable. Getter failure is unavailable. The hub start RPC only opens the proxy, never creates a host thread or waits for a TUI-created one. Request/response shapes are model-free schema evidence from Codex CLI 0.164.0, not a native/account execution receipt. Unknown is never read-only. The attached TUI is the person's and may change later native turn policy; this remains a ceiling, not a locked sandbox claim;
4528
5399
  - owner admission already refuses it for code classes (no implementer role, `routing.ts:401`); it may own `plan`-class tasks.
4529
- - Claude planner instances stay channel instances (I3). For a declared planner-only Claude instance the facts hook grants nothing for mutating file tools or shell, whatever its permission mode; Claude's own prompt goes to the person in that terminal. The builtin `claude` is unchanged.
5400
+ - Claude planner instances stay channel instances (I3). For a declared planner-only Claude instance the authenticated facts pre-reply carries `mutationGrants: false` and lowers the existing `permission` field to `ask` for every non-read call, so older hooks also receive no automatic mutation grant without changing the person's runtime mode; the hook grants nothing for mutating file tools, shell or unknown tool kinds, whatever its permission mode; Claude's own prompt goes to the person in that terminal. The builtin `claude` is unchanged.
4530
5401
  - Planning writes go through `Tasks` as today.
4531
5402
 
4532
5403
  ### R7. Visibility
@@ -4639,7 +5510,7 @@ resumeFromRoster?(): Promise<void>;
4639
5510
  /** Fixed builtins only: a person's frontend attaches to the native session through this hub proxy. */
4640
5511
  readonly frontendProxy?: FrontendProxyPort;
4641
5512
  /** Fixed builtins only: a fixed operator notice; never native ids or provider text. */
4642
- readonly statusNotice?: { code: "unidentified-cancellation"; message: string };
5513
+ readonly statusNotice?: { code: "unidentified-cancellation" | "qwen-detached" | "qwen-stalled"; message: string };
4643
5514
  /** Fixed builtins only, and only with session.nativeRevert: roll the native conversation back to before this native turn. */
4644
5515
  revertTurn?(nativeTurn: string): Promise<void>;
4645
5516
  ```
@@ -4671,7 +5542,7 @@ Who sets what (parity by construction; each value is today's member, renamed at
4671
5542
  | `detachForRoster` | `WsPeer` (ignores the mode, `channel-peer.ts:91`), `CodexPeer` (`codex-appserver.ts:473`), `PiPeer` (`pi.ts:471`) | `daemon.ts:4758-4760` |
4672
5543
  | `resumeFromRoster` | `PiPeer` (`pi.ts:496`) | `daemon.ts:4773` |
4673
5544
  | `frontendProxy` | `CodexPeer`: `url` = `proxyUrl` (`:137`), `attached` = `proxyAttached` (`:488`), the two methods as at `:494`, `:500` | `daemon.ts:3701`, `:5198-5280` |
4674
- | `statusNotice` | `CodexPeer` (`codex-appserver.ts:99`), unchanged | `daemon.ts:2791` |
5545
+ | `statusNotice` | `CodexPeer` (`unidentified-cancellation`) and the existing `QwenDualPeer` (`qwen-detached`, `qwen-stalled`), unchanged | fixed-builtin-gated status projection |
4675
5546
  | `revertTurn` | `CodexPeer.revert` (`codex-appserver.ts:610`) | `daemon.ts:2446`, `:2459` |
4676
5547
 
4677
5548
  Per-port absent and undefined semantics:
@@ -4924,7 +5795,7 @@ Common acceptance criteria:
4924
5795
  - `frontendProxy` undefined refuses permission changes and `already`.
4925
5796
  - The `unidentified-cancellation` notice shows only from a fixed builtin.
4926
5797
  - The Codex sticky-override, restoring-turn, detached-clear and attached-refusal tests stay unchanged.
4927
- - `isCodexPeer` rows leave the table.
5798
+ - The proxy/startup/revert `isCodexPeer` rows leave the table. The later `actionEnforcement` branch remains intentional: it reads Codex `actionGated` to choose the existing enforceability refusal. It is outside 2d and waits for a separately specified action-policy port; no `actionGated` member is added to `FrontendProxyPort`.
4928
5799
  5. **2e: Claude launch proof extraction.** Area note: `daemon.md` (Claude launch and hook rules). Size M.
4929
5800
  - Paths: new `src/hub/claude-launch-proof.ts`, `src/hub/daemon.ts` (hello, `permissionDisplay`, `changePermission`, facts), tests.
4930
5801
  - No change to `src/cli/launch.ts`, `src/cli/facts-hook.ts`, `src/adapters/claude-channel.ts` or the plugin bundle.
@@ -5072,6 +5943,8 @@ Periodic roster and supervision reads use pending delivery metadata/rows rather
5072
5943
 
5073
5944
  Slice 525-5 is implemented. The temporary #735 blanket refusal is replaced by the existing implementer-role and host owner-isolation gate for every write class. The bounded launch-proof reader binds the current daemon instance and launch id; owner proof additionally needs a private, single-link launch record, a matching recorded execution root, the MCP server's real cwd and the validated noncanonical branch worktree. The proof belongs to the exact attached claim generation and socket; a pending preface is not ownership, and detach, replacement or a changed launch record revokes it. Old/missing root evidence attaches without isolation. Facts grants for declared implementers and terminal-recovery publication require the current proof. Canonical planner/reviewer behavior and protocol 16 remain unchanged. The launcher preserves canonical AGENTHUB_PROJECT_DIR for hub identity while passing the execution root as native cwd. These are same-user process/record attestations, not a sandbox.
5074
5945
 
5946
+ Status isolation fields and their read-only stall projection use retained host bindings plus cheap current-owner/generation/socket checks. They perform no terminal-root Git validation and grant no authority. Filesystem/branch or launch-record drift may remain unseen in that display until the next routing admission, facts permission reply or terminal recovery owner proof; those paths still revalidate and revoke mismatches before effects. The status snapshot is passed explicitly through read-only stall evaluation only, never through an ambient mode or a request-controlled bypass. Unrelated external-instance evidence and ordinary checkout probes keep their existing contracts.
5947
+
5075
5948
  ## Amendment: planner walk and reviewer independence in `assign()` (#525-3)
5076
5949
 
5077
5950
  Slice 525-3 is implemented, as the "Purity" bullet of the #525 amendment specifies; this records what exists and what does not yet.
@@ -5079,7 +5952,7 @@ Slice 525-3 is implemented, as the "Purity" bullet of the #525 amendment specifi
5079
5952
  - **Planner walk.** `plannerWalk` (`src/hub/routing.ts`) is a pure function of the sorted settled holders, the task id, an optional named planner, an optional kept planner and a per-holder `blocked` reason. A PII task has no planner (`planner: none (pii: planning stays with the person)`). A named planner is exact: a name that is not a holder is refused in the trace and never replaced by rotation, and a named holder that is paused or offline is still the choice, with the state shown. Otherwise a recorded planner is kept while it is still a holder. Otherwise rotation starts at `task.id mod n` over the holders sorted by id and walks forward past unavailable holders; the denominator is every holder, so availability never renumbers it (the #519 rule); when all are unavailable the seed holder is taken, `rotation (none available)`.
5080
5953
  - **Inputs and outputs.** `assign()` takes `planners`, `namedPlanner` and `keptPlanner` and returns `Assignment.planner` and `plannerReason` (`rotation`, `named`, `kept`, `none`). Without `planners` it walks no planner, so a caller that does not pass them is unchanged. A holder is unavailable when the adapter role ceiling excludes planning, it is off roster, not attached, offline, paused under `budget_paused = skip_peer`, or failing: the same state inputs the owner and reviewer walks read. The walk is independent of both: planner and owner, or planner and reviewer, may be the same peer.
5081
5954
  - **Explain parity.** The walk's lines (` planner candidate X: ...`, then `planner: X (reason)`) are part of `assign()`'s trace, after the owner and before the reviewer. `Tasks.weights()` supplies `planners` (the rostered holders of the settled role map, sorted) to every `assign()` path, so `ahub route explain` prints exactly what assignment ran.
5082
- - **I-2.** A peer with an `accepted` or `done` history entry on the task never reviews it (`implementers`), whoever owns it now, a named reviewer and the declared reviewer pool included: refusal code `policy`, reason `implemented this task`. It is the last check in `blocked()`, so it only ever removes a candidate that was otherwise eligible: a former implementer that is offline, not attached, paused, failing or off roster is refused for that real reason and leaves the outcome as it was before I-2 (a task with no other reviewer still approves directly). When I-2 removes the last eligible candidate, the reviewer is the person and `reviewBlocked` is set (`every eligible reviewer implemented this task: the person reviews`); there is no fallback to an implementer. The person's own entries are not an implementation. A review-class and a PII task are unchanged.
5955
+ - **I-2.** A peer with an `accepted` or `done` history entry on the task never reviews it (`implementers`), whoever owns it now, a named reviewer and the declared reviewer pool included: refusal code `policy`, reason `implemented this task`. It is the last capability/independence check in `blocked()`, before queue admission, so it only ever removes a candidate that was otherwise eligible: a former implementer that is offline, not attached, paused, failing or off roster is refused for that real reason and leaves the outcome as it was before I-2 (a task with no other reviewer still approves directly). When I-2 removes the last eligible candidate, the reviewer is the person and `reviewBlocked` is set (`every eligible reviewer implemented this task: the person reviews`); there is no fallback to an implementer. The person's own entries are not an implementation. A review-class and a PII task are unchanged.
5083
5956
  - **Not in this slice.** Nothing records the planner: no board column, no history entry, no notice, no authority check. `Tasks` passes `planners` and reads nothing back; 525-2 owns recording and the per-task authority. A task proposed today behaves exactly as before.
5084
5957
 
5085
5958
  ## Amendment: deliverable records bound to content hashes (#509a)
@@ -5100,7 +5973,7 @@ The only artifact identity was GitHub (`repo`, `issue`, `pr`, `head`, `reviewedH
5100
5973
 
5101
5974
  The second slice of #509 (AC3): a person-facing list of what tasks delivered and what a reviewer approved, and a dashboard marker. Nothing new is stored: both read the records 509a already writes.
5102
5975
 
5103
- - **Command.** `ahub deliverables [--task <id>] [--approved] [--check] [--json]` is the person's (`identity.ts` classifies it `console`; the daemon's `deliverables` task op sits behind the console gate, so an agent's tools connection is refused; `liveTestRefusal` refuses the delegated form). Each row is a task that has delivered files, newest task first: `#id title [delivered|approved]`, then each file's id, path (only where `publicDeliverables` allows), format, version, size and full sha256. A PII task or a `pii.paths` file shows id, hash and size only, and the title is `publicTitle`. `--task` takes one positive integer (a repeated `--task` is refused rather than keeping the last); `--approved` lists only tasks whose approval bound records, and shows those approved bytes.
5976
+ - **Command.** `ahub deliverables [--task <id>] [--approved] [--check] [--json]` was the person's; since #933 it is Allowed, and an agent's list holds only the tasks `hub_task_show` would show it (#933: see the amendment "the console class keeps trust, money, attribution and terminals"). Each row is a task that has delivered files, newest task first: `#id title [delivered|approved]`, then each file's id, path (only where `publicDeliverables` allows), format, version, size and full sha256. A PII task or a `pii.paths` file shows id, hash and size only, and the title is `publicTitle`. `--task` takes one positive integer (a repeated `--task` is refused rather than keeping the last); `--approved` lists only tasks whose approval bound records, and shows those approved bytes.
5104
5977
  - **Measured or not.** Without `--check` no file is opened: the list is the board's records, `checked` is false and no row says anything about change. With `--check` every listed file is measured now through `Tasks.deliverableStatus` (the 509a reader, with its limits and its `unknown` for a file the hub cannot vouch for), each file shows `unchanged`, `changed`, `missing` or `unknown`, and the row says `changed since review` only for a measured difference. The row's own reading (`result`) follows the same order: it says `(unchanged)` only when every file did. Consumers of `--json` read `result`: `changedSinceReview` stays false next to `result: "missing"` or `"unknown"`, because it is true only for a measured difference. `Tasks.deliverableList` is the one reader.
5105
5978
  - **Dashboard.** The snapshot's task rows gain an optional `deliverables: {state, files, checked?}` (`Tasks.deliverableBadge`): the same public projection of the same records, never a file read, because the page polls every second and hashing up to 512 MiB per task on the poll is not acceptable. `checked: {at, result}` appears only when a live read (`task_show`, `ahub deliverables --check`) measured exactly these records; `result` is `changed`, `missing`, `unknown` or `unchanged` for the whole task, and `unchanged` only when every file measured unchanged (a change outranks a missing file, which outranks one the hub could not read, such as every file under an unverified execution root). The page and the CLI say `a file is missing` or `could not be measured` for those, never `unchanged`; `Tasks.deliverableResult` is the one reducer (the cache is keyed by the record ids and hashes, so a redelivery or a new approval clears it). The card shows a `Deliverables n` chip (with `approved` and `changed` when known); the task sheet gains a Deliverables group with each file's path (or `private file`), format, version, size and a twelve-character hash prefix, and says `not measured now: run ahub deliverables --check` when nothing was measured. `PROTOCOL` is unchanged: the field is optional and an older page ignores it.
5106
5979
  - **Interpretation.** The issue says "a dashboard column". The dashboard's board is kanban columns by task state with no per-task table, so a new column of its own has no place; the chip on the card and the group in the task sheet are the equivalent surface. Say so in review if a table was meant.
@@ -5174,3 +6047,260 @@ Ceiling: a scan lists the board once per status revision (at most one per flush)
5174
6047
  - `hub_task_show` also admits the configured planner (a peer holding the `planner` role, `Tasks.isConfiguredPlanner`, `ConductorHooks.isPlanner`) for any task outside the backlog (a staged backlog row stays hidden from agent task tools, #263, and is read through the planner tools; a peer that is also the conductor takes the audited conductor path). It gets exactly the conductor's view (`publicConductorTask` over `Tasks.publicView(task, true)`), so a PII task is still a stub with no history. The read writes no conduct audit event and grants nothing else: the planner cannot assign, escalate, hold or start through the conductor tools, and every other peer is refused with the conductor-role error as before.
5175
6048
  - `hub_task_cancel` by the planner keeps every check it had (its own proposal, `backlog` or `proposed` state, planner authority, no private turn on ordinary work, the flow revision, recovery and shutdown exclusion) and replaces only the acceptance test with the evidence test `hub_backlog_return_untouched` uses (`taskHasWorkEvidence`): a branch, commit, PR, recorded local check, done, verdict or reopen refuses with the unchanged text `planner may cancel only its own historically unaccepted backlog or proposed work`. An accepted-then-declined task with none of them (the observed case, #454) is cancellable; the history keeps the acceptance and the decline.
5176
6049
  - Reason: the planner could not read a task it had proposed once an owner took it, and could not clear the board of a task its owner declined untouched. The person's cancellation is unchanged.
6050
+
6051
+ ## Amendment: conductor-bound recovery continuation and task unassign (task 535, #900)
6052
+
6053
+ - A configured conductor may create a restart with `ahub restart --yes`, or an upgrade with `ahub upgrade --yes --to <exact-version>`, only when the source hub can issue the operation's private continuation proof. The parser still validates `--to` as an exact version. A target that requires handoff to a newer or otherwise unsupported coordinator is unknown for this purpose and remains person-only.
6054
+ - The source preflights every planned project for recovery support and current authority before staging. An upgrade stages and verifies its exact target next; a restart has no package stage. Only after target bytes and feature support are verified does the source issue the bounded, fixed-shape per-operation host HMAC record, before publishing the public operation, taking its lock or applying runtime holds. Its mode-0600 key and record stay in host state, separate from public recovery JSON and `delegatedAudits`. The seal binds operation id, initiating peer, project id and canonical root, source instance, target root/package digest, plan fingerprint and a version-neutral digest of bounded raw config/routing texts, source-resolved fallback routing bytes and the fresh actor/privacy decision. It does not hash expanded defaults, default roles or parsed routing; the public `conductorRoutingDefaults` map is advisory and the target inherits the sealed source fallback. The private record stores digests rather than raw config, routing contents or the HMAC key. Upgrade package identity includes the exact hoisted `@staix/agent-mem` dependency; restart uses its verified source dependency.
6055
+ - The record authorizes continuity only for the same conductor and its own source-created restart or authorized upgrade. Another conductor cannot adopt it, and a public or person-created recovery operation cannot be converted into conductor authority. The person retains the ordinary recovery path. `recovery resume|abort` may continue only that same bound operation; `recovery-run` is not a general conductor command.
6056
+ - A detached runner or target launcher may continue only the operation's captured, validated argv. No arbitrary CLI command, target selection, fresh-session override or other console authority is conveyed. `--live-test` is a separate existing invocation gate and does not issue this proof.
6057
+ - The host rechecks the initiating peer's current conductor role and roster membership, project/write authority, and non-PII/privacy policy before every effect and after awaits. Offline continuation is limited to bounded current actor/privacy evidence; unrelated public board writes alone do not invalidate it. Missing, changed or unreadable evidence stops the next effect before it occurs.
6058
+ - Captured TUI terminal restoration remains person-only: a delayed native launcher cannot re-prove the conductor at its eventual spawn. Conductor continuation refuses captured terminal launches; headless restoration uses the target-owned snapshot through a tools connection.
6059
+ - An unmanaged reconnect-only Claude channel may reattach itself without a native launch. The source keeps a SHA-256 fingerprint of its observed session identity separate from restoration metadata (the raw unmanaged session id is omitted from the plan) and verifies the exact peer, kind and attachment against the live channel. This exception requires no terminal or restoration arguments, no managed launch proof, and an absent or confirmed-ended launcher; unknown launcher evidence refuses. Other peer identities still compare exactly.
6060
+ - Recovery does not adopt unpublished protected config or routing changes. Candidate policy must match the source's accepted running policy. The replacement validates the exact captured startup inputs against the private grant before adapter imports and uses those same inputs for config and routing publication; a later file edit cannot replace the verified snapshot. Changed or unknown policy requires the person.
6061
+ - The proof is a same-host integrity mechanism, not an OS boundary: a process able to replace both the host key and its private record can replace the seal. Native and real-account receipts are not implied by fixture tests.
6062
+ - `task unassign` is also a conductor operation, executed and recorded as the actual conductor. It does not route a replacement or stop native work. An owner unassign keeps automatic offers suppressed until a later authorized explicit assignment or reservation clears that suppression; other task eligibility and planner rules are unchanged. Open tasks with host-marked conductor assignment history require a `conductor_recovery` target even for person-run recovery, so a downgrade cannot erase manual intent.
6063
+ - Ordinary peer classifications do not expand: `backlog list`, `release list` and `sdlc costs|metrics` remain Console for agents; existing public read forms and dry-run previews are unchanged.
6064
+
6065
+ ## Accepted contract: external module-kind instances (#754)
6066
+
6067
+ **Status: accepted on 2026-10-27.** The [person's decision on #754](https://github.com/STAIxBWLB/agent-hub/issues/754#issuecomment-6441781951) selects O1 explicit module opt-in, O2 implementer/verifier/ordinary continuing reviewer, O3 live startup catalog only and O4 retained live-module recovery refusal. The contract-only deliverable was board task #287 (PR #892), against main `7a599197`; it enabled no source or feature. Implementation is board task #529, with field names frozen below and A1-A8 as its delivery gate. The original A4 blanket instance refusal applies to earlier hosts; this implementation replaces it only for opted-in, host-verified instances. Source/fixture support is distinct from native/account receipts.
6068
+
6069
+ ### Existing boundary and accepted scope
6070
+
6071
+ - Keep person-owned, untracked machine-local bundle declarations, content pins, daemon-startup-only imports and immutable project catalogs. CLI reads, previews, reloads and watchers never evaluate a module. A tracked, shared, cloned or unknown-provenance declaration grants nothing.
6072
+ - Keep the A4 executable subset: headless ACP, the public `AcpPeer` transport, bound peer id, `deliveryOnly === true`, `hubOwned === true`, `form === "headless"`, verified owned-group admission and load-session echo. A module pin authorizes daemon-privileged code; it is not a sandbox or a proof that an approved module cannot be malicious. An import timeout is not cancellation or rollback of privileged initialization.
6073
+ - Keep builtin namespace reservations and every fixed-builtin capability exclusion. No native engine, native/fresh-reviewer variant, host proof port, native home/model/provider selector, native hook, budget callback or arbitrary configuration callback becomes an external-instance capability. `instances` policies reserved for builtin definitions do not become module-supplied callbacks.
6074
+ - The accepted first slice requires explicit module opt-in and permits implementer, verifier and ordinary continuing reviewer instances within an explicit module-role subset and the kind ceiling. Every instance has its own managed worktree. Existing reviewer independence and MCP restrictions apply; planner, conductor and fresh/isolated reviewer capabilities remain excluded, with no claim of fresh-reviewer isolation. Semantic validation uses the live startup catalog, and live external controlled recovery remains refused.
6075
+
6076
+ ### Stable module identity and a current startup binding
6077
+
6078
+ - A kind name, display label, instance suffix, roster row or adapter-reported generation is not module identity. Startup retains the provenance of each validated definition alongside its captured bundle: canonical project root, normalized project-relative declaration path, full lowercase SHA-256, exported kind, `ADAPTER_API` and a digest of the validated data-only capability/permission ceiling. Duplicate kinds, builtin/configured namespace collisions, wrong hash/API/kind, unsupported fields and unknown provenance refuse independently before any affected native start or bus admission.
6079
+ - Resolve `instances.<id>.kind` only against that daemon's verified startup catalog. Its module binding is the exact retained tuple, not a later path lookup or an entry from the process-global builtin registry. No kind is inferred from an id prefix to repair a missing declaration, and no missing external kind falls back to a builtin, configured kind or another module with the same label.
6080
+ - Keep stable identity separate from liveness: a new daemon has a new catalog epoch and new owner generations. Recovery may compare an exact stable tuple, but it never reuses an old catalog epoch as current-owner proof. Pin, declaration-path, exported-policy or instance-policy changes invalidate continuity; no automatic repin, path relocation, native-session rebinding or fresh-session fallback is implied.
6081
+ - Module and instance declarations remain restart-bound. A reload can report pending changes and revoke affected authority when its evidence no longer matches, but cannot import, repin, create a new catalog or silently publish the desired replacement policy as running policy.
6082
+
6083
+ ### Implementation field and schema names (#529)
6084
+
6085
+ - Module opt-in is `AdapterDefinition.headlessInstances?: { form: "headless"; roles: readonly ("implementer" | "verifier" | "reviewer")[] }`. Only own-data values within the module's roles are admitted. Raw builtin `instances` policy remains forbidden by presence; the host derives its internal headless policy after validation.
6086
+ - `CreateContext.instanceStateDir?: string` carries the private host-derived namespace at `<stateDir>/instances/<id>`; `stateDir` remains canonical daemon control/MCP state. An instance factory must pass the exact host `daemon.acp` options object to the public `AcpPeer`; copying or replacing those options cannot mint a managed-spawn receipt. MCP children receive a fresh launch id and remain mutation-fenced until owned startup admission completes.
6087
+ - `ExternalInstanceBinding` schema 1 has exactly `schema`, `projectRoot`, `instance`, `kind`, `modulePath`, `sha256`, `api`, `capabilityDigest`, `policyDigest` and `executionCwd`. Paths and module pin are captured at verified startup, digests cover normalized data-only capability and instance policy. Recovery peers and restart snapshots carry it as `externalInstance`; integrity includes `externalBindingsDigest`. A private `external-instance.json` contains `{ externalInstance, sessionId }` for later same-binding manual resume. A supplied session id requires an exact retained binding; a mismatch refuses before native effects. After restarting to capture changed declarations, a person may instead request a fresh start under the current verified binding, and successful admission replaces the record. A fresh native session never applies instance-policy edits to an already running daemon. These records carry no runtime permission choice or current-owner proof.
6088
+ - The combined feature is `external_adapter_instances`, requiring `external_adapter_modules` and `peer_instances` even for stopped declarations. The bounded data-only status projection is `externalInstanceCatalog` schema 1, scoped by canonical `projectRoot`, daemon `epoch`, policy `fingerprint` and at most 64 instance rows. Authenticated `ControlClient.readExternalInstanceCatalog(projectRoot)` validates its live epoch and features; it never creates executable definitions. Offline declarations remain separate `declaredExternalInstances` rows with fixed `catalog-unavailable` refusal codes. A kind missing from the live startup catalog stays inert with `kind-unavailable`: the host cannot distinguish a misspelled builtin from a refused module kind without guessing its identity.
6089
+
6090
+ ### Instance declaration and opt-in policy
6091
+
6092
+ - Instance ids retain the existing `<declared-kind>-<name>` grammar, nonreserved ASCII id bound of 32 characters and project-wide maximum of 64 instances; modules retain their 16-entry bound. A kind too long to admit a valid instance id refuses; ids are never truncated or rebound. Existing builtin/configured declarations keep their current rules.
6093
+ - Under accepted decision O1, a newly pinned module explicitly declares a bounded own-data headless-instance opt-in. Its proposed logical fields are headless form and an allowed role subset. This is a new reviewed external data subset, not permission to declare builtin `instances`, reviewer backends or executable policy callbacks. Field/API compatibility names must be frozen in the separate implementation task plan; older hosts must clearly refuse the new subset rather than treat it as singleton support.
6094
+ - An instance declares only its kind, role subset, headless form, managed worktree and the existing permission/read-only selection. Command, argv, provider, model, native data home, environment overrides, launch callbacks and fresh review mode are outside the first slice. The host must newly derive a private per-instance state namespace and pass it with the exact instance id, execution root and MCP binding. Today the ACP context carries the shared daemon `stateDir`; it does not already provide that isolation. Keep canonical daemon control/MCP state separate from private instance state, and freeze the context field carrying the new namespace before implementation; config cannot choose a callback or alternate SDK.
6095
+ - Roles are intersected with the captured kind ceiling, the accepted instance-policy ceiling and host role rules at config validation, effective-role derivation, routing and tool admission. A later id-keyed role list cannot restore a refused role. Conductor and builtin-only planner eligibility stay excluded. Accepted O2 includes ordinary continuing reviewer instances alongside implementers and verifiers, only under the explicit module-role subset and existing reviewer-pool independence and MCP restrictions. An external reviewer instance never reviews a task it owns or implemented; the current reviewer-pool family and same-instance rules apply unchanged, as for builtin reviewers. Fresh mode and isolated builtin reviewer capabilities remain refused; every reviewer instance has its own worktree, without any claim of fresh-reviewer isolation.
6096
+
6097
+ ### Execution root and non-transferable current-owner proof
6098
+
6099
+ - Every external instance, including a verifier, names its own existing `.worktrees/<id>` directory. This is intentionally stricter than today's generic verifier fallback. The host checks real directory identity, exact registered worktree root, same Git common directory and non-prunable inventory; no creation, repair, another checkout or canonical-root fallback occurs. A potentially writing instance requires an attached branch. Detached read-only execution needs an explicitly accepted read-only policy and cannot be used as write-owner evidence.
6100
+ - Before native spawn, freeze the module/instance binding and the validated cwd. An affected instance joins the bus or gains `ownerIsolation` only after the host has current evidence tying that exact runtime object and owned-child launch generation to that cwd, module tuple, instance policy and daemon/catalog epoch. A native session or MCP environment string alone cannot establish it.
6101
+ - The implementation must obtain that receipt from the actual managed ACP spawn/owned-process path, through a host-retained non-virtual binding, not a module's `recoveryMetadata().launch.cwd`, arbitrary getter or returned "proven" flag. Bypassing the managed spawn, returning the wrong transport/id/options or failing owned-group verification leaves no admissible receipt and refuses instance admission. A receipt describes the managed runtime; it does not confine daemon-privileged module code or all native tools to a filesystem sandbox.
6102
+ - Recheck current bus owner, child generation, startup binding, policy and the registered worktree before write/task-owner admission, after intervening awaits and immediately before effects. Replacing a directory, switching the attached branch inside its worktree (including a switch to another attached branch), changing the module/policy binding, losing the owned process or receiving unreadable evidence revokes authority. A branch switch requires an explicit stop and person start to bind the new branch; a commit advancing the same branch does not. An unavailable check is unknown, never isolation; retain a failed-start owner with unconfirmed processes for explicit teardown rather than assigning its id to a replacement.
6103
+ - Give every instance its own host records, queue/task identity, native session binding and MCP identity. Child environments replace inherited agent markers with the exact instance id. MCP access requires a host-issued current launch binding, rechecked after awaits; an old child, a base-kind child or a client that echoes another instance's env cannot act for it. Native configuration/credential-home isolation is not implied by worktree isolation, and no automatic native-home copying or credential migration is introduced.
6104
+
6105
+ ### Permission ceilings and lifecycle
6106
+
6107
+ - A per-instance choice is machine-local and no wider than the captured module kind's native mapping and accepted host ceiling. An explicit managed mode must be advertised and acknowledged by that native session before bus admission. Missing mapping, failed acknowledgement or unknown state refuses that selection; it never becomes an implicit allow or a falsely claimed native `ask`. An omitted choice may retain the kind's existing unmanaged behavior, visibly unmanaged.
6108
+ - `readOnly: true` requires the kind's supported native read-only mapping and observed acknowledgement. It also applies the existing host ACP read/search/think restriction and read-only MCP/tool ceiling; neither confirmation, an allow-always choice nor session budgets raise it. Read-only and an independent `permission_mode` selection stay mutually exclusive. Neither read-only acknowledgement nor a pin claims kernel isolation.
6109
+ - Roster removal first fences dispatch, current tool authority and approvals, then stops the instance through the verified headless owned-process path. It does not detach a person's TUI or widen an external kind to a terminal form. Offline is insufficient if an owned process is pending or its absence is unknown: attempt only verified owned teardown, retain the owner/queue/in-flight uncertainty on failure, and perform no permission reset, task handoff, automatic restart or replacement until teardown is confirmed.
6110
+ - A successful removal preserves queued work, independent pauses and recorded history. Return to the roster does not itself start or resume a module instance. A later explicit start obtains a new owner/launch proof under the same accepted identity rules; stale tools and replies never revive a retired generation.
6111
+
6112
+ ### Bounded CLI metadata without code evaluation
6113
+
6114
+ - The current `{kind, displayName}` metadata is insufficient for external-instance semantic validation. A supporting daemon must project a separate data-only view of its verified catalog binding and instance policy. Do not manufacture `start.create`, `instances` callbacks or executable `AdapterDefinition` values from display metadata to make a validator pass.
6115
+ - Proposed public view fields are bounded instance id, kind, display name, role names, headless/read-only and permission-state enums, availability/refusal code and an observation epoch/fingerprint. Keep at most 64 instance rows, kind/id at 32 characters, display names at 256 UTF-8 bytes and fixed refusal codes; truncate display only with an explicit incomplete indication, never silently validate a truncated inventory. Full pins/declaration provenance belong in authenticated person diagnostics. Module bodies, callback source, argv, native ids, provider errors, secrets and arbitrary native text are excluded.
6116
+ - `status`, `help`, `route explain`, previews and upgrade planning consume declarations plus that bounded host projection without importing modules or invoking module callbacks. Metadata explains observed state; it cannot register code, grant permissions, prove current ownership or authorize a start. A mutating/start request revalidates the current authenticated daemon instance, feature set, startup tuple and declaration revision, not metadata echoed by its caller.
6117
+ - Accepted O3 requires live-catalog semantic validation and keeps module declarations as `{path, sha256}`. Without a matching daemon, raw declarations may be listed as declared/unknown but cannot be resolved to an executable instance, assigned a guessed ceiling or used to prepare recovery. A cached projection never becomes runtime authority merely because its hash or label matches.
6118
+
6119
+ ### Recovery compatibility and refusal before effects
6120
+
6121
+ - The implementation advertises daemon feature `external_adapter_instances`, in addition to the existing `external_adapter_modules` and `peer_instances`. Retained receipts and restart snapshots must carry a versioned stable binding containing canonical project root, instance id/kind, normalized module declaration path, full pin, API version, capability and instance-policy digests, and validated execution root. Integrity validation covers every field; any recorded native session is bound to that same tuple. Catalog epoch, runtime object identity and launch generation remain live host evidence, never portable ownership proof. Freeze the serialized schema in the separate implementation task plan and add the feature to every source/target incompatibility preflight. Old metadata cannot be silently read as that new proof.
6122
+ - A supporting coordinator/source must refuse a target missing any required feature before recovery holds, session capture, native stop, terminal close, commit, target startup or module evaluation for that operation. This applies even when the affected instances are offline: a target that cannot interpret their declarations/bindings is not compatible. Apply the same rule to reused staged targets, lease renewal and the last pre-commit read; once committed, retain the verified target/package binding rather than rediscovering identity from mutable config.
6123
+ - A source lacking the combined feature, an unreadable source feature/inventory response, or a declaration with no verified kind-to-pin binding also refuses before lifecycle effects. An older source's unknown/all-offline rows cannot waive the new external-instance floor. The CLI may conservatively report a possible unresolved module instance to refuse; that is not permission to invent its identity. Existing builtin/configured offline exceptions remain unchanged for those declarations.
6124
+ - The staged target's host features/SDK and captured declaration pins are checked without importing project modules. A supporting target later validates the same captured module tuple and policy before admitting any affected instance; changed/missing pin, incompatible API/policy, wrong project/root or incomplete retained binding refuses without reusing a session under another identity. Module evaluation already authorized at daemon startup is not rolled back; a post-import exported-kind mismatch must still prevent native spawn, bus admission and owner proof.
6125
+ - Accepted O4 retains A4's live-module controlled-recovery refusal. Stop affected module instances explicitly before the supported host restart/upgrade path; keep their stopped records and restore builtin peers normally. External instances remain stopped until a person's explicit start with the exact verified module/instance binding. A supplied recorded native session id is bounded and must receive a matching load-session echo; absent proof refuses and never falls back fresh. Automatic external-instance resume would require separately accepted source/target native-session, process-group and restoration receipts, not inference from `resumeById`.
6126
+ - Feature-aware binaries (and any explicitly designated compatibility backport) must reject unsupported module-instance declarations before project-module evaluation or native start. Already released A4 binaries cannot provide this guarantee: at baseline `7a599197`, daemon startup imports pinned modules before validating instance declarations, so direct execution can evaluate a module and then refuse the instance. A supporting coordinator must refuse that target before starting it; direct legacy execution remains an explicit migration limitation, not a promised safe path. No client attaches an instance id without the combined feature. Removing a declaration to cross an initial unsupported-source upgrade is an explicit person action with stopped/history evidence, never an automatic workaround; a backport/version floor requires separate acceptance.
6127
+
6128
+ ### Acceptance matrix for the later implementation
6129
+
6130
+ These are the implementation delivery requirements. The earlier contract-only PR #892 did not run them or enable support; #529 reports their exact-head results separately. Use fake bundled modules, fake ACP agents, isolated homes and host-side effect counters first; real native concurrency/session restoration needs its own declared receipt.
6131
+
6132
+ | Claim | Required positive and negative proof |
6133
+ |---|---|
6134
+ | A1 mixed catalog/identity | A builtin extra instance and implementer, verifier and ordinary continuing reviewer instances of an opted-in external kind coexist with distinct roots, native sessions, records, queues and MCP identities. The reviewer is admitted only within the explicit module-role subset and existing reviewer independence/MCP restrictions, with its own worktree and fresh mode refused. Missing opt-in, duplicate/colliding kinds, kind/pin/API/policy changes, ambiguous namespace and malformed/over-limit declarations refuse without rebinding. |
6135
+ | A2 current execution owner | Registered exact worktrees succeed. Missing, symlink-replaced, prunable, foreign-repository, canonical-fallback and detached-writer roots refuse. Swap the root/pin/policy or owner across each await; no stale write/owner admission occurs. A factory that lies in metadata or bypasses managed spawn cannot mint owner proof. |
6136
+ | A3 policy ceiling | Implementer, verifier and ordinary continuing reviewer roles and id-keyed overrides cannot exceed the explicit module-role subset or accepted kind/instance ceiling. Continuing reviewers cannot review tasks they own or implemented; existing reviewer-pool family, same-instance and MCP restrictions apply unchanged, and fresh mode is refused and planner/conductor remain excluded. Missing or unacknowledged native modes, read-only escalation and mutating MCP operations refuse before effects. Builtin-only fields, fresh reviewers, TUI/native engines, native proof ports and config callbacks remain refused, including explicit undefined declarations where presence is forbidden. |
6137
+ | A4 no-import CLI | With import/callback tripwires, exercise online and offline `status`, `help`, `route explain`, preview and upgrade planning. No module evaluation occurs; unknown/stale/foreign-epoch/incomplete metadata cannot enable a factory, guess a ceiling or grant ownership. Outputs respect all row/byte bounds and contain no argv/provider/source data. |
6138
+ | A5 roster and process lifetime | Fence an active delivery and approval before stop. Repeat with offline-pending and unknown owned groups: no handoff/reset/replacement until confirmed gone; queued and uncertain work is retained. Return and stale MCP/reply attempts cannot revive an old launch. |
6139
+ | A6 source/target compatibility | Test missing features on source and target, unknown/all-offline old rows, retained/reused target mismatch, changed pins/API/root/policy and feature-aware/backported binaries reading unsupported config. Counter assertions show zero holds/captures/stops/closes/commits/new imports/native starts at a supporting preflight refusal. Separately cover the unpatched A4 startup order with a harmless import tripwire: it may evaluate the pin before refusing the instance, and must not be represented as satisfying pre-import refusal. Legacy builtin/configured recovery remains byte/behavior compatible. |
6140
+ | A7 supported recovery ceiling | Under accepted O4, live external peers refuse controlled recovery; explicitly stopped instances stay stopped while compatible builtin recovery completes. Manual same-binding session load requires exact echo; missing/foreign echo and stale native ids refuse with no fresh fallback. Automatic resume is outside the accepted first slice; any later extension needs separate acceptance and its additional receipt matrix. |
6141
+ | A8 delivery gate | Amend the relevant source/target feature checks and docs in the implementation PR; focused mixed regressions, conformance, affected seeded guards and the full `scripts/check.sh` pass at its head with `check: OK`, as required by issue #754 AC5. The earlier docs-only contract PR used documentation checks; that evidence does not satisfy this implementation gate. Report native/account checks and unknown coverage separately from fake/local/hosted evidence. |
6142
+
6143
+ ### Decisions
6144
+
6145
+ The [person accepted these choices on 2026-10-27](https://github.com/STAIxBWLB/agent-hub/issues/754#issuecomment-6441781951). The acceptance authorized the separate implementation task #529 under the required proofs; it enabled no source behavior by itself.
6146
+
6147
+ | Decision | Accepted first-slice choice |
6148
+ |---|---|
6149
+ | O1 opt-in authority | Require an explicit bounded headless-instance policy in a newly pinned module; an existing singleton module does not acquire multiple ids by config alone. |
6150
+ | O2 initial roles | Implementer, verifier and ordinary continuing reviewer within an explicit module-role subset and the kind ceiling, with existing independence and MCP restrictions. Every instance has its own worktree. Planner, conductor and fresh/isolated reviewer capabilities stay excluded; no fresh-reviewer isolation is claimed. |
6151
+ | O3 offline declaration binding | Keep module declarations as `{path, sha256}` and require the matching authenticated live startup catalog for semantic instance resolution; offline rows remain declared/unknown. |
6152
+ | O4 controlled recovery scope | Preserve the live-module refusal. Stop the instance explicitly, then a person starts it again with the same binding; there is no automatic resume. |
6153
+
6154
+ ## Queue backpressure for automatic assignment (#800)
6155
+
6156
+ `tasks.queue_backpressure` is an opt-in machine-local config block. It does not
6157
+ change the shipped routing template or frozen routing corpus. Its independently
6158
+ optional integer fields are `max_queued` (0..10,000 envelopes) and `max_oldest_s`
6159
+ (0..604,800 seconds). Each defaults to zero, which disables that threshold. Trusted
6160
+ config layers merge these fields independently. A committed project cannot enable
6161
+ the policy, and the person-only settings controls write untracked local config.
6162
+ Reload applies the policy live. Zero disables a guard, so these controls have no
6163
+ conductor-editable permissive floor.
6164
+
6165
+ The guard applies only to new automatic owner/reviewer offers. Counts strictly
6166
+ above the configured maximum hold an offer; equality is allowed. An oldest pending
6167
+ age strictly above its threshold also holds. Missing, invalid, future or otherwise
6168
+ unproven age holds when age policy is enabled, rather than becoming zero or fresh.
6169
+ A verified empty queue has no pending age and is allowed. Unknown count holds when
6170
+ a queue policy is enabled. The fixed reasons are `queue-count`, `queue-age` and
6171
+ `queue-age-unknown`. A queue-blocked automatic reviewer walk never turns into direct
6172
+ approval: it remains blocked for a person or a later eligible reviewer.
6173
+
6174
+ The pure `assign()` receives policy, pending metadata and one host clock as data.
6175
+ `Tasks` uses the same inputs for real assignment and `ahub route explain`; routing
6176
+ never reads a journal or clock itself for this guard. The guard does not replace
6177
+ WIP, capability, privacy, independence, roster or runtime ownership admission.
6178
+ Planner-pull reservations still count in their own WIP metric, not as queue work.
6179
+ When both thresholds are off, routing results/traces and delivery behavior stay
6180
+ unchanged, and no queue metadata query is added to routing.
6181
+
6182
+ Only an explicit person/current single-conductor nomination can bypass queue
6183
+ pressure, and only for that nominated owner or reviewer role. An explicit reserved
6184
+ pull may bypass the owner's queue guard; an ordinary planner pull, an unreserved
6185
+ pull choosing its owner automatically, or HUB auto-pull on a planner's behalf may
6186
+ not. Merely having `namedOwner`, `namedReviewer` or a reservation is not authority.
6187
+ A self-claim delivers no owner offer and skips owner queue admission only. A
6188
+ person reservation holds its target under pressure instead of rerouting it.
6189
+ Override authority and all other admission conditions are rechecked after awaits.
6190
+ An owner nomination does not waive an automatically chosen reviewer's guard.
6191
+ Future automatic offers reevaluate current queue conditions; no lasting override
6192
+ is inferred from free-text history.
6193
+
6194
+ `Bus.assignmentQueue()` matches the number of envelopes actually waiting in
6195
+ `Bus.queued`, not the number of digest batches, active deliveries or WIP tasks.
6196
+ Age comes from host queue-arrival provenance, not an envelope sender's timestamp.
6197
+ Opt-in arrival metadata is bounded and persisted with the existing v1 bus snapshot;
6198
+ default-off snapshots keep their old shape. Legacy, missing or recovery-deferred
6199
+ arrival provenance remains unknown across resaves/restarts until those entries
6200
+ leave the queue. Advisory metadata may be omitted at the existing snapshot byte
6201
+ ceiling rather than refusing an otherwise valid delivery.
6202
+
6203
+ When tracking is enabled, the journal keeps small per-peer summaries committed
6204
+ with the bus snapshot; disabled writes do not rewrite the summary rows. Reads
6205
+ use summary/count/oldest/revision metadata only, never delivery body JSON or a sort
6206
+ of full delivery history. A revision mismatch, including an older daemon writing
6207
+ the snapshot without updating summaries, produces unknown age. Summaries are also
6208
+ checked against the bus's exact current queue count. Existing queue/delivery
6209
+ ordering, steering, withdrawals, settlement and recovery traffic are not changed.
6210
+
6211
+ Queue-origin holds are reconsidered after queue mutations commit, on relevant peer
6212
+ state/policy changes and at bootstrap. Rechecks are coalesced and bounded; they do
6213
+ not poll delivery history or reassign unrelated legacy/person-unassigned work.
6214
+ Ownerless proposals, including automatic decline/budget reroutes, are identified
6215
+ by their host-written root queue stall code. A planning revision triggers fresh
6216
+ admission rather than discarding that origin. A refusal from a separate plan or
6217
+ policy gate replaces the queue origin, so queue mutations cannot poll GitHub for
6218
+ plan evidence. Reserved retries pass through the existing WIP/in-flight-slot
6219
+ admission; a full slot hands readiness back to the existing WIP release path. A held named proposal keeps its
6220
+ intended target. Continuing reviewers stay bound unless that reviewer is actually
6221
+ queue-refused (or the existing reviewer-pool rules require selection). Automatic review holds carry a host-only `queueReviewHeld` history marker,
6222
+ bound to the current review role boundary and an opaque SHA-256 of the selected
6223
+ head/repository/PR identity; manual reviewer or artifact changes invalidate it.
6224
+ Candidate pages use board metadata, with fresh role/head/privacy/admission checks
6225
+ before effects. Shutdown/recovery cancels queued recheck callbacks and fences awaited passes.
6226
+
6227
+ Status exposes policy and bounded per-peer queued count, host oldest timestamp,
6228
+ age and fixed blocked reason separately from WIP. Unknown numeric values are null,
6229
+ and human output says unknown. With the policy on, an unknown host age does not
6230
+ fall back to a sender timestamp and appear fresh. The stall view retains the same
6231
+ queue reason codes; metrics contain ids/numbers/closed words, not envelope bodies
6232
+ or task text. The policy changes assignment admission only: it never drops,
6233
+ reorders, auto-resolves or cancels existing delivery.
6234
+
6235
+ ## Vendor model and effort controls (#266 AC1)
6236
+
6237
+ This is the capability snapshot from 2026-10-29, for Codex 0.165.0, Claude Code
6238
+ 2.1.312 and Kimi 2.3.2. It precedes task/role `exec`, execution policy and bench
6239
+ matrix implementation. The [sanitized receipt](../verification/2026-10-29-model-effort-capabilities.md)
6240
+ separates native configuration/control acceptance from provider execution.
6241
+ A successful setter or readback does not identify the model that served an answer.
6242
+
6243
+ | Vendor / setting | Launch | After launch | Verified boundary and remaining unknown |
6244
+ | --- | --- | --- | --- |
6245
+ | Codex model | `--model` advertised by help; the `model` config override has verified native readback | `thread/start` / `thread/resume` model override and `turn/start.model`; per-turn setting becomes a thread default | A bounded, network-denied turn attempt followed by interrupt and no-override resume returned the requested replacement model. Provider execution is **UNVERIFIED**. |
6246
+ | Codex effort | `model_reasoning_effort` config override; native config readback verified | Thread config override and `turn/start.effort`; sticky across later turns | Native reasoning-effort readback changed and persisted through the same interrupted attempt. Effort is a nonempty string in this version's schema; eligible values come from each model's `supportedReasoningEfforts`, not a universal four-level enum. Provider application is **UNVERIFIED**. |
6247
+ | Claude Code model | `--model`; accepted by the isolated print/control session | Session `set_model` control ACK and canonical-family readback verified; `/model` is also documented for the terminal | Session configuration, not an active inference switch. Provider execution and a real interactive/account leg are **UNVERIFIED**. |
6248
+ | Claude Code effort | `--effort`; launch-level status readback verified | `/effort` changes the session setting for a later message; print-session set/status readback verified | Native status changed from the launch choice to the new choice. Model-specific effort support and actual provider application are **UNVERIFIED**; thinking on/off or a thinking budget is not interchangeable with graded effort. |
6249
+ | Kimi model | `--model` is advertised for that invocation | Vendor documents `session/set_model` and `session/set_config_option` with `configId: model` | Native initialize succeeded, but `session/new` returned `-32000` in the isolated, unconfigured, network-denied fixture. Selection ACK/readback and provider execution are **UNVERIFIED**, not established unsupported. |
6250
+ | Kimi effort | No graded-effort flag was advertised by the observed root help; config-driven launch effort is **UNVERIFIED** | Vendor documents a `thinking` config picker; graded ACP effort is **UNVERIFIED** | Do not equate an on/off picker, permission `session/set_mode`, or an arbitrary graded string with supported effort. An open upstream report describes a graded-to-boolean collapse; revalidate the installed model/options before any mapping. |
6251
+
6252
+ The native interfaces and the hub's current wiring are different contracts. At
6253
+ `88e38f3d`, the Codex adapter forwards TUI fields but hub-created turns supply no
6254
+ model/effort override. Claude's headless writer supplies its declared model when
6255
+ launching each child, including resumes; its current transport closes stdin after
6256
+ input and exposes no later setting control. Kimi's ACP path has a launch model
6257
+ argument and permission-mode control, with no hub model/effort setter or selected
6258
+ model readback. These observations add no runtime setting or permission change.
6259
+
6260
+ The [Codex app-server contract](https://learn.chatgpt.com/docs/app-server),
6261
+ [Claude model/effort controls](https://code.claude.com/docs/en/model-config),
6262
+ [Kimi ACP coverage](https://moonshotai.github.io/kimi-code/en/reference/kimi-acp)
6263
+ and [graded ACP effort report](https://github.com/MoonshotAI/kimi-code/issues/1486)
6264
+ corroborate the surface descriptions; moving documentation does not extend this
6265
+ receipt to another native version. `turn/steer` is not the Codex setting interface.
6266
+ An unsupported or unverified mapping must remain unavailable to a future policy,
6267
+ rather than silently retaining a default or treating unknown as supported.
6268
+ The [remaining smoke legs](../smoke.md#model-and-effort-capability-verification-266-ac1)
6269
+ are separate from AC2-AC10 and any real matrix, account or provider proof.
6270
+ ## Amendment: domain packs (#513a)
6271
+
6272
+ A domain pack bundles a profile with the roles it expects, a review rubric and example checks in one file, so a project adopts a way of working with one command. This amendment records where the shipped implementation differs from the first plan for #318, and why.
6273
+
6274
+ - Format. `<name>.pack.json` holds exactly `name` (equal to the file name, never a profile's name), `profile` (one of the four shipped profiles), `summary`, `roles` (at most 8 catalog role ids), `rubric` (1 to 12 criteria, each `id`, `text` and `required`; ids are the #507 criterion ids) and `checks` (at most 8, each `id` and a display `note`). Any other field is refused. A pack is data and never authority: it adds nothing to `checks`, selects no command and grants no permission.
6275
+ - Checks carry a `note`, not a `command`. The first plan gave a check a display command string. A command-shaped field in a file that `ahub init` writes into the project invites reading it as runnable, and `example_checks` is already the display surface: an example check is advisory text that is never selected or run. The pack's checks therefore become `profile_overrides.example_checks` as `<id>: <note>`. The deterministic check scripts that run are #513b's, and they need their own reviewed way to be selected, since profile selection installs no command (#259).
6276
+ - Roles are named, not created. The pack lists catalog role ids that must already exist (shipped, or a project `.agenthub/roles/<id>.md`); a pack naming an unknown role is refused whole. `ahub init --profile <pack>` previews and writes the project's config, and does not copy role files: the project assigns roles with `catalog_roles` as before. The first plan had init install role files; that would make the pack a second writer of the role catalog, which has one loader and one validator.
6277
+ - Layers. A shipped layer (`src/hub/shipped-packs.ts`, files in `templates/packs/`, bundled as text like the shipped roles) and a project layer (`.agenthub/roles/<name>.pack.json`, in the directory the role catalog reads; the catalog takes `.md` only). A project pack of the same name replaces a shipped one. Only the format example ships in #513a; the writing, analysis and ops packs are #513b.
6278
+ - Init. `--profile` takes a profile or a pack name; a pack's base profile must equal the project's existing profile, and existing `profile_overrides` that differ from the pack's (compared as data, with keys sorted, so key order never matters) are refused rather than overwritten. The preview names a pack by ids only (rubric ids, check ids, role ids), never its text. The apply writes the rubric as `profile_overrides.review_checklist` (each line `<id>: <text>`, optional criteria marked, then the `{check}` line) and the checks as `profile_overrides.example_checks`, keeping every other key of the config; applying the same pack again changes nothing.
6279
+ - Rubric ids follow the live #507 ids (a letter, then letters, digits or `-`, at most 48 characters, upper case allowed); the lower-case pattern in the first plan was stale. The rubric only words the review checklist: a task's recorded criteria are never added or removed by a pack.
6280
+
6281
+ ## Amendment: the console class keeps trust, money, attribution and terminals (#933)
6282
+
6283
+ Plan of record: ai-workspace#60 decision 8. `classifyPeerCommand` (`src/cli/identity.ts`) is policy; every reclassified form changed its daemon-side check in the same change.
6284
+
6285
+ - **The person keeps** `permission`, `agents` beyond `--json`, `role assign`, `budget` changes, every `--as-user` form, a launcher's `personOnlyFlags`, `pet --window`, `watch`, `recovery reattach` and unknown commands, with the unchanged cause line and `OPERATIONAL_PERSON_GUIDANCE`. Also kept, by ruling: `upgrade --yes` (its detached recovery runner inherits the agent markers and the plan covers every running project; deferred to #900 with restart), the memory changes (`promote|demote|forget|supersede|rebuild|machine --label`) and the `goal` changes other than `link` (the pinned amem 0.12.0 records their actor as `user`; the upgrade path is agent-mem 0.13.0 attribution, agent-mem#143, after the pin bump in #958), and `route` beyond `explain` (no such form exists).
6286
+ - **Settings.** The conductor's keys are `CONDUCTOR_SETTING_KEYS` plus `tasks.escalate_after`. `tasks.wip_limits.*`, `tasks.auto_pull` and `tasks.queue_backpressure.*` stay person-only because they decide how much work the hub hands out unattended; the routing-store keys (`routing.classes.*.peers`, `escalate_to`, `route`, `pi_backend`, `routing.stay_switch`) stay person-only because they decide who gets work.
6287
+ - **The conductor** runs `board import`, `intake approve`, `schedule run|pause|resume` and `goal link` through their own daemon request, which calls `admitOperational` (role recheck, one `agent_cli` row) before Tasks sees it; Tasks admits the person or the admitted conductor (`TasksDeps.operational`) and records the conductor as actor. `research backfill`, `bench run` (its tasks proposed as the conductor) and `source add` run in the CLI, so the `admit` handshake is their one audit row, sent only after the CLI has checked their arguments. A native launcher with `cli.authority: "person"` starts through `hub_peer_start`, which needs `conductorStartable`.
6288
+ - **Any agent, on its own task:** `task cancel` through the unchanged `Tasks.cancel` rule of `hub_task_cancel`; `task role` when it owns the task and the task is not in review; `task label` when it owns the task (`task_label_check`), with `by` on the research record, where a person's label for the same record always wins and an agent's counts only without one; `ahub deliverables` filtered by `taskShowAllowed`, the rule `hub_task_show` uses, before any file is read.
6289
+
6290
+ ## Operational constants settings mechanism (issue #489 H1)
6291
+
6292
+ H0-classified operational numbers use fixed metadata in
6293
+ `src/hub/operational-settings.ts`, consumed by the existing closed configuration
6294
+ schema, `configApply`, shared settings rows and `DEFAULT_CONFIG`. Registration
6295
+ is static and each schema leaf is explicit; writes, reload authorization,
6296
+ inheritance and undo retain their existing paths. No new configuration file,
6297
+ environment knob or request-level override is added.
6298
+
6299
+ The first conversion is `tasks.groom_fyi_window_min`: 1-1440 minutes, default 10,
6300
+ live at the next planner-notice flush. Its unset behavior is pinned to the
6301
+ independent pre-conversion `GROOM_FYI_WINDOW_MS = 10 * 60_000`. Only state-change
6302
+ FYI coalescing changes; initial/planning/dependency notices, ownership and
6303
+ sessions do not. A new row never grants conductor permission automatically.
6304
+ Safety floors and contract constants are not converted by H1. A later exposed
6305
+ safety floor must allow tightening only in schema bounds and have its own test;
6306
+ wire/storage/protocol contracts remain constants.