@staix/agent-hub 0.31.0 → 0.32.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ Issue and pull request numbers in the entries for 0.7.7 and earlier refer to the
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.32.0
8
+
9
+ - Rooms gain advisory liveness and person-visible epoch changes (T3 of #1204, task 736). An ephemeral-capable broker enables 30-second presence refresh and unknown status after three missed periods; aging never changes sending authority. Graceful stop fences new work and attempts a signed offline snapshot within two seconds without overtaking an attempted FIFO head. Legacy brokers report liveness untracked with an upgrade step. Epoch-changing commands preview sealed outstanding delivery IDs, recheck the exact set and roster, and immediately hold old-epoch work as needs_review with the cause "epoch rotated". Bus-only work and person/terminal dispositions are preserved; T4 remains open.
10
+ - Classify 149 more baseline keys of the hard-coded values inventory, the `src/hub` length bounds (#741 batch 1, refs #1180): 25 operational, 77 safety floor, 47 contract. A fresh blind sample first scored 70%, six rulings (docs/verification/2026-11-05-hardcoded-values-classification-rules.md, lines 8 to 13) and 17 relabels followed, and a second sample scored 85%. The baseline shrinks from 1155 to 1006 keys; `--check` passes.
11
+
12
+ - 0.32.0 N-1: published 0.31.0, the latest 0.31.x patch, with agent-mem 0.21.0 and protocol 17. The release N-1 fixture and matrix now pin it; the separate protocol-16 fixture stays 0.25.0.
13
+ - Wire/protocol: control remains 16 through 17 and room presence stays v1. A 0.31.0 hub already opts into advertised ephemeral support and understands requestReply and signed offline. With a supporting broker, 0.32.0 requests a signed observation every 30 seconds and 0.31.0 can answer it. A delivered 0.32.0 graceful-stop snapshot is understood by 0.31.0. A 0.31.0 stop has no final offline snapshot, so 0.32.0 relies on its three-period advisory aging; 0.31.0 adds no aging of its own and retains its state-only view. A broker without ephemeral advertisement disables periodic refresh and aging on 0.32.0 and is shown as liveness untracked, with an upgrade step.
14
+ - Local semantics: only 0.32.0 adds advisory unknown display, pending-ID previews and immediate epoch-rotated holds. Aging never changes native PeerState, outbound admissibility or authority. Existing signed roster, exposure, link, Bus proofs, person dispositions and codec checks remain. An epoch hold is local metadata, not a new remote control, cancellation, completion or receipt reason; a 0.31.0 member retains its own epoch-settlement behavior.
15
+ - Files and floors: hub.db remains schema version 2, RoomStore remains schema 5 and the exact agent-mem pin stays 0.21.0. Epoch adoption uses existing needs_review/boundary_refused storage states and does not raise a format floor. The new hold label is a local status projection, not a new room receipt reason.
16
+ - CLI/status: a 0.31.0 client talking to a 0.32.0 daemon drops additive presence/liveness/holdReason fields, including in JSON, and does not display outstanding preview IDs. A 0.32.0 client accepts their absence from a 0.31.0 daemon and keeps its state-only view and existing fingerprint confirmation. New liveness and pending-ID displays require the matching 0.32.0 CLI; existing server ticket checks remain in effect.
17
+ - MCP tools: schemas and capability checks are unchanged from 0.31.0; no new peer tool or native authority follows from T3 liveness or epoch metadata. Full room codec-feature negotiation remains T4.
18
+ - Evidence ceiling: room directions above follow source comparison and isolated current-version broker fixtures. They do not claim a mixed published-version room execution, guaranteed offline-frame delivery under backpressure, live TLS/proxy behavior or two-machine acceptance. Published-package common-control interoperability is recorded separately by the retargeted fixture gate.
19
+
7
20
  ## 0.31.0
8
21
 
9
22
  - Delivery failures carry one bounded diagnostic cause: peer offline, adapter refused, push error, turn aborted or timeout (#1178, PR #1224, task 714). `ahub queue show` and JSON listings expose it without message text; legacy rows say when no cause was recorded. Status shows each peer's unresolved failed count and the discard next step, excluding explicitly resolved rows; inspect the causes with `ahub queue list` or `show`. Causes do not authorize replay or alter settlement.
package/README.md CHANGED
@@ -5,7 +5,7 @@ Code, Codex, Kimi Code, Pi and a hub-owned local worker (plus Qwen Code, Jcode a
5
5
  Antigravity if you want them) share a message bus, a task board and a memory, each
6
6
  connected through its own native interface.
7
7
 
8
- Status: 0.31.0, control protocol 17 (accepts 16). Bun + TypeScript, one loopback daemon per
8
+ Status: 0.32.0, control protocol 17 (accepts 16). Bun + TypeScript, one loopback daemon per
9
9
  project. Site: [agent-hub.staix.net](https://agent-hub.staix.net).
10
10
 
11
11
  ## Quick start
@@ -143,7 +143,7 @@ uses the home must run a version that understands the new layout.
143
143
  [#258](https://github.com/STAIxBWLB/agent-hub/issues/258)).
144
144
 
145
145
  Live checks that still need a real account or a person are listed in the
146
- [release verification report](docs/verification/2026-11-05-0.31.0.md) and the
146
+ [release verification report](docs/verification/2026-11-06-0.32.0.md) and the
147
147
  [smoke checklist](docs/smoke.md).
148
148
 
149
149
  ## Documentation
@@ -48,5 +48,7 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
48
48
  - `resolveDelivery` (#713): a `queued` delivery was never handed to the peer, and a `failed` one was refused before it ran (#1103), so a busy peer blocks discarding neither; `dispatching` stays refused as `active delivery`, and so does `accepted` except for its own explicit channel's completed/discard (#1172, below); unrelated resolutions keep the busy fence. Only the recipient may explicitly complete/discard its own `needs_review` row, or for a peer with `acceptedSettlement: "explicit"` its own `accepted` row, through `resolveOwnDelivery` with an observed revision and reason (#1079), recording the peer actor through the existing journal audit. Another peer, remote-room disposition and active dispatching/accepted row refuse; this never replays the originals. Every other operator resolution stays fenced while busy. `resolveOwnDelivery` refuses with its own `PreEffectToolRefusal`, which names the cause and the next step, never through `resolutionRefusal`. A row that carries an invocation stays person-only (#1112). The daemon names the refusal from a closed list (`resolutionRefusal`), never from free error text. `queue discard --peer` goes through `resolveDelivery` row by row, reading each row's current revision just before its discard, never the revision listed at the start. Without `--state` it takes the peer's `queued` rows and its unsettled `failed` rows (wire `states`, sent only to a hub whose `features` list `queue_discard_states`; `needs_review` still needs an explicit `--state`). A row a resolution already settled (`settledIds()`) is history: `queue list` hides it unless `all`, and resolving it again is refused as `already resolved by <action>`, never `stale revision` (#1098, #1103).
49
49
  - Accepted channel deliveries (#1172): an `accepted` receipt means the channel transport took the notification, never that the session handled it. `expireAcceptedDeliveries`, run from the daemon's 5 s invocation sweep with the host-only `DaemonOptions.acceptedSettlementNow` clock, moves an explicit channel's `accepted` row older than `bus.accepted_settle_ms` (`acceptedSettleMs`, default 10 min) to `needs_review` with `ACCEPTED_SETTLE_TIMEOUT_REASON` under a revision fence. It holds the peer, routes an invocation through `onInvocationUncertain` and never replays. A repeated `accepted` receipt never renews the deadline. `settleSupersededDeliveries` and supervision skip timeout rows; an important, task or review timeout gets one ids-only conductor status notice unless `conductor.feed` is `off`. The explicit channel's `inbox` reply, a refused one included, carries `held` metadata for its own accepted and `needs_review` rows (`heldDeliveryHints(peer, true)`), or `{ unavailable: true }`, never an empty claim.
50
50
  - Review provenance (#1056): `reviewResponse` runs on every publish (`publishResult` and the daemon's channel `send`). It strips a caller's `refs.reviewPackage`/`refs.reviewResponse` and stamps a host-derived `reviewResponse` only on a chat or status that answers a review-scoped parent (`lastDelivery`, `lastPulled`, the bounded `reviewParents`, or `reviewObserverScope`, whose failure counts as scoped); never trust a peer's marker or match content. `beforeReviewInputHandoff` runs before every native handoff (steer, drain, pull); a throw leaves the work queued as `ReviewInputHandoffError` and is never a storage error. `publishReviewEnvelope` republishes a saved package only when `reviewEnvelopeStatus` is `absent`, refuses `failed`, `discarded` and `unknown`, and never replays a published one. Terminal pruning keeps hub review-package rows (`refs.reviewPackage` `review-*`). The bus constructor restores from `transportSnapshot()`; only recovery uses the strict `snapshot()`, which refuses malformed review provenance.
51
- - Room delivery rows (#1204): a remote (`remoteId`) `needs_review` row holds its peer only when it carries an invocation; an ordinary room chat uncertainty never blocks later queued work. Settle, requeue, hold or resume a remote row only through the room adapter's host ports (`settleRemoteReceipt`, `requeueRemoteNotSent`, `holdRemoteMissingIntent`, `holdInvalidatedRemoteOutcome`, `resumeRemotePending`). Each is bound to the current adapter, room and member, revision-fenced and refused after a person disposition. `deliveryReceipt` never calls `completeDelivered` or `deliveryFailed` for a remote peer, and an uncertain invocation stays person-only. At journal open, `recoverInterrupted` keeps an active single-envelope, non-invocation remote row active only while the bus snapshot still holds its room, member and an unexpired queue deadline; every other `dispatching`/`accepted` row becomes `needs_review`.
51
+ - Room delivery rows (#1204): a remote (`remoteId`) `needs_review` row holds its peer only when it carries an invocation; an ordinary room chat uncertainty never blocks later queued work. Settle, requeue, hold or resume a remote row only through the room adapter's host ports (`settleRemoteReceipt`, `requeueRemoteNotSent`, `holdRemoteMissingIntent`, `holdInvalidatedRemoteOutcome`, `holdRemoteEpochRotated`, `resumeRemotePending`). Each is bound to the current adapter, room and member, revision-fenced and refused after a person disposition. `deliveryReceipt` never calls `completeDelivered` or `deliveryFailed` for a remote peer, and an uncertain invocation stays person-only. At journal open, `recoverInterrupted` keeps an active single-envelope, non-invocation remote row active only while the bus snapshot still holds its room, member and an unexpired queue deadline; every other `dispatching`/`accepted` row becomes `needs_review`.
52
52
  - Closed publication (#1222): once the `Bus` is closed, `publish` returns `{ targets: [] }` before touching the journal or observers, so a late room or presence publication after shutdown writes nothing; a live storage error still propagates. Keep the closed check first when you touch the publish path.
53
+
54
+ - Room liveness (#1204 T3): keep expired or unavailable observation time as advisory `presence: "unknown"` only; never use aging to change native `PeerState`, outbound admissibility or authority. Keep status reads free of clock and delivery mutations. An epoch change uses `holdRemoteEpochRotated` only for the captured sealed intents under current room/member proof; preserve person dispositions and never interpret the hold as completion, cancellation or replay. Read `docs/design/bus.md` "Room liveness and epoch changes" before changing these paths.
@@ -141,6 +141,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
141
141
 
142
142
  - Channel tool-result hints (#493, #221): `withPendingHint` (`src/adapters/claude-channel.ts`) appends fresh `delivery_holds` counts, and drops the "call hub_inbox" advice whenever the same reply carries a held-delivery hint, because `bus.pull` refuses under that hold and the advice would send the session into a refused read. A `hub_inbox` result that already says how many messages remain unread adds only the important count, never a second queued count. A `hub_inbox` result that renders its own held or accepted metadata (`HELD_HINT_INCLUDED`) gets no second held hint and no polling advice, and unavailable metadata adds the inspect step instead of "Nothing is waiting" (#1172). Do not add a Claude client-version gate on steer without a probe of the older client and an optional hello field that carries the version (background: README).
143
143
  - Memory sync (#438): stop an in-flight `syncMemory` only through `syncStop` (`src/hub/daemon.ts`), and await `syncRun` before the memory store closes, as `stopOnce` does. agent-mem runs the sync's git in its own process group, so a process that ends without `daemon.stop()` leaves that git and its transport child holding the memory repository. A signal the hub exits on goes through lifecycle stop (`STOP_SIGNALS` in `src/hub/lifecycle.ts`: SIGINT, SIGTERM, SIGHUP, SIGQUIT), never the default action; SIGQUIT in particular must not reach its default core dump (Bun honours it: agent-mem #435 measured a 5.9 GB core file and a delayed exit). The forced exit after the shutdown deadline stays `process.exit`. A sync result is logged as a failure only when `quietSyncError` is false: a missing remote in either agent-mem wording (`no sync remote`, `no generation sync remote configured`) and a stop are not failures (#401); a new agent-mem wording for them extends that one predicate. Every pass gets the host screen and policy version (`currentMemorySyncPolicy`, `memorySyncPolicyVersion`, #1173); a version change during the pass records `binding`, never success. Record outcomes only through `reduceMemorySync` and publish them only as `memorySyncView` (`sync` in memory status: closed cause, counts, next step). The log line names the closed cause (`classifyMemorySyncFailure`), never the raw error text. `DaemonOptions.memorySyncTrigger` is a host-only fixture seam.
144
+ - Room shutdown (#1204 T3): `RoomCommands.stop()` fences new room work synchronously and returns a promise. Await it in `stopOnce` before closing shared storage: it waits for pending room work and each link's bounded offline-presence attempt before closing RoomStore. Preserve an attempted FIFO head and consume shutdown storage controls without admitting new presence or application work.
144
145
  - Schedules timer (#512a): `schedulesTimer` in `startDaemon` is its own interval, apart from the task sweep's, armed whenever `config.schedules` or `config.folder_triggers` is non-empty and re-armed in `reloadConfig` on a change to either; never gate it on `task_sweep.enabled`. `schedules` is machine-local (`MACHINE_LOCAL`) and never editable from settings (`NEVER_EDITABLE`): a committed file or a settings write must not be able to make the hub create tasks. The daemon status carries `schedules` (`scheduleViews`) only when some are configured.
145
146
  - Anything the daemon starts on a timer and registers only with `startupCleanup` is not stopped by a normal `stop()`: `startupCleanup` runs on a failed startup only. Clear the timer and close the store in `stopOnce`, and wait for a pass in flight before the board closes (`schedulesPass`, #512a). A console op that runs the same writer on demand (`schedule_run`, #335) joins that wait through `manualSchedulePasses`; add any new on-demand entry to it, or `stopOnce` closes the board and the store under it.
146
147
  - Corpus (#510a, `src/hub/corpus.ts`, `corpus-config.ts`): `corpus.paths` is machine-local and `corpus` is never editable from settings. Keep `corpus-config.ts` free of file-system imports, because the config schema (`config-apply.ts`) reaches the Claude channel bundle. A refusal of a root is a reason string, never a thrown error and never a partial read: evidence the check cannot read refuses, and only a completed read certifies a folder. The vault check is `.obsidian` at the real root, in every ancestor and anywhere below (any entry of that name), always on the `realPath`. A folder the pii, deny or local.deny rules protect is never listed, so it cannot be shown vault-free: the root that holds it is refused, never certified with that subtree skipped (a protected file is only skipped).
@@ -12,7 +12,7 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
12
12
  - A test timeout that fires while `Bun.spawnSync` runs can start the next test inside spawnSync's own event loop (Bun 1.3.14 and 1.4.2; background: README). `scripts/check.sh` runs tests with a 20 s timeout and under `scripts/hang-watch.sh`; a test that spawns and reads the process table gets a timeout of its own, never above 20 s: an explicit larger timeout overrides the gate's `--timeout 20000`, so shorten the scenario instead.
13
13
  - Sandboxed commands and the MLX server get a fixed environment (`PATH`, `HOME` and their own keys), so test knobs for their fakes travel in a wrapper script or argv, not in `process.env`. Daemon-owned peer children use `peerChildEnv`, which drops `ORCA_*` and replaces agent identity markers; a person-owned native launch preserves only its own pane's `ORCA_*` context while dropping inherited agent identity.
14
14
  - Bind agent identity markers explicitly in native/CLI fixture children. The full gate starts the test runner without inherited agent markers (including `AGENTHUB_DELIVERY_ONLY`) and without an `ahub claude` session's hub variables (`AGENTHUB_PROJECT_DIR`, `AGENTHUB_STATE_DIR`, `AGENTHUB_INSTANCE_ID`, `AGENTHUB_LAUNCH_ID`); a new variable a native session exports and a fixture child reads joins that `env -u` list in `scripts/check.sh`. Never weaken production marker detection to make them pass.
15
- - A fixture that spawns the real channel plugin (`src/adapters/claude-channel.ts` or `plugins/agent-hub/server.js`) to act as a peer runs it with `cwd` at the hub's project root, made with `realPath` (the macOS temp dir is a symlink). The plugin chooses its hub through `channelProject` (rule: `daemon.md`), so from another cwd it joins the wrong hub, joins as a distinct session or connects nowhere. Only tools mode with both `AGENTHUB_STATE_DIR` and `AGENTHUB_PROJECT_DIR` set skips that check. A fixture whose plugin must push as a channel (not fall back to tools mode) injects the parent row with `syntheticClaudeParentEvidenceSetup()` (`test/fakes/claude-channel-parent.ts`, #1171), which replaces only `claudeParentCommandPort.read`; the production marker and classifier still run, so never stub `classifyClaudeParentCommand` or skip the evidence check. A second opted-in session attached while a fixture owner holds the peer stays a standby (#1174): close the owner and wait for offline before attaching the session under test (`test/channel-inbox-daemon.test.ts`).
15
+ - A fixture that spawns the real channel plugin (`src/adapters/claude-channel.ts` or `plugins/agent-hub/server.js`) to act as a peer runs it with `cwd` at the hub's project root, made with `realPath` (the macOS temp dir is a symlink). The plugin chooses its hub through `channelProject` (rule: `daemon.md`), so from another cwd it joins the wrong hub, joins as a distinct session or connects nowhere. Only tools mode with both `AGENTHUB_STATE_DIR` and `AGENTHUB_PROJECT_DIR` set skips that check. A fixture whose plugin must push as a channel (not fall back to tools mode) injects the parent row with `syntheticClaudeParentEvidenceSetup()` (`test/fakes/claude-channel-parent.ts`, #1171), which replaces only `claudeParentCommandPort.read`; the production marker and classifier still run, so never stub `classifyClaudeParentCommand` or skip the evidence check. For an unchanged published bundle with an inlined observer port, use the published-leg raw port-fact injection in `test/published-protocol-compat.test.ts`: bootstrap its import with Bun's built-in `mock.module` only for `child_process` and `node:child_process`, export the real functions and replace only `spawnSync` for `/bin/ps -ww -p <direct-parent-pid> -o command=`. Return `SYNTHETIC_CLAUDE_PARENT_COMMAND` from the same fixture helper and delegate every other call. Preserve the bundle bytes, classifier, inherited marker, parent-identity check, assertions and timeouts. This synthetic host observation is not native-launch acceptance. A second opted-in session attached while a fixture owner holds the peer stays a standby (#1174): close the owner and wait for offline before attaching the session under test (`test/channel-inbox-daemon.test.ts`).
16
16
  - Orca environment (#613, #1048): the `test/hub-home.ts` preload also deletes every inherited `ORCA_*` variable, and `scripts/check.sh` unsets them before `bun test`. A run from an Orca pane otherwise reaches the person's Orca through `ORCA_WORKTREE_ID`, `ORCA_TERMINAL_HANDLE` and `ORCA_CLI_COMMAND` (the launcher's four names are `ORCA_LAUNCHER_ENV` in `src/hub/orca-env.ts`, the one list; `personEnv` drops the whole prefix) and opens real tabs and agents there. A test that needs one sets it itself after the preload and restores it; a test of the launcher uses a stub `ORCA_CLI_COMMAND` and never the real one. `test/orca-env-guard.test.ts` runs a real `ahub up` with fake values and a recording stub and must see zero calls.
17
17
  - `bunfig.toml` preloads `test/hub-home.ts`, which first clears inherited `AMEM_HOME`, gives every test run (the gate and `bun test <file>` alike) a temp `AGENTHUB_HOME` unless one is set, and always replaces `KIMI_CODE_HOME` with a temp dir, an inherited value included (#285); scoped tests may set their own `AMEM_HOME` or `KIMI_CODE_HOME` after preload. Hub memory (#258) is on with any project config, and no test may write the person's `~/.agenthub`. Do not remove it; a test that needs its own home sets and restores the variable. The preload keeps an inherited `AGENTHUB_HOME`, which an agent shell exports as the person's hub home and `scripts/check.sh` does not unset: run `bun test` and the gate with `env -u AGENTHUB_HOME`.
18
18
  - `test/registry-guard.test.ts` statically scans `src/` against the committed `test/registry-guard.baseline.json` (#451); a test never writes, regenerates or repairs that baseline, and a failing guard is fixed in the source by moving the policy into a definition (rule: `docs/agent-notes/adapters.md`); the baseline stays `[]`. Scanner behavior is pinned on inline sources in `test/registry-guard-fixtures.test.ts`, never on edits to real files. A new table, hint or flag list in generic code that names a peer (`"pi"`, `isJcodePeer`) is a new group: derive it from the definitions instead (`ADAPTERS.filter(...)`, a port such as `statusNotice`). Missed twice on 2026-11-02 (PR #1093 `jcodeTuiHint`, PR #1100 `INIT_SETTINGS`), both found only by CI's shard 3; run `scripts/check.sh --changed` or `bun test --timeout 20000 test/registry-guard.test.ts` before pushing a new peer-shaped table (a bare `bun test` times out at 5 s on this file).
@@ -58,4 +58,4 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
58
58
  - A test of `scripts/release-prepare.ts` or `scripts/release-tag.ts` runs the script with its root in a scratch repository whose only remote is a local bare directory: set `AHUB_RELEASE_ROOT` to it, or copy the script into it as `test/release-tag-daemon.test.ts` does. Without that the script takes its root from its own location and fetches and runs `ls-remote` against this checkout's real origin. Consent comes from a fake port, a stand-in `AHUB_CLI` or a scratch daemon, never the person's hub.
59
59
  - Production `MemoryClient` (`src/memory/client.ts`) is the in-process amem store and never opens `memory.worker_url` (#752). A test that needs the legacy worker shape injects `FakeMemoryClient` (`test/fakes/mem-worker.ts`) as `DaemonOptions.memoryClient` or in place of the client; never add an HTTP path to the production client for a test. An amem-backed test sets its own temp `AMEM_HOME` and reads only its own fixture memory directory.
60
60
  - CLI print guards (#814, `test/cli-json-guard.test.ts`): two scans over every file in `src/cli`. One finds `console.log(JSON.stringify(...))` and passes only behind a `--json` branch (`selectsJson`) or a named `DATA_FORMAT` entry (`NOT_YET_CONVERTED` is empty since #899; keep it empty). The other finds a print of a hub tool's text (`console.log(await taskOp(<op>))`, or a variable bound from one) and passes only behind `--json`, for a named `SENTENCE_OPS` op (checked against the daemon's case), or named debt (`TEXT_DEBT`, `CONDUCTOR_DEBT`, `REFUSAL_FALLBACK`). A list entry with no matching site fails, so the lists only shrink. Add a command's `--json` branch or renderer; never add a list entry to make a new print pass. Per-command output tests (`test/task-output.test.ts`, `test/sdlc-output.test.ts`) run the real CLI against a fake control server and assert default not JSON, `--json` byte equality, 80 and 200 columns and `--color=never`.
61
- - Hard-coded values inventory (#719, #1180, `scripts/inventory-hardcoded-values.ts --check`, a once-only step of `scripts/check.sh`): a constant or inline literal you add to `src/` or `scripts/` is an inventory entry, and the gate fails until it is classified in `scripts/inventory-hardcoded-values.classifications.json` (`operational`, `safety floor`, `contract` or `test seam`, by the reasons of `docs/verification/2026-10-30-operational-settings-closure.md`). Never add a key to `scripts/inventory-hardcoded-values.baseline.json`: it is the list of keys that were unclassified when the check began and it only shrinks. Classify a baseline key by adding it to the classifications file and deleting it from the baseline in the same commit; the check refuses either half alone. The classification rules and their calibration are in `docs/verification/2026-11-05-hardcoded-values-classification-rules.md` (#738): a baseline key is labelled by a mechanical rule the committed file agrees with on at least 94% of its decided keys, never by reading the code against a guess, and a shape the file labels inconsistently (length bounds and truncations in `src/hub`, timeout clamps, default-parameter timeouts) stays in the baseline until #739 settles its convention. Do not classify keys in bulk from reading the code: a blind pass agreed with the committed file on 58% of a sample, because the file counts hub-internal caps, injected `opts.x ?? N` defaults and script deadlines as operational (`docs/verification/2026-11-05-hardcoded-values-ratchet.md`). A line moved inside a file changes no key; a changed value or name does.
61
+ - Hard-coded values inventory (#719, #1180, `scripts/inventory-hardcoded-values.ts --check`, a once-only step of `scripts/check.sh`): a constant or inline literal you add to `src/` or `scripts/` is an inventory entry, and the gate fails until it is classified in `scripts/inventory-hardcoded-values.classifications.json` (`operational`, `safety floor`, `contract` or `test seam`, by the reasons of `docs/verification/2026-10-30-operational-settings-closure.md`). Never add a key to `scripts/inventory-hardcoded-values.baseline.json`: it is the list of keys that were unclassified when the check began and it only shrinks. Classify a baseline key by adding it to the classifications file and deleting it from the baseline in the same commit; the check refuses either half alone. For `src/hub` bounds and truncations, apply the committed conventions and rulings in `docs/verification/2026-11-05-hardcoded-values-classification-rules.md` (#739/#741). Before each batch, run a fresh blind sample of 80 already-decided entries and proceed only at 85% agreement or more. Outside that settled slice, a baseline key needs a calibrated mechanical rule that the committed file agrees with on at least 94% of its decided keys; leave an undecided shape in the baseline pending a ruling, never guess its label. Do not classify keys in bulk from reading the code: a blind pass agreed with the committed file on 58% of a sample, because the file counts hub-internal caps, injected `opts.x ?? N` defaults and script deadlines as operational (`docs/verification/2026-11-05-hardcoded-values-ratchet.md`). A line moved inside a file changes no key; a changed value or name does.
@@ -140,8 +140,47 @@ packet's deadline. Unsupported recipients never receive the new frame shape.
140
140
  A refresh request
141
141
  remains on non-reply snapshots until all asked members supply validated presence;
142
142
  replies never request a reply. The bounded round resets on reconnect or roster
143
- change. The latest-per-sender crossing ceiling and its #736 upgrade path are
144
- recorded in the rooms spec.
143
+ change. Periodic idle refresh in T3 repairs a crossed request even without a peer-state
144
+ change; latest-per-sender coalescing and the existing dedupe ceiling remain.
145
+
146
+ ## Room liveness and epoch changes (#1204 T3)
147
+
148
+ Only a connection whose broker negotiated ephemeral presence publishes periodic
149
+ `requestReply` snapshots, every 30 seconds by default. Replies never request
150
+ replies. Three missed local refresh periods (90 seconds) make that member's room
151
+ presence `unknown` in its exposed-peer status. Aging is advisory only: it changes
152
+ no outbound admissibility, native peer state or authority. Sending remains governed
153
+ by the signed roster, exposure, link and Bus proofs; the broker can queue for a
154
+ member that is not live. An explicit signed offline presence keeps its existing
155
+ handling. Freshness uses local observation time, never a saved snapshot or a sender
156
+ clock; restart needs a fresh observation for its displayed liveness. Invalid time
157
+ or clock rollback makes that advisory view unknown without changing transport gates.
158
+ Status computes the advisory view without mutating the clock or delivery state.
159
+ The refresh period and clock are injectable for fixtures.
160
+
161
+ A legacy broker gets no periodic refresh or aging. Identical-snapshot dedupe and
162
+ state-change publication remain, with at most one final offline observation at
163
+ stop. Its view says `liveness not tracked (broker without ephemeral)` and asks
164
+ the person to upgrade the broker. Optional room-only `presence` and `liveness`
165
+ metadata keep the closed native `PeerState` type unchanged; older readers ignore
166
+ the added fields. No new codec or full capability handshake is introduced; T4
167
+ remains separate.
168
+
169
+ Stop is asynchronous and idempotent, with a synchronous publication fence. It
170
+ attempts one signed final offline observation behind an already attempted FIFO
171
+ head, then disconnects within two seconds. Durable application intents and
172
+ receipts survive. A blocked or failed final observation falls back to aging at
173
+ supporting brokers; it does not stop a remote process or grant native authority.
174
+
175
+ Before an actual epoch change, the person preview lists only already sealed,
176
+ unresolved RoomStore delivery ids. The commit quiesces link application
177
+ publication and rechecks that exact id set and the current roster digest.
178
+ RoomStore atomically quarantines old active intents as `needs_review` with the
179
+ existing `boundary_refused` reason and drops their sendable ciphertext. The local
180
+ Bus holds use `room epoch rotated; inspect ahub queue show <id>`; current room status exposes `holdReason: "epoch rotated"`.
181
+ Bus-only unsealed work stays queued. This is no completion, replay or cancellation;
182
+ terminal rows and person dispositions remain unchanged. T2 already supplied the
183
+ crash-gap quarantine; T3 adds the person preview and named local hold.
145
184
 
146
185
  ## Room application retry and settlement (#1204 T2)
147
186
 
package/docs/dogfood.md CHANGED
@@ -24,6 +24,7 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
24
24
  | Room membership and presence (#446 X3) | off; no person room enrollment or exposure configured here | isolated fake-hub/broker tests for OOB enrollment, MITM refusal, durable sequence/replay/grace, explicit fingerprint confirmation/abort, refused-frame retirement, exposure revocation and ids-only presence; room list/status for ids and link state; #1204 T1 fixtures cover unchanged-presence dedupe, 1,000 flips with an offline member, latest-only ephemeral broker retention, old-broker wire fallback and same-frame queue_full backoff; CI task 742 adds counter-bounded deferred-presence reuse, concurrent chat ciphertext/Store FIFO and post-close publication regressions (test/rooms-presence-coalescing-742.test.ts, test/bus-closed-publication-742.test.ts); Linux diagnostic counts and targeted hosted macOS acceptance (unchanged AC1 787.72 ms; controlled idle-entry 783.51 ms) are in docs/verification/2026-11-06-rooms-presence-cost.md; no live rooms receipt; X4 chat and X7 two-machine live receipt remain separate and unverified |
25
25
  | Presence publication cost (#1204, #740) | source and isolated broker coverage; installed rooms remain off here | unchanged 1,000-flip AC1 test under its 20 s guard; transient pause/resume journal-write count, explicit manual-pause persistence, requestReply and exposure fences; hosted macOS is the acceptance gate |
26
26
  | Room automatic resend and late settlement (#1204 T2) | off here; source and isolated in-process broker coverage after validation; release and person enrollment/exposure required; live two-machine behavior remains unverified | AC2-AC4: receiver offline for 15 minutes then each application admitted once; pre-enqueue refusal retries without uncertainty; ACK deadline resends within the original TTL; signed late receipt settles once; local receipt commit survives restart before broker storage; no person-disposition override or native/task-read evidence |
27
+ | Room liveness and epoch-change preview (#1204 T3, #736) | source and isolated fixtures, validation pending; rooms remain off here and installed/two-machine behavior is unverified | `rooms-liveness-link-736.test.ts`: signed offline within five seconds, idle crossed-request convergence and no reply loop, synchronous stop fence/FIFO preservation; `rooms-presence-aging-736.test.ts`: three local periods to advisory unknown without changing outbound admissibility, legacy untracked behavior, restart and clock rollback; `rooms-rotation-736.test.ts`: sorted ids-only preview, changed id-set/digest refusal, sealed hold versus Bus-only queue, unchanged person/terminal disposition; `rooms-liveness-status-736.test.ts`: native state preserved, broker upgrade guidance and named epoch-rotated cause; defaults 30 s refresh, 90 s aging, 2 s stop bound; exact-head gate and live proof remain separate |
27
28
  | Worktree shelf (#1057): `ahub worktrees`, `tidy`, `tidy --confirm <fingerprint>`, the Agents panel section, Park/Pin, the explicit PR refresh and the inbox alert | fixture-only until a person uses it, and only after release and upgrade: this repository keeps every task in `.worktrees/<name>`, so it is the first real use, but nothing here has run `tidy --confirm` yet, and `worktrees.quiet_days` stays 7 until the owner has used it for a week | `ahub worktrees` live and settled counts over a week, and how many settled rows a person then Pins (a Pin means a rule was wrong); `tidy` preview refusals by reason (`task not approved or cancelled`, `unpushed commits`, `branch changed since preview`); after a confirm, `git worktree list` against the preview; test/worktree-shelf.test.ts, test/worktree-tidy.test.ts and test/worktree-panel.test.ts (a PII row stays live and redacted, a branch switch refuses, no GitHub call on a read, the daemon removes nothing) |
28
29
  | Message bus, digests, priorities | on | `ahub report`: messages, digests, overflow, undeliverable; delivery latency from events |
29
30
  | Control-send recipient normalization (#963) | active after release and hub upgrade; this PR does not change the running hub | string recipients reach only the named peer, invalid shapes publish nothing, omitted/empty arrays preserve broadcast; real native recipient behavior remains unverified |
@@ -3569,7 +3569,7 @@ or the plugin is older than the daemon it installed (#1179). It prints nothing w
3569
3569
  | Runtime | Control protocol window | Legacy status field |
3570
3570
  | --- | --- | --- |
3571
3571
  | 0.12.20 through 0.25.0 | 16 only | `protocol: 16` |
3572
- | 0.26.0 through 0.31.0 | 16 through 17 | `protocol: 16`, `protocolMin: 16`, `protocolMax: 17` |
3572
+ | 0.26.0 through 0.32.0 | 16 through 17 | `protocol: 16`, `protocolMin: 16`, `protocolMax: 17` |
3573
3573
 
3574
3574
  New clients choose the highest common protocol. The protocol window remains 16
3575
3575
  through 17, so the published protocol-16 client and channel plugin remain usable; unrelated feature
@@ -3577,11 +3577,11 @@ capabilities still require their own labels. A disjoint window refuses with 4426
3577
3577
  The compatibility-floor `protocol` field remains readable by old clients.
3578
3578
 
3579
3579
  Release N-1 is counted in minor versions, separately from this protocol window
3580
- (#1164, family contract entelecheia/ai-workspace#70). For 0.31.x, N-1 is 0.30.0,
3581
- the latest published patch of 0.30.x (exact `@staix/agent-mem` 0.20.1, control
3582
- protocol 17 with descriptor floor 16). Every patch on 0.31.x keeps that release
3583
- baseline. A change that cannot retain it needs a new minor release. For 0.30.x
3584
- the baseline was 0.29.0.
3580
+ (#1164, family contract entelecheia/ai-workspace#70). For 0.32.x, N-1 is 0.31.0,
3581
+ the latest published patch of 0.31.x (exact `@staix/agent-mem` 0.21.0, control
3582
+ protocol 17 with descriptor floor 16). Every patch on 0.32.x keeps that release
3583
+ baseline. A change that cannot retain it needs a new minor release. The 0.31.x
3584
+ baseline remains 0.30.0 (agent-mem 0.20.1); for 0.30.x it was 0.29.0.
3585
3585
 
3586
3586
  In the minor release PR, together with its `package.json` version bump, query
3587
3587
  the published versions of `@staix/agent-hub`,
@@ -5976,6 +5976,54 @@ chat stays usable; upgrade the broker to avoid offline presence retention.
5976
5976
  The supporting broker withholds ephemeral frames from older member hubs; they
5977
5977
  must upgrade to see upgraded hubs' presence. Chat wire shapes remain unchanged. See [wire compatibility](design/bus.md#room-wire-compatibility-1204-t1).
5978
5978
 
5979
+ ### Room liveness and graceful stop (#1204 T3)
5980
+
5981
+ On an ephemeral-capable broker, idle hubs request fresh signed presence every
5982
+ 30 seconds. After three missed periods (90 seconds), room presence becomes
5983
+ `unknown` in exposed-peer status. This expiry is advisory only and never blocks
5984
+ outbound transport: current signed roster, exposure, link and Bus proofs still
5985
+ control sending, including broker queueing for members that are not live. An
5986
+ explicit signed offline presence keeps its existing handling. Saved presence after
5987
+ restart is not a fresh liveness observation.
5988
+ The optional room advisory presence is separate from native peer state; neither
5989
+ an offline observation nor expiry stops the other person's agent process.
5990
+
5991
+ With a legacy broker, status says `liveness not tracked (broker without ephemeral)`.
5992
+ Upgrade the broker to enable periodic refresh and aging. Legacy connections keep
5993
+ identical-snapshot dedupe and ordinary state-change publication, with no
5994
+ periodic refresh or freshness expiry. Older clients may ignore the new advisory
5995
+ status fields. Full capability advertisement remains #1204 T4.
5996
+
5997
+ A graceful room stop immediately fences new publications and attempts one signed
5998
+ offline snapshot behind an already attempted FIFO frame, bounded to two seconds
5999
+ before disconnect. Durable sends and receipts remain for recovery. If that
6000
+ attempt fails or the broker is blocked, supporting recipients expire their last
6001
+ observation through aging. A crash has the same aging fallback. A legacy broker
6002
+ has no such expiry guarantee.
6003
+
6004
+ ### Inspect outstanding work before rotating an epoch (#1204 T3)
6005
+
6006
+ Before an epoch-changing owner `expose`, `invite --accept`, `revoke` or an
6007
+ existing-room `join --roster`, the person preview prints sorted outstanding
6008
+ sealed delivery ids, without message text. It excludes work still queued only
6009
+ in the local Bus. The operation rechecks the exact id set and roster digest
6010
+ with link application publication quiesced; if either changed, obtain a fresh
6011
+ preview rather than reuse the old confirmation.
6012
+
6013
+ The epoch commit quarantines old active sealed intents and removes their
6014
+ sendable ciphertext. Their local delivery becomes `needs_review` with reason
6015
+ `room epoch rotated; inspect ahub queue show <id>`; `ahub room status team` labels
6016
+ its `holdReason` as `epoch rotated`. Inspect each id with
6017
+ `ahub queue show <delivery-id>` and choose a person disposition. Rotation does
6018
+ not assert completion, cancel a native turn or replay old ciphertext. Existing
6019
+ terminal results and person dispositions remain unchanged. T2 already fences
6020
+ the crash gap between the Store commit and local journal update.
6021
+
6022
+ If the command says `epoch rotated but delivery journal holds are unavailable`,
6023
+ the epoch has committed and old ciphertext is fenced. Inspect `ahub room status`
6024
+ and the affected queue ids before taking further action; do not assume the
6025
+ command rolled the roster back.
6026
+
5979
6027
  ## Screened room chat (#446 X4)
5980
6028
 
5981
6029
  After joining and confirming fingerprints, expose each receiving native peer with
@@ -32,6 +32,12 @@ terminal in that project: it reviews the plan with you, applies it and follows
32
32
  it; other projects keep their version. It then names the `bun add -g` and
33
33
  `ahub setup` step when your global CLI or plugin is older than the new daemon.
34
34
 
35
+ ## Upgrading to 0.32.0
36
+
37
+ Rooms on an ephemeral-capable broker now refresh signed presence every 30 seconds; three missed periods display advisory unknown without changing sending authority. A graceful stop attempts a signed offline snapshot within two seconds. Epoch-changing room commands list sealed outstanding delivery IDs before confirmation and immediately hold old-epoch work with the named cause. Upgrade the global CLI and plugin with the step below to see the new previews and advisory status: a 0.31.0 CLI ignores those additive fields, even in JSON. A broker without ephemeral support keeps state-change publication and reports liveness untracked, with no periodic aging. Protocol 16 through 17 and room v1 shapes remain; the N-1 baseline is published 0.31.0, with the same agent-mem 0.21.0 pin.
38
+
39
+ `ahub upgrade` does not update the global CLI or the shared Claude plugin: run `bun add -g @staix/agent-hub@0.32.0` and then `ahub setup` yourself, and `ahub doctor` warns while either is older than a running daemon.
40
+
35
41
  ## Upgrading to 0.31.0
36
42
 
37
43
  The control protocol stays at 16 through 17 and hub.db keeps schema version 2, but this release adds the nullable `deliveries.failure_cause` diagnostic column. N-1 recovery ignores the extra snapshot key, and older rows without it remain unknown. The exact `@staix/agent-mem` pin moves from 0.20.1 to 0.21.0; upgrading does not activate placement or migrate a memory home. `ahub memory declined` now lists complete bounded physical IDs and safe rule categories when inspection succeeds. Automatic v3 sync remains unsupported until agent-mem#281 provides activation and fresh host attestation. The [N-1 statement](../CHANGELOG.md) distinguishes the hub 0.30.0 fixture from agent-mem's own 0.20.3 compatibility proof.
package/docs/security.md CHANGED
@@ -179,6 +179,30 @@ completion authority. Old brokers receive the legacy frame shape. The broker wit
179
179
  hubs until they upgrade; chat
180
180
  frames remain compatible. See [room wire compatibility](design/bus.md#room-wire-compatibility-1204-t1).
181
181
 
182
+ Room liveness (#1204 T3) is advisory availability, never native execution or
183
+ process ownership proof. Periodic refresh and three-period expiry run only after
184
+ broker ephemeral negotiation. A legacy broker reports liveness untracked, with
185
+ no periodic refresh or aging. Unknown room presence adds no native `PeerState`
186
+ value or authority. Aging neither grants nor removes outbound admissibility;
187
+ signed roster, exposure, link and Bus proofs still govern sending. Explicit signed
188
+ offline presence keeps its existing handling. Freshness uses validated local
189
+ observations only; restored presence and rolled-back clocks cannot renew the
190
+ advisory liveness view, and status remains read-only.
191
+ Graceful stop fences publications synchronously and attempts one signed offline
192
+ frame in FIFO order within two seconds; durable intents/receipts remain, and
193
+ failure relies on supporting recipients' aging rather than remote process stop.
194
+
195
+ Before a person changes a room epoch, an ids-only preview binds already sealed
196
+ unresolved Store intents and the roster digest. The final commit rechecks both
197
+ with link publication quiesced. Old active intents are quarantined atomically
198
+ using the existing `boundary_refused` reason and schema, with sendable ciphertext
199
+ removed. T3's local Bus `needs_review` journal reason is the fixed
200
+ `room epoch rotated; inspect ahub queue show <id>` text; room status labels its
201
+ `holdReason` as `epoch rotated`. No body enters either reason. Bus-only queued work, terminal results and person
202
+ dispositions are unchanged. T2's existing crash-gap quarantine remains the
203
+ recovery boundary. No completion, replay, cancellation, new wire codec or new
204
+ native authority follows from the preview, rotation or liveness metadata.
205
+
182
206
  ## What is not defended
183
207
 
184
208
  - **A malicious local process running as you.** It can read the control token, `hub.db`, the project and your home directory without the hub's help. The token keeps web pages and other users out, not your own processes.
@@ -1171,7 +1171,7 @@ at most 8 pending packets/2 MiB per link, 1 MiB packet/backpressure limits and a
1171
1171
  minutes and do not block local work. Stop, leave, replacement and rotation fence
1172
1172
  all awaited work by link generation.
1173
1173
 
1174
- With latest-per-sender coalescing, A awaiting C can have its queued request replaced by A's broadcast reply to B, so C sees the request only on A's next state change or reconnect; task #736 idle refresh or per-(sender,recipient) coalescing is the upgrade path.
1174
+ With latest-per-sender coalescing, A awaiting C can have its queued request replaced by A's broadcast reply to B. T3's periodic idle refresh requests another observation without waiting for a state change or reconnect; per-(sender,recipient) coalescing remains a possible stronger transport design. Legacy brokers retain the state-change/reconnect behavior without periodic refresh.
1175
1175
 
1176
1176
  Presence is an encrypted, signed frame containing only exposed peer ids and closed
1177
1177
  idle/busy/paused/offline states. It is sent on connection and coalesced peer-state
@@ -6522,3 +6522,46 @@ permanent refusal remains inspectable. A signed late receipt may settle an
6522
6522
  unresolved ordinary remote hold once without overriding a person disposition.
6523
6523
  Invocation uncertainty retains its person-only ledger hold. Receiver application
6524
6524
  deduplication, current identity/privacy fences and all caps remain.
6525
+
6526
+
6527
+ ### Room liveness and person-visible rotation (#1204 T3)
6528
+
6529
+ The T3 contract is [room liveness and epoch changes](../design/bus.md#room-liveness-and-epoch-changes-1204-t3).
6530
+ The broker's existing negotiated ephemeral bit alone enables 30-second periodic
6531
+ `requestReply` publication and three-period (90-second) local observation aging.
6532
+ It adds no wire payload kind, codec advertisement or native `PeerState` value.
6533
+ Legacy brokers have no periodic refresh or aging and explicitly report liveness
6534
+ untracked, with a broker upgrade step. The existing state-change dedupe,
6535
+ request/reply fencing, caps, privacy and T2 receipt/dedupe ceilings remain.
6536
+
6537
+ An asynchronous, idempotent stop fences publication synchronously, attempts one
6538
+ signed offline snapshot in FIFO order behind an attempted head, and disconnects
6539
+ within two seconds while retaining durable intents and receipt outbox rows.
6540
+ Failure leaves supporting recipients to age the observation; it grants no
6541
+ remote process-stop or execution authority. Refresh period and observation clock
6542
+ are fixture seams. Restored observations cannot establish freshness; clock
6543
+ rollback makes displayed liveness unknown and status reads mutate neither time
6544
+ nor delivery state. Aging is advisory only and never changes outbound admissibility;
6545
+ signed roster, exposure, link and Bus proofs remain authoritative. Explicit signed
6546
+ offline presence keeps its existing handling, and the broker can queue for members
6547
+ that are not live.
6548
+
6549
+ Person previews for epoch-changing owner exposure, invite acceptance, revocation
6550
+ and existing-room `join --roster` adoption bind the current roster digest and the
6551
+ sorted, exact set of already sealed unresolved Store delivery ids. Link
6552
+ application publication is quiesced before the final recheck and commit; a new or
6553
+ settled id invalidates the preview. Bus-only unsealed work is excluded.
6554
+ RoomStore uses its existing schema and `boundary_refused` reason to quarantine
6555
+ old active intents atomically with the epoch and remove sendable ciphertext.
6556
+ T2's crash-gap quarantine remains the recovery fence, not a new T3 repair.
6557
+ T3 gives matching local Bus deliveries `needs_review` with fixed journal reason
6558
+ `room epoch rotated; inspect ahub queue show <id>` and room-status
6559
+ `holdReason: "epoch rotated"`. Terminal rows and person
6560
+ dispositions remain authoritative; rotation is not completion, replay or
6561
+ cancellation. A committed rotation whose local journal hold cannot be written
6562
+ returns `epoch_rotated_hold_unavailable` with an inspection step, without
6563
+ restoring old sendability.
6564
+
6565
+ Source and isolated fixture coverage are listed in [dogfood](../dogfood.md).
6566
+ Those fixtures do not establish installed or two-machine liveness. #1204 remains
6567
+ open for T4's full capability advertisement and compatibility work.
@@ -67,6 +67,15 @@ Truncations (`.slice`, `.substring`) are decided by what is cut:
67
67
  6. The **hub's own list** cut to N (`pending.slice(0, 64)`, `history.slice(-5)`) is **operational**, and so is a **display cap on the hub's own report** (`pool.walk.length > 16`, `paths.length > 20`, a held-rows cap).
68
68
  7. **Text cut for a log line, notice or reply** is a **safety floor** only when the cut text **originates outside the hub process**: a model, a peer, a person, another project, a native hook process, or a subprocess, network, git or file-parse error that can carry external content. The hub's own composed message, or its own internal error text logged locally, is **operational**. Ask where the text comes from, not where it goes.
69
69
 
70
+ Rulings of #741 (the conductor, 2026-11-06), added after a fresh blind sample (seed 741001) agreed on 70% and the misses fell into six shapes. They extend lines 1 to 7; where one differs, the later line decides its shape:
71
+
72
+ 8. A cap on **the hub's own cache, set, map or concurrency limiter** is **operational**, and so is the line 3 reading of lists, batches and reports. Exception: decide by **what one insert is caused by, not by the name**. A set or map that gains one entry per event **another process controls** (a peer's envelope, a remote room frame, a native hook's tool call, a file a person or agent writes, an acknowledgement a caller names) is a **safety floor**: it bounds memory against outside input. A set the hub fills only from its own decisions (a check run, a launch candidate, an approval, a prepared review) is operational.
73
+ 9. A **silent truncation of a tool argument** (the hub cuts it and goes on, with no refusal) is a **contract** when the limit is stated in that tool's schema (`maxLength` in `hub-tools.ts`) or in `docs/operations.md`; otherwise it is a **safety floor**. The same argument in two tools is decided per tool: `hub_backlog_split` states `title` 300 and `detail` 8000, `hub_task_propose` does not, so its cut is a safety floor.
74
+ 10. **Error or stack text** cut for a log, notice or reply is a **safety floor**, unless the text is a **fixed phrase the hub composed**: then the cut is a display cap and **operational**.
75
+ 11. A cap on **what a read tool returns** (edges, lines, bytes of one result) is a **contract**: it is the documented shape of that result.
76
+ 12. A bound on **a field of a documented message or envelope** (a task handoff key's project id, the `refs.paths` of a deferred envelope, a hub task envelope's `refs.backlog`) is a **contract** under line 1, as is a shortened id or hash (`lease.holder.slice(0, 8)`).
77
+ 13. A bound on something **parsed from git, GitHub or a subprocess** (a path inventory, a control line, a config key listing, a documents list, a tail of runner output) is a **safety floor**: it fails closed against content the hub does not write.
78
+
70
79
  A number is never evidence by itself. Many unrelated schema entries share `32`, `64`, `256` or `4096`, so "the same number appears in `hub-tools.ts`" decides nothing: the first measurement of #739 counted 280 of 378 `src/hub` bounds as mirrors of a schema by number, and only a handful really repeat a tool schema's limit once the call sites are read.
71
80
 
72
81
  ### What the committed file did before this change (measured on `8e44babc`)
@@ -254,4 +263,49 @@ Round 2 clears the 85% bar. It is **in-sample in one respect**: lines 5 to 7 and
254
263
 
255
264
  Round 2's nine misses: three are keys the first list left out (`workflow.ts` criteria 32, criterion refs 8 and `claude-launch-proof.ts` launchId 128, relabelled contract above); `check-runner.ts` `lines.slice(-40)` (a cut of a subprocess's output, which line 7 calls safety floor, labelled by me operational) and `version.stdout.trim().slice(0, 80)` (the converse); `daemon.ts` `schedules pass failed: ...message.slice(0, 160)` and `tasks.ts` `task.title.slice(0, 120)` (a memory title from the task's own title, where whether the origin is outside the hub is a judgement); `execution-budget.ts` `input.id.length > 200` and `delivery-hints.ts` `held.rows.length > 32`. The remaining disagreements are about where text originates, and line 7 is the place to sharpen if a later sample falls short.
256
265
 
257
- No baseline key is labelled in this change. That is #741.
266
+ No baseline key is labelled in this change. That is #741 (below).
267
+
268
+ ## Batch 1 of #741: 149 `src/hub` length bounds (#741)
269
+
270
+ Date: 2026-11-06. Refs #1180. The population is the 187 baseline keys in `src/hub` that are a length or size bound and not a truncation. The 38 left in the baseline are the keys no line decides without a ruling or a caller trace: lower bounds that only say "not empty" or "at least N" (`< 1`, `>= 2`, `< 8`, `before.size < 1`), bounds whose message the key does not name (`daemon.ts` `values.length > 256`, `tool-request.ts` scope and bucket 4096, `operational-view.ts` metadata lists, `specs.ts` `base.length > 200`), research-campaign and experiment bounds in `board.ts` (`owners`, `hostEvidence`, runner receipts), and read-side display caps in `publicReadValue`. A key left here is reported for a ruling, not guessed. Truncations (`.slice`, `.substring`) are batch 2.
271
+
272
+ ### Calibration
273
+
274
+ | Round | Seed | File state | Reader | Agreement | Result |
275
+ | --- | --- | --- | --- | ---: | --- |
276
+ | 1 | 741001 | after #739, lines 1 to 7 | blind reader, lines 1 to 7 | 56 of 80 (70.0%) | below the bar: 24 misses in six shapes (below) |
277
+ | 2 | 741002 | after the 17 relabels below, lines 1 to 13 | blind reader, lines 1 to 13 | 68 of 80 (85.0%) | meets the 85% bar |
278
+
279
+ Both samples are drawn from the decided `src/hub` bounds and truncations (`hub-tools.ts` excluded), without replacement across rounds (round 2 excludes round 1's 80 keys). The reader read each key at its call site with the committed label hidden and wrote every label before scoring.
280
+
281
+ Round 1's misses, by shape: 6 bounds on a message or envelope field the file called safety floor or operational (line 12); 5 bounds on content parsed from git, GitHub or a subprocess (line 13); 4 caches, sets and limiters (line 8); 4 cuts of error or stack text (line 10); 3 silent truncations of a tool argument (line 9); 2 caps on a read tool's output (line 11). The conductor ruled each; the rulings are lines 8 to 13 above.
282
+
283
+ Round 2's 12 misses. Five are **relabelled** in this PR (the rule decides and the file contradicted it, so they follow #739's practice of changing a contradicting entry by name): `plan-conformance.ts` `planPaths.length > 1000` (operational to safety floor, rule 13: it is the same check as `completePlanInventory`, on the plan paths compared with a PR's file list read from GitHub), `usage.ts` transcript size 32 MiB (operational to safety floor, rule 13: it bounds a file a Claude process writes), `github-import.ts` 2 MiB read (operational to safety floor, rule 13: GitHub's output), `deferred-hub.ts` `env.to.length > 256` (safety floor to contract, rule 12: a field of the deferred envelope, like its `refs.paths`) and `deliverable-files.ts` `message.replace(path, "the path").slice(0, 160)` (safety floor to operational, rule 10: a guardPath refusal the hub composed). One is **decided and left**: `delivery-journal.ts` `Object.keys(queues).length > 4096` stays safety floor, because `queues` is a field of the journal snapshot the hub writes and reads back, which no document names, so rule 12 does not apply and rule 13 reads a damaged file as outside content. Six **stay open for batch 2**: `settings.ts` `presetValue` 4096, `dogfood-read.ts` `line.slice(0, 24)`, `claude-launch-proof.ts` `executionCwd` 4096, `board.ts` `failure.head.length > 300`, `code-facts.ts` `a.at.length < 9` and `spec-read.ts` `issues.size > 3`: no line decides them without a ruling.
284
+
285
+ ### Existing labels changed by the rulings (17, by key, old to new; the five round 2 relabels are listed in the round 2 paragraph above)
286
+
287
+ | Rule | Key | Old | New |
288
+ | --- | --- | --- | --- |
289
+ | 8 | `check-runner.ts` `this.entries.size >= 3` (in `start`) | safety floor | operational |
290
+ | 8 | `code-facts.ts` `this.cache.size>128` (in `query`) | safety floor | operational |
291
+ | 8 | `memory-observations.ts` `this.pending.size > 2048` (in `delivery`) | safety floor | operational |
292
+ | 8 | `daemon.ts` `injected.size > 512` (in `readTranscript`, one entry per hook line a native process writes) | operational | safety floor |
293
+ | 8 | `limits.ts` `this.recent.size > 1000` (in `admit`, one entry per distinct message a peer sends) | operational | safety floor |
294
+ | 8 | `daemon.ts` `retainedCliAudits.size > 4096` (in `drainCliAuditsNow`, one entry per audit a CLI process sends) | operational | safety floor |
295
+ | 10 | `daemon.ts` `sanitize(error.message).slice=1000` (in `error`, a fixed Pi version refusal) | safety floor | operational |
296
+ | 12 | `deferred-hub.ts` `refs.paths.length > 256` (in `validateDeferredEnvelope`) | safety floor | contract |
297
+ | 12 | `tasks.ts` `projectId.length > 200` (in `handoffStateByKey`) | safety floor | contract |
298
+ | 12 | `bus.ts` `backlog.length > 64` (in `queuedBacklogIds`) | safety floor | contract |
299
+ | 12 | `daemon.ts` `g.lease.holder.slice=8` (in `goalLine`) | operational | contract |
300
+ | 13 | `plan-conformance.ts` `evidence.paths.length > 1000` (in `completePlanInventory`) | operational | safety floor |
301
+ | 13 | `code-facts.ts` `keys.length>128` (in `nativeEnv`) | operational | safety floor |
302
+ | 13 | `code-facts.ts` `control.length>64` (in an anonymous stream handler) | operational | safety floor |
303
+ | 13 | `sdlc-release-authorization.ts` `value.documents.length > 128` (in `docsVerdict`) | contract | safety floor |
304
+ | 13 | `check-runner.ts` `lines.length > 40` (in `cut`) | operational | safety floor |
305
+ | 13 | `check-runner.ts` `Buffer.byteLength(result) > 4096` (in `cut`) | operational | safety floor |
306
+
307
+ (The 17 are the 15 of the first pass and two found when rule 8 was read by cause. `daemon.ts` `error` is the Pi refusal, a fixed phrase, and the other `sanitize(error.message).slice=1000` keeps safety floor.)
308
+
309
+ ### The 149 keys labelled, by label
310
+
311
+ Each key was read at its call site and decided by the rule named. 25 operational (rule 8 for the hub's own caches and display caps, rule 6 for a hub-built report), 77 safety floor (rule 13 for content parsed from git, GitHub or a subprocess, the config or environment values the hub reads and fails closed on, rule 8 for sets one outside event fills, rule 2 for free text no message defines), 47 contract (rule 1 and 12 for a documented message or envelope field, rule 4 for a documented file the person writes, rule 11 for a read tool's result). The keys are in `scripts/inventory-hardcoded-values.classifications.json`; the baseline file lost exactly those 149 and gained none (1155 to 1006).
@@ -0,0 +1,149 @@
1
+ # 0.32.0 release verification
2
+
3
+ This record covers release preparation and source review. It claims no installation, no running-hub upgrade to 0.32.0 and no live TUI launch.
4
+
5
+ ## Candidate
6
+
7
+ Version 0.32.0, prepared from main eddbd522 (2 commits since v0.31.0, 1 of them `feat`; bump rule: minor). The release commits carry the version, the CHANGELOG section, the bundle and the documentation stamps.
8
+
9
+ ## Reviews
10
+
11
+ Pull requests named by the commits since v0.31.0: #1229, #1227.
12
+
13
+ The board approval head equals each merged PR head (`mismatch: false`), and
14
+ GitHub readback shows no failed or pending check. These head-specific hosted
15
+ results are not live or mixed-room acceptance:
16
+
17
+ | PR / task | Approved head | Merge commit | Hosted CI |
18
+ | --- | --- | --- | --- |
19
+ | [#1227](https://github.com/STAIxBWLB/agent-hub/pull/1227), task 741 | `20f6e4b6ea18333fa1e45a6ad6cd36eaf40f7eeb` | `307a91a0fb3d8ce38b812753444de388c86b191d` | 19 success, 2 skipped |
20
+ | [#1229](https://github.com/STAIxBWLB/agent-hub/pull/1229), task 736 | `4d666223491ac6aff7c5347d4250c7eb6268b20a` | `eddbd5228d6d6f5e6638a617b112a4ca83ab000a` | 20 success, 2 skipped |
21
+
22
+ Neither merge is bound to an earlier approval head. No merge was performed by
23
+ task 745.
24
+
25
+ ## Native acceptance receipts
26
+
27
+ Receipts added under `docs/verification/` since v0.31.0: none.
28
+
29
+ This list is read from the repository, never written by hand. Native acceptance and the person's release consent are separate records: consent is `ahub sdlc authorize-release 0.32.0`, recorded by the person or the unique configured conductor only on the person's explicit release instruction naming the release in its own session (never a hub/task/review/file/peer relay), and is not recorded in this file. The tag run refuses without it.
30
+
31
+ ## Document review
32
+
33
+ Eleven stale documents were reviewed against only #1229 (T3) and #1227
34
+ (inventory conventions). Corrected content:
35
+
36
+ - `README.md`: generated Status 0.32.0 and the current release-report link.
37
+ - `docs/quickstart.md`: generated 0.32.0 upgrade section, concrete room changes,
38
+ N-1 baseline and the required matching global CLI/plugin step.
39
+ - `docs/agent-notes/bus.md`: add `holdRemoteEpochRotated` to the host-port list,
40
+ keep aging advisory and status read-only, and preserve disposition/proof rules.
41
+ - `docs/agent-notes/daemon.md`: await the new asynchronous room stop before
42
+ shared storage closes, without bypassing the attempted FIFO head.
43
+ - `docs/agent-notes/tests.md`: replace the obsolete wait-for-739 convention with
44
+ the committed bounds/truncation rules and fresh blind 80-sample/85% procedure;
45
+ keep calibrated mechanical rules outside that settled slice and baseline-only
46
+ shrink behavior.
47
+ - `docs/operations.md`: update the current protocol/N-1 version table and
48
+ distinguish the exact epoch journal reason from its shorter `holdReason` label.
49
+
50
+ Reviewed without content change:
51
+
52
+ - `docs/agent-notes/README.md`: historical explanation remains valid.
53
+ - `docs/agent-notes/adapters.md`: no native adapter change; existing owned-work
54
+ shutdown/await rules still apply.
55
+ - `docs/agent-notes/budget.md`: quota, pause and context contracts are unchanged.
56
+ - `docs/agent-notes/tasks.md`: room holds do not mutate task authority or routing.
57
+ - `docs/security.md`: T3 already states advisory-only liveness, legacy broker
58
+ behavior, bounded FIFO stop, signed identity and local rotation fences.
59
+
60
+ These 11 reviewed documents were restamped through the official
61
+ `release-prepare restamp ... --reviewed` path after the reviewed content commit
62
+ `2f4c372a6ce48dc01acb4a002f6a46e9557cbd17`. Following the approved fixture
63
+ exception, the tests note was reviewed again and its final stamp points to
64
+ `4ad77748c96383bdef8e55a11ff53808a1359500`; the other ten retain `2f4c372a`.
65
+ Fresh benchmarks, local-worker, models and help stamps were not refreshed.
66
+ All watched path definitions remain unchanged.
67
+
68
+ ## Checks
69
+
70
+ The official generator was dry-run first, then executed on dgx02 from committed
71
+ `eddbd5228d6d6f5e6638a617b112a4ca83ab000a`, on exact branch `release/0.32.0`,
72
+ after fetching the existing published v0.31.0 tag and a frozen root install.
73
+ Bun 1.4.2 generated the bundle/manifest and the separately retargeted fixture
74
+ lock. Approved artifact-return exception `981f5052` returned only the bundle and
75
+ manifest plus task source/generator outputs and fixture lock; no node_modules,
76
+ target or other build output returned. Producer hashes:
77
+
78
+ - `server.js`: `6f48c2f68e062798eab50b00b14c3319f03f06465f194d8b133f5641607e4bce`.
79
+ - Plugin manifest: `a8c5f3b2295ac58c935eb6df65617dfc887f1fd357cc989ee0400752d8232606`.
80
+ - N-1 `bun.lock`: `de66be194ed847153324d9715422355e0984d49fa7c903ac15451a49db171a91`.
81
+
82
+ npm readback confirms 0.31.0 is the only published 0.31.x patch and pins agent-mem
83
+ 0.21.0. The fixture manifest/matrix now match; protocol floor 0.25.0 is unchanged.
84
+ The initial full run at `a182b06b` exposed a fixture-only positive-host gap:
85
+ the now-retargeted published 0.31.0 plugin already requires native parent-row
86
+ evidence, while the fixture injected it only for current source. The unchanged
87
+ published bundle correctly attached tools-only under the test runner parent.
88
+ Approved exception `67147dad` bootstraps that published import with one exact
89
+ `/bin/ps` raw parent-row observation and delegates every other subprocess call.
90
+ Pinned-runtime inspection showed Bun 1.4.2 does not rebind static builtin named
91
+ exports after a default-export mutation and syncBuiltinESMExports. Approved
92
+ amendment `06f31614` therefore uses Bun's built-in `mock.module` only inside the
93
+ published-leg child for `child_process` and `node:child_process`. Every real
94
+ function is exported; only `spawnSync` gets the same exact ps predicate and
95
+ other calls delegate. It adds no runtime dependency or product change.
96
+ Both fixture forms use the shared synthetic row constant; published bytes,
97
+ classifier, marker, parent check and every assertion/timeout remain unchanged.
98
+ This is synthetic host setup, not real native-launch acceptance. The original
99
+ red run was intentionally stopped after the known setup failure (697 seconds,
100
+ exit 143); it is not a completed full-suite result. Pinned Bun 1.4.2 validation
101
+ of the corrected setup passed as recorded below.
102
+
103
+ At committed `b9c5e1ffb13f0991bfe66b1dd5bcd8621f8538b9`, dgx02 used frozen
104
+ root and published-fixture installs, Bun 1.4.2 and typecheck. The focused actual
105
+ published 0.31.0 client/unchanged plugin and protocol-floor 0.25.0 legs passed
106
+ in both directions, followed by production evidence-classifier tests: 7 pass,
107
+ 0 fail, 79 assertions across two files in 2.09 seconds. These are common-control
108
+ and MCP interoperability results under synthetic native-host observations;
109
+ they are not mixed-room or real-Claude acceptance.
110
+
111
+ The same committed-head run then executed
112
+ `node scripts/check-docs.mjs --require-fresh && scripts/check.sh --changed`.
113
+ The changed gate selected the full suite and passed:
114
+
115
+ ```text
116
+ 12014 pass
117
+ 101 skip
118
+ 0 fail
119
+ 311749 expect() calls
120
+ Ran 12115 tests across 934 files. [2010.22s]
121
+ check: no current-invocation agent-hub daemon leaks
122
+ check: OK
123
+ remote_dev: verify dev/agent-hub/.worktrees/release-0.32.0 b9c5e1ffb13f exit 0 in 2067s
124
+ ```
125
+
126
+ Documentation was fresh with 0 stale documents; roles, inventory (3281 entries,
127
+ 2276 classified, 1005 baseline), bundle freshness and package contents (795 files,
128
+ 0.32.0) passed. The run's total wall time includes the focused leg above.
129
+ This following report-only commit records that completed result; it does not
130
+ inherit a new full-suite claim for itself. Final-head documentation/delta checks
131
+ and hosted CI are recorded in the PR, and exact-head CI remains the merge gate.
132
+
133
+ ## Known gaps, not blocking
134
+
135
+ - No installation, running-hub upgrade, real account, live TLS/proxy or two-machine
136
+ room run is performed by this preparation. No mixed published-version room
137
+ execution is claimed: the N-1 room directions in CHANGELOG follow unchanged
138
+ codecs/source comparison and isolated current-version broker tests.
139
+ - An offline stop snapshot is a bounded attempt behind the existing FIFO head,
140
+ not a guarantee under backpressure. Legacy brokers without ephemeral support
141
+ do not refresh or age; upgrade the broker for that liveness behavior.
142
+ - A 0.31.0 CLI drops additive advisory/hold fields and outstanding preview IDs,
143
+ including in JSON. A 0.32.0 client accepts absent metadata from a 0.31.0 daemon
144
+ and keeps its prior state-only/fingerprint-confirmation behavior. New displays
145
+ require the matching CLI and daemon.
146
+ - #1204 remains open for T4 visibility, deferred-inbound wake and full codec
147
+ features. #1173 still waits for supported activation/fresh host attestation in
148
+ agent-mem#281. Existing live gates for #1053, #1056 and #1196 remain separate.
149
+ - No merge, tag, npm publication or release-consent record is made by this task.