@staix/agent-hub 0.30.0 → 0.31.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 (125) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +13 -7
  3. package/docs/agent-notes/README.md +1 -0
  4. package/docs/agent-notes/adapters.md +5 -2
  5. package/docs/agent-notes/budget.md +1 -0
  6. package/docs/agent-notes/bus.md +9 -4
  7. package/docs/agent-notes/daemon.md +15 -8
  8. package/docs/agent-notes/local-worker.md +1 -0
  9. package/docs/agent-notes/tasks.md +7 -3
  10. package/docs/agent-notes/tests.md +6 -4
  11. package/docs/design/adapters.md +52 -0
  12. package/docs/design/agent-hub-architecture.md +46 -0
  13. package/docs/design/bus.md +139 -1
  14. package/docs/design/task-board.md +82 -0
  15. package/docs/dogfood.md +22 -4
  16. package/docs/events.md +3 -0
  17. package/docs/memory.md +17 -1
  18. package/docs/operations.md +307 -78
  19. package/docs/profiles.md +35 -0
  20. package/docs/quickstart.md +18 -5
  21. package/docs/security.md +154 -60
  22. package/docs/specs/2026-09-19-agent-hub-design.md +84 -14
  23. package/docs/specs/2026-09-19-multi-project-design.md +7 -2
  24. package/docs/verification/2026-11-04-1171-channel-client-evidence.md +31 -0
  25. package/docs/verification/2026-11-05-0.31.0.md +182 -0
  26. package/docs/verification/2026-11-05-claude-implementer-2.1.318.md +70 -0
  27. package/docs/verification/2026-11-05-hardcoded-values-classification-rules.md +257 -0
  28. package/docs/verification/2026-11-05-hardcoded-values-ratchet.md +55 -0
  29. package/docs/verification/2026-11-06-rooms-presence-cost.md +152 -0
  30. package/docs/verification/claude-shell-wrapper-native.py +1 -1
  31. package/docs/verified.json +14 -14
  32. package/package.json +3 -4
  33. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  34. package/plugins/agent-hub/server.js +4393 -3609
  35. package/src/adapters/acp.ts +5 -4
  36. package/src/adapters/channel-peer.ts +72 -3
  37. package/src/adapters/claude-channel-evidence.ts +52 -0
  38. package/src/adapters/claude-channel.ts +61 -18
  39. package/src/adapters/claude-implementer.ts +29 -7
  40. package/src/adapters/claude-print-child.ts +12 -2
  41. package/src/adapters/codex-appserver.ts +13 -12
  42. package/src/adapters/compat.ts +3 -3
  43. package/src/adapters/definitions/builtin-metadata.ts +13 -13
  44. package/src/adapters/definitions/qwen.ts +1 -0
  45. package/src/adapters/local-worker.ts +61 -18
  46. package/src/adapters/pi.ts +48 -6
  47. package/src/adapters/registry-types.ts +12 -0
  48. package/src/adapters/registry.ts +2 -0
  49. package/src/adapters/reviewer-print.ts +50 -11
  50. package/src/broker/server.ts +44 -13
  51. package/src/broker/store.ts +38 -8
  52. package/src/cli/answer-args.ts +50 -0
  53. package/src/cli/attention.ts +17 -2
  54. package/src/cli/backlog-return-args.ts +37 -0
  55. package/src/cli/console-question.ts +110 -0
  56. package/src/cli/console-state.ts +77 -16
  57. package/src/cli/console.ts +54 -1
  58. package/src/cli/handoff-args.ts +15 -5
  59. package/src/cli/handoff-output.ts +15 -2
  60. package/src/cli/help.ts +8 -5
  61. package/src/cli/identity.ts +13 -3
  62. package/src/cli/main.ts +128 -16
  63. package/src/cli/memory.ts +107 -3
  64. package/src/cli/output.ts +29 -6
  65. package/src/cli/pet.ts +4 -4
  66. package/src/cli/results-args.ts +30 -0
  67. package/src/cli/runtime-version-warning.ts +50 -0
  68. package/src/cli/status-lines.ts +41 -1
  69. package/src/cli/tail-render.ts +1 -0
  70. package/src/cli/upgrade-interactive.ts +14 -2
  71. package/src/hub/action-class.ts +9 -1
  72. package/src/hub/board.ts +369 -18
  73. package/src/hub/bus.ts +493 -53
  74. package/src/hub/completion-detail.ts +50 -0
  75. package/src/hub/completions-store.ts +168 -0
  76. package/src/hub/completions.ts +120 -0
  77. package/src/hub/config-apply.ts +7 -5
  78. package/src/hub/config-reload.ts +10 -1
  79. package/src/hub/control-client.ts +40 -4
  80. package/src/hub/daemon.ts +1569 -134
  81. package/src/hub/delivery-failure.ts +14 -0
  82. package/src/hub/delivery-hints.ts +14 -4
  83. package/src/hub/delivery-journal.ts +751 -42
  84. package/src/hub/delivery-superseded.ts +4 -1
  85. package/src/hub/envelope.ts +3 -3
  86. package/src/hub/events.ts +6 -0
  87. package/src/hub/handoff.ts +145 -6
  88. package/src/hub/hub-tools.ts +56 -7
  89. package/src/hub/manager.ts +59 -3
  90. package/src/hub/memory-policy.ts +44 -1
  91. package/src/hub/memory-sync-policy.ts +109 -0
  92. package/src/hub/memory-sync-state.ts +145 -0
  93. package/src/hub/no-writes.ts +1 -1
  94. package/src/hub/operational-settings.ts +11 -2
  95. package/src/hub/operational-view.ts +12 -1
  96. package/src/hub/peers.ts +25 -2
  97. package/src/hub/planning.ts +6 -1
  98. package/src/hub/question-desk.ts +201 -0
  99. package/src/hub/question-detach.ts +6 -0
  100. package/src/hub/questions.ts +260 -0
  101. package/src/hub/research-evidence.ts +106 -0
  102. package/src/hub/restart.ts +14 -0
  103. package/src/hub/review-anchor.ts +181 -0
  104. package/src/hub/review-brief.ts +13 -0
  105. package/src/hub/review-comments.ts +115 -0
  106. package/src/hub/review-findings.ts +7 -2
  107. package/src/hub/review-material.ts +76 -0
  108. package/src/hub/routing.ts +3 -1
  109. package/src/hub/task-sweep.ts +2 -0
  110. package/src/hub/tasks.ts +577 -63
  111. package/src/memory/capture.ts +5 -2
  112. package/src/pet/config.ts +25 -13
  113. package/src/rooms/link.ts +209 -21
  114. package/src/rooms/remote-peer.ts +186 -19
  115. package/src/rooms/service.ts +338 -22
  116. package/src/rooms/store.ts +437 -65
  117. package/src/rooms/wire.ts +24 -6
  118. package/src/ui/index.html +138 -7
  119. package/src/ui/needs.ts +10 -4
  120. package/src/ui/plugin-questions.ts +56 -0
  121. package/src/ui/plugin-results.ts +81 -0
  122. package/src/ui/plugin.ts +207 -27
  123. package/src/pet/window.html +0 -114
  124. package/src/pet/window.jxa.js +0 -122
  125. package/src/pet/window.ts +0 -154
@@ -19,10 +19,12 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
19
19
  - Capture the coordination preference once per task at creation (`coordinationModes` from `TasksDeps.coordination`, in `propose` and for each split child, #439). `formCohort` joins only overlaps with the same captured mode and forms a silent cohort only for a turn-free one; a reload never silences an existing task or lifts an existing silent cohort. A daemon restart re-pins retained rows to the startup mode (a compatibility ceiling, not task metadata). `configureSweep` takes effect at the next pass: a running `sweep` keeps the config it started with.
20
20
  - The planner "board changed" notice (#758, only with the lifecycle on) is built from what was added to a card's history since the last scan (`Tasks.groomNotices`, `groomScan`), never from the status revision alone: skip the peer whose own entry it was (`entry.by`), tell only the card's planners (`taskPlanner`, #525-2), send a planning revision or dependency change (a `planning` entry, or `deps` that differ from the last scan whatever wrote them: a propose with `after`, a split) and a card's creation (a peer's propose, a split child, a generated card) at task weight, since new work and its dependencies are what a plan review consolidates, and replace an unsettled copy (`sendGroomNotice` withdraws it by the `The board changed` prefix) so a planner meets the latest revision once, and send state moves, reviews and assignments at `fyi` weight (recorded, no turn), at most once per planner per current `tasks.groom_fyi_window_min` (default `GROOM_FYI_WINDOW_MS`, 10 minutes); read the host config at each notice flush, never capture it at construction. The hub's own bookkeeping entries (`by` is `hub` with no `planning`: lifecycle links, idle sweeps, holds, plan observations) tell nobody and spend no window; a deps change with no new entry has no known author, so no planner is skipped for it. Scan every configured planner, not only the attached ones, so a change made while one was offline is kept for it. The first notice a planner gets after a hub start is task weight. Keep the scan out of the status revision and never read history that is not new.
21
21
  - Planner work-ready notices (#785 P1a, `writePlannerNotices`): a write that can free a reserved owner's WIP or release backlog (done, approval, decline, cancel, backlog return or pull, untouched return, pull undo) takes `capacityBefore` (the `wip()` of the task's owner and reservation) before its board write and calls `writePlannerNotices` after it. Each rostered planner of the task (`taskPlanner`) gets one ids-only `task` envelope starting `Planner work ready:`: `capacity_released: <peer> WIP <n>; reserved ready #<id>|none` for a peer whose WIP dropped, and, when the write approved the task, `backlog_ready: #<id>` for each backlog dependent with nothing left to wait for. Merge the lines of that planner's unsettled copies, read back from `bus.pendingEnvelopes` (they survive a restart; never keep them in memory), withdraw them, then publish, so at most one stays queued per planner; all of it runs inside `releaseNotice`. The notice only wakes the planner: never pull, reserve or assign from it, and never put a title or note in it.
22
+ - Person-run cards (#1176, `needsPerson` / column `needs_person`): the mark is the person's alone (`planBacklog` refuses an agent that sets or clears it or plans a marked card), and every way an agent could be offered the card checks it: `topReservedReady` (auto pull and the planner notice), the one pull gate in `pullPlanned`, `offerReady`, `releaseReady` (not a stall), `assign()` (pure, before any routing, so `route explain` agrees), `nextSweep`, and the two `plannerWip` queries; and the other doors: `splitBacklog` (agent refused in `Tasks` before and after the screen and in the board transaction; the person's children inherit the mark), `accept` and `acceptReviewed`, `assignTo` for the owner role, `relevant` (drops a queued offer), and `planBacklog` (refuses a mark on an owned card). A new way to offer a card to an agent must check `needsPerson` too; `test/person-run-cards.test.ts` has a mutation-checked test per gate.
22
23
  - Opt-in auto pull (#785 P1c, `tasks.auto_pull`, `Tasks.queueAutoPulls`, `autoPull`, `topReservedReady`): `writePlannerNotices` collects the peers whose WIP this write lowered and, only when `TasksDeps.autoPull()` is true (read at each release, so a reload applies to the next one), queues one `autoPull(peer)` per peer on a promise chain, so two releases never pick the same card and each job re-reads the board. It takes the top backlog card explicitly reserved for that peer (`topReservedReady(peer, undefined, true)`, the rule the notice's `reserved ready #id` line also uses; never an unreserved card, never PII, which `topReservedReady` skips for a pull), finds the card's own planner of record (`taskPlanner(card)[0]`, which must be rostered) and calls `pullPlanned(HUB, input, { onBehalfOf: planner })`. `onBehalfOf` is accepted only with `by === HUB`; the planner's authority is what `planningAuthority` checks, the actor in history and the planning telemetry stays `hub`, and the planner binding records the planner it acted for. Every other check is `pullPlanned`'s own, unchanged, and a refusal is one console notice with the public title (never detail). A held or recovering hub, a storage error or the setting off pulls nothing, and that is checked twice: at entry and again by `pullPlanned`'s `opts.guard` (`autoPullAdmitted`) wherever authority is rechecked, which includes right before the assignment or board write after the awaited PII screen and `planReviewGap`, so turning the setting off or starting recovery or shutdown mid-admission writes nothing and reports nothing. `autoPullsPending()` counts queued and running pulls and `recoveryReady` waits for it (also named in the recovery view's blockers), as it does for `checksPending`. The key is in `ALL_MACHINE_LOCAL` (`config-trust.ts`) so a committed config is ignored for it, and it is not in `CONDUCTOR_SETTING_KEYS`: an agent never turns it on. Never add a timer or sweep that pulls: the only trigger is the write that freed the slot.
23
24
  - Planned start (#1092): `plannedStartPending` (`src/hub/task-sweep.ts`) is the only test that a card's `plannedStart` is still ahead. Every automatic path skips such a card: dependent readiness, `backlog_ready` notices, the reserved-ready pick, auto pull and its `guard`, a hub-actor (`by === HUB`) offer, pull or assign of an ownerless backlog or proposed card, and the idle sweep. The planner digest shows it as `scheduled`. A new automatic path asks it too.
24
25
  - Per-head CI watcher (#785 P1b, `src/hub/ci-watch.ts`, `Tasks.watchCi`, `ciCompleted`, `reofferHeldReview`): when a `done` leaves a review whose admission was not ready at the task's exact full-commit head (or a review is held as premature in `sendReview`), `CiWatchers` starts at most one watcher per task and head, with chained unref'd `setTimeout`s and no interval. Each poll is one `gh pr view` through the `sdlc.gh` port (`pollCi`); it ends on a passed result for the required checks, on `CI_WATCH_DEADLINE_MS`, on `stopCiWatchers` (daemon `stopOnce`), or silently when `ciWatchCurrent` fails (the task left `in_review`, its head or PR changed, the lifecycle is off, it turned private). `complete`, `review` (both verdict writes) and `cancel` cancel it. Each (head, outcome) is told once (`settled`), and only `passed` settles a head for good (#540): after `failed` or `incomplete` the watcher keeps reading for a rerun of that head (the first pending read after each newly told result is a new run attempt and renews the deadline once; later pending or unreadable reads and an unchanged failed result never renew it, so the watcher always ends; a deadline after a told result ends silently), and a later `hub_task_done` at that head starts one again. A newer head replaces the watcher. The result is one ids-only `ci_completed:` task envelope at `status` weight to the owner and the reviewer (`fyi` is never delivered to a peer, so it would not reach them), a console notice for `user`, and for a deadline a console notice only. A passed or incomplete result re-offers a review held as premature once (`heldReviews`, keyed by task, head, reviewer and review revision) after a fresh `reviewRequestAdmission` says ready; a failed one re-offers nothing. No task text goes in the notice, no PII task is watched, and nothing is restored after a hub restart (the next `hub_task_done` starts one). Never add a second periodic gh read outside this watcher.
25
26
 
27
+ - Handoff idempotency (#1203 T1): the target writes a handed-off task only through `Board.proposeHandoff` (the proposal and `task_handoff_origin` row, key and request digest in one `atomic` transaction); `handoffIn` asks `replay()` before the duplicate check and again after the PII screen's await, so a retry of a key whose first attempt is still screening files nothing twice. Never `propose` then `setHandoffOrigin` for a handoff, and never compare a key to anything but this origin project's own rows.
26
28
  - Completed-change notices (#713, one per recipient per done, accepted work only, superseding by `completedFor`): rule in `bus.md`.
27
29
  - Plan-symbol overlap (#682 1b) compares through `symbolsOverlap` (`src/hub/symbol-normalizer.ts`): arguments stripped, `section `/`source ` keys exact, two qualified names equal, otherwise last segments equal. `places`, `overlapHits`, `contradict`, the completed-notice replay, `plannerGrooming` and `planSymbolMatches` all use it; never compare plan symbols with `includes` or set membership.
28
30
  - Task code symbols (#684 2a) are host-derived: stored only in the `task_code_symbols` table and read through `TASK_SELECT` (`src/hub/board.ts`), so a `SELECT * FROM tasks` reader never exports them. `Board.update` writes `codeSymbols` only with `HOST_CODE_SYMBOLS` and drops it when the head (`refs.commit`, else `sdlc.head`, else `sdlc.localHead`), `refs.repo`, `refs.paths` or `sdlc.repo` changes or the task turns private. Tasks observes them only inside an admitted `done (checking)` or `done` write (`observedCodeSymbols`: non-PII, a full commit, the project repository); a failed or mismatched observation is unknown, never a refusal. Optional code facts (`prepareReviewBrief`) skip PII tasks, never refuse a review, and the actor guard runs again after them.
@@ -47,7 +49,7 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
47
49
 
48
50
  - Memory read tools (`hub_memory_search`, `hub_memory_show`, `hub_memory_graph`, #412) take their project only from the daemon. Validate UTF-8 input caps before logs or storage, require `memory_read`, refuse PII turns and exclude arguments/results from capture. Screen complete rows and graph metadata before counting or truncating; keep whole-result byte caps, complete id/provenance/kind headers and one neutral unavailable-id refusal.
49
51
 
50
- - Compose structured `hub_remember` fail fields through `composeRememberText` before screening or writes; reject over 4000 UTF-8 bytes without truncation. Treat plan-fail matches as screened advisory priority only, never a state-transition guard; skip all plan-memory work for PII tasks.
52
+ - Compose structured `hub_remember` fail fields through `composeRememberText` before screening or writes; reject over 4000 UTF-8 bytes without truncation. Treat plan-fail matches as screened advisory priority only, never a state-transition guard; skip all plan-memory work for PII tasks. A long-term note (`horizon: "long"`, or an approved task's promoted kind) passes `memorySyncPolicy().screen` (`src/hub/memory-sync-policy.ts` plus the hub's PII patterns and private paths) before `store.note`. A decline, or a policy that is unavailable, writes nothing: `hub_remember` refuses pre-effect and names horizon `short`, and a hub-written note becomes a console notice. Never fall back to writing it long-term (#1189).
51
53
 
52
54
  - Keep candidate production in Tasks: screen review/correction payloads and current task/turn privacy before CandidateStore writes; recheck after asynchronous screening. Never make candidate failures roll back a saved review, or let a peer accept a candidate. Keep candidate tool arguments/results out of capture.
53
55
 
@@ -56,7 +58,7 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
56
58
  - Resolve any id that may be an extra instance or a configured kind through the startup-bound project catalog: `this.d.adapterCatalog` with `this.d.instances?.()` in Tasks, `opts.adapterCatalog` in `assign()` and `relayCandidates`, and the `adapterCatalog` hook in `Conductor`/`conductorStart`; a bare `definitionOf(id)` answers builtins only. Only a kind's routing preference expands to its instances; manual ids, reservations and nominations stay exact. Reviewer independence compares families: take the own family only when no other is eligible, record it through `assignmentNote` (`sameFamilyFallback=true`), explicit nominations included; persist the `instance`/`family` refusal codes as `policy`.
57
59
  - Tool publication (`projectToolList`, `projectConductorTools`, `configuredToolContext` in `src/hub/hub-tools.ts`) only narrows the list or adds start targets, using authenticated status metadata (`configuredAdapters`, `startablePeers`, `readOnlyPeers`); in a configured project, malformed or incomplete metadata publishes the read-only set and adds no start target. Publication is never authority: `taskOp` refuses read-only mutations independently, and MCP task and conductor refusals keep their text and set `isError: true`.
58
60
  - `hub_set_title` (`SESSION_TITLE_TOOL`, #1055) is in `TASK_TOOL_NAMES` but never in `TASK_TOOLS`; its publication rule is in `adapters.md`, and its `taskOpBody` case accepts only `title`.
59
- - Plan sources (#1144): `propose`, `acceptReviewed` and `accept` call `validatePlanSources` on the plan they will keep (the new one, else the stored one) before any board write or notice; an id the source registry (`TasksDeps.intake.sourceKnown`) does not know is a `PreEffectToolRefusal` naming `ahub source list`. A new path that admits a plan calls it too.
61
+ - Plan sources (#1144): `propose`, `acceptReviewed`, `accept` and `splitBacklog` (every part's cleaned plan, planner split and the hub's intake split alike, #1161) call `validatePlanSources` on the plan they will keep (the new one, else the stored one) before any board write or notice; an id the source registry (`TasksDeps.intake.sourceKnown`) does not know is a `PreEffectToolRefusal` naming `ahub source list`. A new path that admits a plan calls it too.
60
62
  - SDLC metrics and bands (#481 S7): `sdlc-metrics.ts` is pure (history in, numbers out, no clock, no I/O); every metric a task cannot derive is `null`/`unknown`, never 0, and a private task is only counted as withheld. Stale-head re-reviews need both heads on a round (`reviewCost.head` on the done, `reviewFindings.head` on the verdict): never derive them from event order. `Tasks.sdlcMetrics` is the one read (planner or person, refused during a private turn) and `Tasks.proposeFromBands` the one writer. It is held like every host writer (`sweepHeld`, `bus.isRecoveryHeld`, `bus.storageError`), keys a breach by its episode (`BandResult.episode`, derived from history so a restart finds the same key), writes nothing while the metric has an open band card, and records a log notice in `band_notices` so it is announced once. Card text is fixed words, numbers and task ids, screened through `signalsFor` and `candidateTextAllowed` like the recurring-finding card; never put task titles, notes or PR text in it, and never open a GitHub issue. Drift honours `min_delta` like every tier. `[bands]` is validated by `bandsConfig` with field-level paths; do not add a tier or a metric without a test that fires it. Merge-time figures (#442) come from `MergeRecord.cost`, written once by `guardedMerge` after it observes the merge (`mergeCostFrom`, `cleanMergeCost` in `cost-governance.ts`): re-validate a stored record with `cleanMergeCost` before use, never let that read touch the merge result, and keep a missing or partial read `null`, never 0. `hotfix_rate` and `security_open_alerts` are external series (`EXTERNAL_METRICS`, `Tasks.externalSeries`) whose points are releases and scans, so a card names its `subject`; a legacy release with no train history is left out of the series, never a 0. The security scan (`security-baseline.ts`, `Tasks.runSecurityScan`) reads GitHub's own open-alert counts, runs no scanner, schedules nothing without `[security_scan]`, is held like every host writer, re-judges its schedule after the awaited read and records counts only; a source it cannot read is an unknown total, never zero.
61
63
  - Admit a declared extra (a person-started `ahub claude --as` instance and a headless Claude implementer included, #742, #747) as owner of any class whose owner role is implementer (`ownerRoleOfClass`) only with the current implementer role and `ownerIsolation[id] === true`, the daemon's proof that the live adapter generation started in `executionRoots[id]` outside the canonical cwd. Never derive it from the declaration's `worktree`, its roles or a reload. Only the status path passes a display `ownerIsolation` projection (`stalled(projection)`); every admission and effect caller uses fresh `TasksDeps.ownerIsolation`.
62
64
 
@@ -72,6 +74,7 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
72
74
  - Owner review re-file (#1000, #1047): admit a replacement `hub_task_done` while `in_review` or approved and unmerged only for a different full lowercase SHA through the existing owner, privacy, lifecycle and completion-check gates. Fence the original review revision through every await; same-head, active-check and superseded submissions refuse before effects. Clear stale artifact-head evidence without minting reviewer consent, retain current-head local evidence, open Important findings and rejection counts, and publish a fresh request. A configured check returns the task to `in_progress` until completion. Every verdict pins its initial review request revision across awaits, including off mode with no explicit head. On an approved unmerged task, allow only the reviewer/person to withdraw approval with `changes_requested`; retain Important findings and leave that state for the owner. Persist each verdict head in history and preserve a legacy receipt-only approval head before dropping stale evidence. Refuse merged/cancelled reopens and unknown linked-PR merge state; re-read merge state after other awaits before the reopening write. Require a reviewer after a re-file, including configured-check completion. Project approval binding through `task-approval.ts`; merge queue, decision, dispatch and cleanup must compare it with the latest reported head.
73
75
  - Stale heads on re-file (#1121): a re-filed `hub_task_done` after a rejection or re-accept must name the new full SHA in `refs.commit`; summary text is never head proof. A linked PR whose head moved, or whose head cannot be read (bounded head-only read over `SdlcGh`), refuses the re-file and an approval at the old head, naming both SHAs. Owner-submitted full refs outrank host-derived PR receipt heads; revalidate role, task and head after every async read.
74
76
  - Keep structured review findings in verdict history, bound to the reviewed head. Validate bounded reports and person-only accepted dispositions before effects; recheck authority, privacy and head identity after awaits. A supplied `head` that differs from the task's resolved head (`findingHead`) is refused pre-effect, and the resolved head is part of the identity rechecked after the screen (#657). Key findings by `id` alone, never by pass (`findingsForHead`): a nit or open reclassification never discharges an Important, only an explicit disposition does. Omitted IDs never settle findings; carry open Important across heads until explicitly dispositioned, while nits stay head-local. Use the shared valid-head resolver for projections. Project text through publicView, and deduplicate recurring public pass/id pairs into backlog proposals through durable `Review finding key:` markers (a pair recurs only across separate work: tasks sharing a PR, a branch in one repository or a supersession count once, `workUnits`, #736; a task counts only where a review that is not withheld recorded the finding's id, whatever its pass, as open or accepted, never where it is only recorded fixed, #328) read only from hub-proposed `plan` cards (first history entry by `hub`), without editing instruction files.
77
+ - Person review comments and anchored findings (#1056, #1181, #1192, #1205; `src/hub/review-comments.ts`, `src/hub/review-anchor.ts`, `src/hub/review-material.ts`, `Tasks.reviewComment*`): only the console person writes a comment (`reviewPerson`), and authorship comes only from the server-built `ActionMeta`. A send binds immutable anchors and bodies into one package, appends one count-only history entry and attempts one envelope; it is never retried automatically, and only an explicit Send with `packageId` recovers that saved package. Placement is computed at read and send and never persisted over the stored anchor. Dashboard snapshots never read comment material. PII comments stay board-local and count-only. Comment writes count in `reviewCommentsPending`, which recovery and shutdown wait for. A finding `anchor` must agree with its `path`. Bounds and the full contract are in `docs/design/task-board.md` "Anchored review packages (#1056)"; read it before changing these paths.
75
78
  - Recurring unmet criteria (#514a, `src/hub/learning.ts`): grouping is pure (rows in, groups out); `Tasks.proposeCriterionCandidates` is the only caller and runs beside `proposeRecurringFindings`. It files a backlog card with ids and counts and the `profile_overrides` fragment as text, never applies it, and counts only rows that `candidateTextAllowed` passes (no PII). Its dedupe is the `Criterion candidate key:` line; keep it first in the detail. `ahub learn` (#514b) is the person's verdict on those cards: `parseCandidateCard` reads only that first line (and the hub-authored `plan` card, and the title that must match it), the task count and the line are recomputed from the board (`learnEvidence`, `overridesDiff`), and the card closes through `Tasks.cancel` only after `settingsSet` wrote the one managed key `LEARN_CHECKLIST` (never before, never by a direct board write); every `Tasks.learn*` method refuses any `by` but the person, and a new learn op never joins `CONDUCTOR_TASK_OPS`. Rewording the card's key line or title changes the parser in the same change.
76
79
  - Plan conformance (#481 S2, `src/hub/plan-conformance.ts`): compare only literal repository-relative `plan.paths` entries (file or directory prefix, no glob) with complete PR file metadata; `completePlanInventory` requires a full head, `pathsComplete` and `typesComplete` (false on any rename or copy). Root, absolute-unverified or unsafe entries, a missing plan, a truncated inventory and a failed read are `unknown`, never covered; never read patches or the filesystem. An untouched planned path is advice; a changed path outside the plan is a departure, and `planConformanceBlockers` blocks enforce approval on `unknown` and on an unaccepted departure. Plan `proof` and `risks` (32 strings of 512 characters) cover no file. `Board` records `planRevision` only while `trackPlanRevisions` (mode not off), separate from backlog `planningRevision`.
77
80
  - Departure consent is written only by `Tasks.acceptPlanDeparture` for the person (`ahub sdlc accept-departure <task> <head> <reason...>`; no peer tool) on a currently known departure at the current PR head, never on `unknown`, and never for a PII task. It binds repo, PR, head, plan revision and fingerprint, the inventory, `planScopeIdentity` and this host's `departureGeneration`, so a restart or a change to any of them invalidates it (`validPlanDepartures`). Status and merge-queue reads never write the board: they only note observations and invalidations in memory (`observedPlans`, `invalidatedPlanDepartures`), which `planObservation()` attaches to the next owner or reviewer transition write. Off captures no observation or revision; a plan or refs change still invalidates an existing acceptance.
@@ -121,6 +124,7 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
121
124
  - Campaign storage (#930 T1, #596; spec "T1 implementation boundary"): every study write runs inside `studyWrite` (counted by `studiesPending`, which `recoveryReady` and the shutdown drain read; `stopStudies` fences new ones) and asks `requestCheck(by, () => this.studyHold())` again after every await and right before the board write. `studyApprove` screens and builds units outside any transaction, re-reads the latest hash and references, then writes once through `Board.activateCampaign`; never move a screen, source lookup or file read into that transaction. A replay creates nothing (it may only tighten privacy), a stable unit id keeps its task only for an identical definition (changed work takes a new id), and a private campaign never retains a public task. `research-campaign.ts` validates only inert plain data (plain prototype, no proxy, accessor or symbol key; a caller's `toJSON` never runs) before canonical hashing. A receipt carries ids, revision, status and counts; no raw contract or close reason enters parent history or memory.
122
125
  - Experiments (#932, #598, #599; spec "Experiment T1 implementation boundary" and "T2 comparison implementation boundary"): `hub_experiment_record` and `hub_experiment_show` are thin callers of `Tasks.experimentRecord` and `experimentShow`; the person alone approves through `study_approve_protocol`. Forward the complete record envelope and authenticated actor unchanged, keep record calls out of read-only publication, refuse all PII-turn contexts, and exclude their arguments/results from capture. Only the existing receipt kind admits a person relay; ordinary records keep the current in-progress unit-owner check. New forms need their own admission and recovery review. A writer is the current in-progress owner of a unit task in the protocol's current approved campaign revision (`Board.researchDispatchBinding`), rechecked after every screen, receipt read and hash. A host measurement is its own `research_attempt_observations` row, never a rewrite of a declared event, and no path upgrades a provenance level. Output-path privacy uses `matchesPolicy` with `failClosedOnDiscoveryLimit` (`experimentOutputPrivate`); keep that opt-in to experiments, never the default for other callers. Approval binds the exact protocol hash and evaluator hash, never carries to a changed hash, and a comparison never ranks.
123
126
  - Registered runner receipts (#1018): admit a new signed receipt only through `Tasks.experimentRecord`, with current task/source/campaign/privacy authority and fresh trusted registrations rechecked after screening. Keep declared, host-observed and runner-attested evidence separate; a deliverable protocol never derives a code match from a signed commit claim. Identical canonical receipt/signature replay returns stored proof without registration verification or writes, even after revocation; changed bytes or a new revoked/unknown-key receipt refuse. Validate the whole linked attestation event and absence of host observations on every stored receipt read, including paged views that omit that event. Store receipt/event and advance format 2 in the same transaction; keep new/unused databases at `HUB_DB_BASE_SCHEMA` (1). Preserve task, privacy and source fences and show current key status separately from as-of-signing provenance. No public recording surface runs or contacts a runner; keep relay arguments/results out of memory capture.
127
+ - Claim evidence links (#1169, `hub_evidence_record`, `EVIDENCE_RECORD_TOOL`, `Tasks.evidenceRecord`, `src/hub/research-evidence.ts`): published only for the `research` profile (in `TASK_TOOL_NAMES` and `IMPLEMENTER_HUB_TOOL_NAMES`, never in `TASK_TOOLS`), refused in a private turn or PII context, and kept out of memory capture. The writer is the local in-progress owner of a public research task. A link binds the current #901 `claimId`/`claimRevision` and a registered public source whose raw hash matches. It runs inside `studyWrite` and rechecks `requestCheck`, the task/claim/source context, public metadata and the revision sequence after the screen's await, right before `Board.recordEvidenceLink`. Revisions only append: the next number only, an identical replay returns the stored one, and changed content is refused. It records declared provenance only; never resolve a locator or derive a #901 verdict from it.
124
128
  - Study readback (#930 T3, #601): `ahub study draft|revise|approve|close` and the hub tools are thin callers of `Tasks.study*` and add no board write. The daemon's person ops are `study_draft|revise|approve|approve_protocol|close` (`taskOpBody`), refused on any non-console connection by an explicit `study_` check in the `task` control case before `Tasks` is asked; agents use `hub_study_draft` and `hub_study_revise` (configured planner through `planningAuthority`, never approve), `hub_study_show` (read-only, also in `READ_ONLY_HUB_TOOLS`) and `hub_study_handoff` (the bound in-progress owner). Pass the tool's arguments to `Tasks` whole: `studyFields` refuses an unknown field, and re-picking fields in the daemon would silently drop it (mutation-checked in `test/study-daemon.test.ts`). `STUDY_TOOLS` keeps the four tools' arguments and results out of memory capture. An implementer child gets `hub_study_show` and `hub_study_handoff` only. `Tasks.studyList` returns one page `{ campaigns, next? }` of receipts (ids, revision, status, counts), never question, scope or sources, at most 50 per call: the bound is the `LIMIT` in `Board.researchCampaignPage` (observed on its own SQL in `test/research-campaign-projection.test.ts`), `after` is the cursor, and a caller cannot raise the page size. `studyShow` blanks source ids for a private campaign and carries `budgetNamed`, so a named budget keeps every resource row with `-` for an absent limit: do not add text to either. `liveTestRefusal` refuses every `study` form except `show`, so an agent shell cannot delegate a study write to the person through `--live-test`; the contract file is read through one opened regular-file descriptor, at most 64 KiB plus one byte.
125
129
  - Folder triggers (#512c, `src/hub/folder-trigger.ts`, `Tasks.runFolderTriggers`): the folder pass shares `schedulesRunning` with the schedules pass and `ahub schedule run`, so it is serialized with them and counted by `schedulesPending`; keep it so, and never start it from a path that does not take the guard. A file is recorded in `folder_seen` only AFTER its task exists, and the pass stops at the first failure so order is kept; do not record first (a failure would lose the file) or continue past a failure. A task made for a file names the source id and never the file's name or path, because both can be private. A trigger's baseline (the files there when it starts, possibly over several polls) proposes nothing, and a refused folder never begins the trigger; do not make either propose. The scan must keep no file's bytes (it hashes in chunks under a file and byte budget and never cuts the listing); `registerScanned` re-reads the one file being registered and refuses it unless it still hashes to what the scan saw. Ask `isNew` for each file inside the loop, not only when building the list, and `observe` (never skip) every file whose content is already known, in the list and in the loop, so it is found by `known` next time and cannot starve a new file of the hash budget. `folder_triggers` stays in `MACHINE_LOCAL` and `NEVER_EDITABLE`.
126
130
  - A scheduled run is fenced twice (#512a): `proposeFromSchedules` checks the hold before it starts and passes `propose` a `fence` that `requestCheck` asks again right before the board write, because the PII screen and triage are awaited in between and `requestAuthority` only fences child generations. Any other host writer that awaits a screen before `Board.propose` needs the same fresh admission at the write. A recovery commit also waits for a pass in flight (`Tasks.schedulesPending`, read by `recoveryReady()` in the daemon): every host writer that can write across an await gets a pending count there, next to `checksPending` and `autoPullsPending`. A person's schedule command that changes what a run in flight should do (`pause`, `resume`, #335) must reach that run's fence too: `ScheduleStore.epoch` is bumped by `setPaused` and `runSchedule`'s fence compares it, so the acknowledged command wins over an admitted run; `ScheduleStore.ran` never moves the anchor back. Any manual entry into the same writer (`Tasks.runScheduleNow`) takes the same `schedulesRunning` guard and is registered with the shutdown drain (`manualSchedulePasses` in `startDaemon`), never called around it.
@@ -129,4 +133,4 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
129
133
 
130
134
  - Claim verdicts and fact_check provenance (#901, `Tasks.recordClaimVerdict`, `parseClaimVerdict`, `claimListId`): only the console user records a verdict (`ahub task claim`; an agent tool never does), the claim is named by its 1-based slot and its text is never typed, stored or printed, and every field is parsed closed. A verdict entry carries `list`, the `claimListId` of the plan's ordered claims, and counts only while that id equals the plan's current one, so a replaced or reordered list leaves the slot unknown. `claim verdict` is in `QUIET_EVENTS`. The hub stamps `checkKey` and `missingKeys` on its own check entries from the lookup that chose the command, never from output beyond the example check's exact last line. In `dogfood-profile.ts`, `factCheck.agreement` counts per label only the slots whose model verdict was entered and equals the person's: a slot without the model's verdict is unknown, one that differs counts for no label, and a person verdict alone is never an agreement. Every one of these rows is null, never zero, when its source is missing.
131
135
 
132
- - Cross-project handoff (#604, agent-hub#1030, `src/hub/handoff.ts`, `Tasks.handoff*`, `Board.handoff*`): the pure core (`parseHandoff`, `resolveTarget`, `checkTarget`, `handoffEventFor`, `followBackLine`) takes no clock, file or socket; the daemon's `handoffOut` is the one origin path (parse closed, `handoffOutAuthority`: the person, a configured planner or the conductor, refused in a private turn and for a `[pii]` own task; then registry, the target's own status descriptor, the target's reply) and a refusal at any step leaves no row in either board. The target's `Tasks.handoffIn` parses again (a control message is data from another process), sanitizes, refuses a PII pattern or an uncleared screen, asks the daemon's recovery fence before and after the screen's await, and proposes through `board.propose(HUB, ...)`: the origin peer is a record in `task_handoff_origin` and never an actor, a roster peer or a `PeerId` that `Tasks` admits. The spec `issue` is kept as a line of the task's detail. Once the task and its origin row are written nothing fails the handoff (a link, notice or assignment failure becomes the reply's `note`), so the origin always learns the task id. `handoffStatesFor` answers only for tasks whose recorded origin is the asking project, with the fields a live event carries. The inbound `task_handoff`, `task_handoff_event` and `task_handoff_states` control messages are console-role only (the daemon answers an agent connection with a reason, and an unknown one with the usual "does not know"). Follow-back is read from the history entry's event and the task's state (`handoffEventFor`: observed names `accepted`, `done` (in_review, or approved for a class with no reviewer), `approved`, `declined`, `cancelled`), carries only an event name, a PR number and a full SHA, and never throws after a board write; the origin's outbound row is idempotent and a start-up catch-up (`handoffCatchUp`) asks each running target for the open ones. The tables are additive and `task_handoff` is its own recovery label while either holds a row (`Board.hasHandoffState`). `HANDOFF_TOOLS` keep the arguments and results out of memory capture. Never add text from the target to the follow-back line.
136
+ - Cross-project handoff (#604, agent-hub#1030, `src/hub/handoff.ts`, `Tasks.handoff*`, `Board.handoff*`): the pure core (`parseHandoff`, `resolveTarget`, `checkTarget`, `handoffEventFor`, `followBackLine`) takes no clock, file or socket; the daemon's `handoffOut` is the one origin path (parse closed, `handoffOutAuthority`: the person, a configured planner or the conductor, refused in a private turn and for a `[pii]` own task; then registry, the target's own status descriptor, the target's reply) and a refusal at any step leaves no row in either board. The target's `Tasks.handoffIn` parses again (a control message is data from another process), sanitizes, refuses a PII pattern or an uncleared screen, asks the daemon's recovery fence before and after the screen's await, and proposes through `Board.proposeHandoff(HUB, ...)` (the handoff idempotency rule above): the origin peer is a record in `task_handoff_origin` and never an actor, a roster peer or a `PeerId` that `Tasks` admits. The spec `issue` is kept as a line of the task's detail. Once the task and its origin row are written nothing fails the handoff (a link, notice or assignment failure becomes the reply's `note`), so the origin always learns the task id. `handoffStatesFor` answers only for tasks whose recorded origin is the asking project, with the fields a live event carries. The inbound `task_handoff`, `task_handoff_event`, `task_handoff_states` and `task_handoff_by_key` control messages are console-role only (the daemon answers an agent connection with a reason, and an unknown one with the usual "does not know"). Follow-back is read from the history entry's event and the task's state (`handoffEventFor`: observed names `accepted`, `done` (in_review, or approved for a class with no reviewer), `approved`, `declined`, `cancelled`, and `changes_requested` and `reopened` (#1203 T2, sent only to an origin that advertises `task_handoff_seq`)), carries only an event name, a PR number, a full SHA and `seq` (the target task's flow revision; an event not newer than the stored `seq` changes nothing), and never throws after a board write; the origin's outbound row is idempotent, and the catch-up (`handoffCatchUp`) asks each running target at start, every 5 minutes and when its control descriptor changes, over every row `handoffCatchUpRow` keeps (open, or approved within `HANDOFF_APPROVED_GRACE_MS`); transport, queueing and key-retry rules are in `daemon.md`. The tables are additive and `task_handoff` is its own recovery label while either holds a row (`Board.hasHandoffState`). `HANDOFF_TOOLS` keep the arguments and results out of memory capture. Never add text from the target to the follow-back line. Duplicate handoffs (#1175): `duplicateIssueTasks` (pure, `src/hub/handoff.ts`) is the one finder and `Tasks.handoffIn` runs it after its last await, right before `board.proposeHandoff`, over the board rows with every `[pii]` row left out, so a refusal writes nothing and names only ids; a sender's `duplicate_ok` (bounded, kept as a detail line) skips it, and a handoff with no `issue` is never refused. Never move the check before the PII screen's await, never name a title in the refusal, and never offer or escalate on a task the check refused. The conductor's cancel of an unaccepted handed-off task is `Tasks.conductorHandoffCancel`, asked inside both `validate()` runs of `Tasks.cancel` (the same `configuredManualConductor` and roster check the unassign path uses), limited to `proposed` or `backlog` with `board.handoffOrigin` set and no `taskHasStarted` or `taskHasWorkEvidence`; the board write records the conductor as the actor, and the follow-back is unchanged.
@@ -3,7 +3,7 @@
3
3
  Scope: tests, fakes and the test gate. Read before adding or changing a test or fake under `test/` or `src/adapters/conformance/`, or editing `scripts/check.sh`, `scripts/hang-watch.sh`, `scripts/progress-tee.sh`, `scripts/seeded-check.ts`, `scripts/seeds.json`, `scripts/lib/ts-ast.ts`, `scripts/changed-tests.mjs`, `scripts/check-closing-refs.mjs`, `.github/workflows/closing-refs.yml`, `scripts/release-prepare.ts`, `scripts/release-tag.ts` or `scripts/lib/release-plan.ts`.
4
4
 
5
5
  - A fake's gated action that never sees its gate skips the action; never fall through to a late, unsynchronized action. Owned-group tests inject `AcpOwnedGroupPorts`; a test never signals a process group it did not spawn.
6
- - When extracting production closures into a VM fixture, bind every referenced host port and actual imported policy helper. Keep native identity checks on real unstarted instances; never replace a policy predicate with an always-true stub or weaken existing assertions to accommodate a moved dependency.
6
+ - When a production closure gains a dependency, find its VM fixtures by the closure and its callers as well as the new dependency name. Bind every transitive host port and actual imported policy helper, including asynchronous read paths, and run each affected extracted closure. Keep native identity checks on real unstarted instances; never replace a policy predicate with an always-true stub or weaken existing assertions to accommodate a moved dependency.
7
7
  - On Bun 1.4.2, preserve original fields before `toMatchObject` with asymmetric matchers, or assert primitive fields and strings separately; never use a matcher-mutated value for a later content/canary assertion. Recheck this restriction when upgrading Bun.
8
8
  - A daemon fixture that routes write-class work (`implement`, `bulk_edit`) to a hub-sandboxed peer (Pi, `local`) runs in a throwaway repository made with `gitProject()` (`test/fakes/git-project.ts`), never in `ROOT`: the agent-hub checkout carries the machine's git config (a hosted runner's checkout has an `includeIf` credentials entry the hub refuses), so such a test passes on one machine and fails on another, and it would make a hub worktree and `task/*` branch inside the checkout under test (#1129). Before calling a hosted-only failure a flake, reproduce the runner's condition in a scratch clone.
9
9
  - A fixture that walks a policy table (a `COMPAT` entry list, a registry) derives its per-entry values from the table, never a fixed list of keys or versions a planned entry is not in: the compat watch test builds every served version and the ahead/unknown row from `COMPAT[id]`, so an added entry (`pi`, #843 AC2) is covered without editing the test.
@@ -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.
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`).
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).
@@ -20,7 +20,7 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
20
20
  - A fixture that needs a port takes it from a free-port probe, never a fixed number, which collides with whatever holds it on a shared runner (#548). The probe binds `127.0.0.1`, matching the loopback server that takes the port (#232).
21
21
  - `ahub upgrade` without `--to` asks the registry (`npm view`) and hands a target newer than the CLI to `bun x` (#272): a test that reaches either puts a fake `npm` first on `PATH` and a recording stand-in for `process.execPath`, as `test/upgrade-cli.test.ts` does. The interactive screens are tested through a scripted `ScreenIO`; a CLI test that needs them marks `process.stdin` and `process.stdout` as terminals in a wrapper script.
22
22
  - Cross-process fixtures wait for the consuming client to process its handshake (the channel connected diagnostic), not merely the daemon's attached/idle state. Deadline-routing fixtures hold and release responses at the fallback decision; do not place a fixed sleep between deadline fractions and assume scheduler headroom (#287). Receipt-order fixtures use the normal watchdog for ordinary I/O, trigger expiry after observed native acceptance, and await a consumed transport barrier before asserting on late results.
23
- - A bounded polling wait must assert its condition after the last attempt, or document why giving up is intentional. Append-only event histories wait for a correlated closing event, not removal of the opening event. Before changing a slow wait, identify the unreached state and repair its cause (#296).
23
+ - A bounded polling wait must assert its condition after the last attempt, or document why giving up is intentional. Bound it by time (a deadline with short sleeps), never by a count of event-loop turns (`setImmediate`), which a loaded runner spends before a socket frame or child output arrives (#1163, `untilSeen` in `test/control-client.test.ts`). Append-only event histories wait for a correlated closing event, not removal of the opening event. Before changing a slow wait, identify the unreached state and repair its cause (#296).
24
24
  - A test of turn or notice behavior that is not about the snapshot budget yields an I/O turn (`await new Promise(r => setImmediate(r))`) between captures: captures in one synchronous burst share the 250 ms budget and may correctly record unknown evidence. A daemon test whose assertion needs a turn's snapshot or end evidence passes `snapshotClock: () => 0` (`DaemonOptions.snapshotClock`, #661) so a slow runner cannot spend that budget; never lengthen the budget instead (#550). A wait on concurrent-edit evidence correlates the expected turn ids and paths (#617).
25
25
  - Holding the snapshot budget clock (`snapshotClock`, #661) holds the deadline and, since #554, the git child's wall-clock timeout (the 10 s ceiling instead of the remaining budget). The rule lives in the pure `childTimeoutMs` (`src/hub/snapshots.ts`: production keeps min(10000, remaining), only a budget carrying `now` gets the ceiling). Reproduce a slow git with a deliberately slow `git` first on PATH (`slowGit` in `test/snapshots.test.ts`), never with host-wide load, and never run a high-parallel loop while another gate shares the dgx02 host.
26
26
  - Both OS CI legs use four shards (#548): `check (ubuntu-latest) shard i/4` and `check (macos-latest) shard i/4` runs `AHUB_CHECK_SHARD=i/4 scripts/check.sh` (`bun test --shard --timings=scripts/test-timings.json`), and each stable job name `check (ubuntu-latest)` / `check (macos-latest)` is the aggregator over its OS shards; `scripts/ci-gates.mjs`, branch protection and the reuse proof read that name, so never rename it or turn it back into a matrix job without reading them. `gateJob()` in `scripts/ci-gates.mjs` counts the shard names (`check (ubuntu-latest) shard i/N` and `check (macos-latest) shard i/N`) as gate jobs, so a run with either OS and the seeded shards done and only the other OS shards running reads as a launched full gate and not as unknown scope (`test/ci-reuse.test.ts` pins every mid-run window). A new test file needs no timing entry (it gets the median), but a shard that grows past about 15 minutes means the timings are stale: regenerate them with `scripts/make-test-timings.ts`. A shard starts the suite cold, so `scripts/check.sh` execs the system git once (`AHUB_CHECK_WARM_GIT`, default `/usr/bin/git`) before the tests, because tests that give it `PATH=/usr/bin:/bin` and a 1000 ms timeout (`read-only-shell-proof`, `task-shell-default`) are killed by a cold first exec; a failing or absent warm-up never fails the shard. `check.sh` unsets `AHUB_CHECK_SHARD` after building the test arguments, so the runner and every scratch `check.sh` a test spawns (`test/check-lock.test.ts` copies `process.env`) see an unsharded environment; keep that unset. A test must not depend on running in the same process as another file (shards split files across machines), and anything that has to happen once per gate belongs behind the shard-1 condition in `scripts/check.sh`, which `test/macos-shard.test.ts` pins.
@@ -45,15 +45,17 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
45
45
  - A daemon test of a tool that reaches a public host passes the host-only transport seam (`DaemonOptions.webFetchTransport`, #553) and asserts what the daemon handed the tool, never a real round trip or a longer wait. The tool, the proxy URL and the allowlist stay the daemon's own; only the request is replaced. The proxy's own behaviour has its tests in `test/proxy.test.ts` and the web-proxy block of `test/web-fetch.test.ts`.
46
46
  - The preload leaves `HOME` unchanged and clears inherited `CLAUDE_CONFIG_DIR` and `CODEX_HOME`; tests that need fixture selectors set them after preload. A test that spawns `ahub setup`, `ahub autonomy` or `ahub init` under machine defaults, or reaches `discoverClaudeHomes` or the profile hook, sets `HOME`, `CLAUDE_CONFIG_DIR`, `AGENTHUB_HOME` and `AMEM_HOME` to temp directories, as `test/autonomy-machine-cli.test.ts` does; unit tests pass `{ home, env }` instead of `process.env`. A test that runs `ahub doctor` through the CLI also sets `CODEX_HOME` to a temp directory and puts a fake `amem` first on PATH, because doctor runs `amem setup <tool> --json` (#1025, `test/doctor-memory-capture.test.ts`).
47
47
  - Raising the `@staix/agent-mem` exact pin moves its version literal in `test/memory.test.ts`, `test/memory-profile.test.ts`, `test/memory-profile-daemon.test.ts` and `test/doctor-model-aliases.test.ts` in the same PR; that is the only test edit a pin raise makes.
48
+ - `test/fixtures/release-n-1` and `test/fixtures/protocol-v16` are separately exact-locked published packages (own `bun.lock`, installed by `scripts/check.sh`), never root dependencies, and `PUBLISHED_FIXTURES` in `test/published-protocol-compat.test.ts` pins their versions. Retarget release N-1 only in a minor release PR together with the version bump (AGENTS.md Releasing step 1, procedure in `docs/operations.md` #1164); the guard requires the fixture minor to be the package minor minus one. protocol-v16 stays at its version while protocol 16 is promised (#1168).
48
49
  - Keep CI's Bun version sourced from `.bun-version` through the pinned setup-bun action; update the version file rather than adding per-job version literals.
49
50
  - Release verification requires both OS checks and the aggregate `seeded guards` success for an identical tree, a tag matching `package.json` on `origin/main`, and fresh ancestor-bound documentation stamps. Preserve the separate npm OIDC publish job and GitHub Release write job; neither job gets the other's credential authority.
50
51
  - `test/conformance/` runs real adapters against fake transports. The shared kit and the transport fakes live in `src/adapters/conformance/` (public `@staix/agent-hub/conformance`); `test/conformance/kit.ts` and the matching `test/fakes/*.ts` only re-export them, so change the packaged file, and `--changed` runs everything for it; `test/conformance/enrollment.ts` enrolls every `ADAPTERS` definition, each `:reviewer` variant and each `:implementer` (`headlessVariant`) variant. Record a case that does not apply in its `UNSUPPORTED` or `UNSUPPORTED_LEGS` map with a concrete reason naming the dedicated coverage, never `test.skip` or synthetic success, and never change native production behavior to satisfy a generic case.
51
52
 
52
53
  - Keep the CI admission lock around the whole controller workflow, not just its decision job, in its per-PR (`check-pr-<number>`, `queue: single`: a newer pending run replaces the older) or main (`check-main`, `queue: max`) group, never with `cancel-in-progress: true` beside `queue: max` (GitHub refuses it) (#609); two groups admitting one tree at once is an accepted duplicate run, never a reason to fail closed on another tree. Preserve successful identical-tree full proof and required job names; a skipped job, pending controller or unavailable active inventory is never a successful full gate. In `readFullGateAdmission`, lost successful proof (404, malformed proof, a per-read timeout) only means a fresh gate runs; unknown active coverage and the global read ceilings (`GateReadLimitError`) stay unknown and refuse. A GitHub 429 or 5xx on one read is retried at most twice after a short backoff (#872), only while the shared deadline leaves room for the backoff, and every retry counts against the 240-read budget; never extend retries to any other status, a timeout or malformed data. CI checks out the PR head (`pull_request.head.sha`), not a merge with main: after a fix lands on main, merge main into the branch before reading CI as proof. CI reuse tests use fixed metadata fixtures and never dispatch real duplicate workflows.
53
54
 
54
- - Headless-reviewer tests run `test/fakes/fake-claude-print.ts` and headless Claude implementer tests run `test/fakes/fake-claude-implementer.ts` through `command` or a PATH stand-in (the implementer with `HOME` and `CLAUDE_CONFIG_DIR` in a temp dir), and fresh Codex reviewer tests run `test/fakes/fake-codex-reviewer.ts` through `codex_bin`; a test that reaches the daemon's reviewer or implementer start must never run the real `claude` or `codex`. The fake's sandbox markers test host admission only, never native isolation. Fresh Kimi reviewer tests set and restore `KIMI_CODE_HOME` to a fixture native home (the backend links that home's `credentials/`, `oauth/` and `device_id`, else the person's `~/.kimi-code`) and run `test/fakes/fake-kimi-reviewer.ts` through `command` or a PATH stand-in, never the real `kimi`. Qwen TUI tests run `test/fakes/qwen-dual.ts` (scripted by submit text) as the launched Qwen or the daemon's `qwen_cmd` (an adapter unit test also passes its own `table`), never the real `qwen`. agy tests put a fake `agy` first on PATH and set `HOME` to a temp dir before any transcript readback (`readAgyTranscript` reads `$HOME/.gemini`). Code-facts tests build `new CodeFacts({ env })` with a PATH whose first `ripwire` is a wrapper around `test/fakes/fake-ripwire.ts` (`FAKE_RIPWIRE_MODE` selects its failures), or inject Tasks' `prepareReviewBrief`; never rely on an installed ripwire, which CI does not have.
55
+ - Headless-reviewer tests run `test/fakes/fake-claude-print.ts` and headless Claude implementer tests run `test/fakes/fake-claude-implementer.ts` through `command` or a PATH stand-in (the implementer with `HOME` and `CLAUDE_CONFIG_DIR` in a temp dir), and fresh Codex reviewer tests run `test/fakes/fake-codex-reviewer.ts` through `codex_bin`; a test that reaches the daemon's reviewer or implementer start must never run the real `claude` or `codex`. The fake's sandbox markers test host admission only, never native isolation. Fresh Kimi reviewer tests set and restore `KIMI_CODE_HOME` to a fixture native home (the backend links that home's `credentials/`, `oauth/` and `device_id`, else the person's `~/.kimi-code`) and run `test/fakes/fake-kimi-reviewer.ts` through `command` or a PATH stand-in, never the real `kimi`. Qwen TUI tests run `test/fakes/qwen-dual.ts` (scripted by submit text) as the launched Qwen or the daemon's `qwen_cmd` (an adapter unit test also passes its own `table`), never the real `qwen`. agy tests put a fake `agy` first on PATH and set `HOME` to a temp dir before any transcript readback (`readAgyTranscript` reads `$HOME/.gemini`). Code-facts tests build `new CodeFacts({ env })` with a PATH whose first `ripwire` is a wrapper around `test/fakes/fake-ripwire.ts` (`FAKE_RIPWIRE_MODE` selects its failures) or an inline fake installed with `installFakeRipwire` (`test/helpers/ripwire-probe.ts`, #1197), or inject Tasks' `prepareReviewBrief`; never rely on an installed ripwire, which CI does not have. `installFakeRipwire` runs the shim once outside the measured region, and `withProbe` and the probe's `watch` append where each call spent its time when a test fails; read that before calling a fake-ripwire failure a flake, and never widen the 5 s per-call cap or the 20 s timeout for it.
55
56
  - A Pi fake the daemon starts through `createPi` answers `--version` with a version `COMPAT.pi` verifies, as `src/adapters/conformance/fakes/pi-rpc.ts` and `test/fakes/pi-data-policy.ts` do: the factory's preflight (`piVersionAsync`, #847) refuses the start otherwise.
56
57
  - A daemon test that declares a headless Claude implementer in a repository whose `origin` is on GitHub passes `shellDefaultBranchProbe` (`DaemonOptions`, #845), or the start runs `git ls-remote` against github.com; a test of `readTaskShellDefaultBranch` itself puts a fake `git` first on the `PATH` it passes.
57
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.
58
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.
59
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.
@@ -49,6 +49,40 @@ Optional ports have explicit meaning: absent keeps the generic path, while prese
49
49
 
50
50
  Adapters translate different interaction surfaces without changing the peer's task identity. Claude's plugin channel connects through MCP and the hub control service; Codex is proxied through app-server; ACP peers share an ACP transport; Pi uses its extension bridge. Kimi defaults to a pull-only TUI, with headless ACP as a separate selected form under the same peer id. The TUI does not inherit headless push, session, or recovery behavior. Keeping forms distinct lets the hub preserve the native user interaction model while enforcing one role and delivery policy ([#316](https://github.com/STAIxBWLB/agent-hub/issues/316), [#625](https://github.com/STAIxBWLB/agent-hub/issues/625)).
51
51
 
52
+ ## Channel attachment evidence (#1171)
53
+
54
+ The Claude plugin's inherited launcher marker is insufficient to enable push. In the inspected Claude Code 2.1.318 client path, MCP initialization advertises no usable channel opt-in; the plugin's server capability is not client evidence. The fallback checks the direct parent's bounded process row for the existing launcher's exact leading development-channel flag and supported target. Unknown, malformed, truncated or changed-parent evidence, print/background forms and repeated flags keep the plugin tools-only, with messages waiting for `hub_inbox`. The check does not walk ancestors or treat the marker as an authorization token.
55
+
56
+ The flattened process row is a conservative launch observation, not authentication or proof of registration or model consumption. Unsupported invocation layouts remain pull-only. An attached peer's status reports its mode and a closed evidence label without command text. Evidence belongs to that socket's attachment and is omitted when no current attachment exists. Older clients that omit the optional evidence field retain their prior behavior and report `unreported`; no control protocol version changes. The separate delivery-settlement contract handles an accepted notification that remains unconfirmed. The [investigation report](../verification/2026-11-04-1171-channel-client-evidence.md) distinguishes static client inspection, passive process observation and synthetic fixtures.
57
+
58
+ ## Person takeover of a channel peer (#1174)
59
+
60
+ An opted-in channel process may retain a live, non-owning standby connection when
61
+ a current session holds the peer. The adapter keeps a bounded standby registry;
62
+ status exposes only its server-generated connection id, reported PID when present,
63
+ and arrival time. These values are display metadata, never native session ids,
64
+ launch/isolation evidence or caller authority. Every standby peer operation is
65
+ refused centrally until that exact socket owns the attachment.
66
+
67
+ The person's `peer_takeover` control request, used by `ahub peer takeover <peer>`
68
+ and the console, selects an explicit live standby id or the oldest eligible one.
69
+ The host checks person authority, current membership and lifecycle fences before
70
+ detaching the old ownership; the selected claim/preface reuses normal admission
71
+ and generation checks. A `requested` result means attachment is pending, not
72
+ completed. Socket detachment never stops the native process, grants no delivery
73
+ receipt and replays no work. Unconfirmed deliveries keep ordinary settlement.
74
+
75
+ The old plugin reports a fixed person-detachment cause and stops reconnecting.
76
+ A legacy plugin receives an already-terminal refusal so it cannot undo the
77
+ handover, but may report its older generic admission message until upgraded.
78
+ Older daemons retain their existing close-and-retry standby behavior. An ordinary
79
+ owner departure may release an eligible standby through normal claim admission;
80
+ a standby never evicts a live owner without the person's takeover operation.
81
+
82
+ ## Accepted channel delivery settlement (#1172)
83
+
84
+ Only an attached push channel opts into explicit disposition of its own accepted delivery. An `accepted` receipt acknowledges that the notification was accepted by the channel transport; it is not a native handling or task-completion signal. The host reads `bus.accepted_settle_ms` from accepted configuration on its existing maintenance sweep, defaulting to ten minutes. A row that remains unconfirmed becomes `needs_review` with no replay. `hub_inbox` returns bounded id/revision metadata for the channel's accepted and held rows, including when an accepted push prevents the pull itself. The plugin renders that metadata and avoids both an empty-inbox claim and polling advice while there is an actionable row. Important, task and review timeouts produce one durable status-kind, status-priority conductor message containing ids only when the existing `conductor.feed` policy is on; `feed: off` remains an opt-out. Pull-only sessions do not gain the explicit accepted-disposition port.
85
+
52
86
  ## Permissions, execution, and process ownership
53
87
 
54
88
  Permission modes control whether native actions prompt; they do not widen a sandbox or path grant. The local worker's file tools use path guards, and its command tools run through the host's OS sandbox. Vendor-native tools use their vendor/runtime permission and isolation boundaries; the hub does not claim to confine every ACP or TUI tool. This distinction prevents an approval-mode setting from being mistaken for filesystem or network confinement ([#242](https://github.com/STAIxBWLB/agent-hub/issues/242), [#39](https://github.com/STAIxBWLB/agent-hub/issues/39), [#270](https://github.com/STAIxBWLB/agent-hub/issues/270)).
@@ -71,6 +105,10 @@ Native CLI compatibility is centralized in `compat.ts`. Each gated operation ask
71
105
 
72
106
  External adapter modules are an explicit, person-owned, machine-local opt-in. The daemon verifies and pins the captured bundle at startup, then admits only the bounded headless ACP instance contract through host-managed launch and current-owner checks. Module code runs in the daemon trust domain; pinning is integrity evidence, not a sandbox. Modules cannot supply native reviewer engines, proof ports, permission callbacks, provider selectors, or arbitrary recovery authority. This ceiling limits what imported code can claim and prevents live reload from replacing the executable catalog ([#754](https://github.com/STAIxBWLB/agent-hub/issues/754)).
73
107
 
108
+ `questionWaitMs` (#1053) is the time a `hub_ask` call waits for the person before it answers `pending`. It is a fixed-builtin field like `title`: `assertNativeCapabilityScope` refuses it on a configured or external definition, and the person overrides it per kind with `questions.wait_ms.<kind>`, never the caller. MCP peers carry 45 s, Pi and the local worker 300 s, provisional until the live timeout measurements of #1053 T1.
109
+
110
+ `settle` (#1054) is the code-verified signal that ends a peer's logical run, so the hub can create a completion for a run a person started. It is a fixed-builtin field like `questionWaitMs`: `assertNativeCapabilityScope` refuses it on a configured or external definition. **Owner decision 2026-11-04 (issue #1054):** completions exist only for adapters whose signal the T1 audit verified in code. Claude's TUI (`native-stop`: the transcript-verified Stop, never a lost channel or watchdog) and headless implementer, Codex (`turn-completed`), Pi (`agent-settled`, its own event: the `agent_end` plus retry/compaction fallback is not implemented), the local worker (`route-outcome`), ACP Kimi and Jcode headless (`prompt-response`) and the Qwen TUI (`message-stop`) declare one. **Absent means no completions:** Kimi in its own TUI (pull-only, no signal), agy (its declared `native-hook` signal is not verified end to end) and phase-2 person-typed Codex turns declare none, and the daemon starts no run for a peer attached pull-only. agy joins after a live receipt verifies its Stop readback and completion row; Codex person-typed turns after an origin receipt. The settle rule, outcomes and restart reconstruction live in `src/hub/completions.ts` and `src/hub/completions-store.ts`; a run settles only when the adapter signal fired AND no queued continuation, open permission or question, owned child, running check or held delivery remains.
111
+
74
112
  ## Session title slots
75
113
 
76
114
  Only fixed Pi and local definitions opt into `title: "hub"` (#1055). Their
@@ -130,3 +168,17 @@ and private stubs still apply to authorized label viewers.
130
168
  ## Evidence boundary
131
169
 
132
170
  Compatibility probes and fake transports verify source contracts, not an account login, live model turn, native permission prompt, or operating-system isolation. Those receipts remain separate because protocol inventory alone cannot prove runtime behavior ([#625](https://github.com/STAIxBWLB/agent-hub/issues/625), [#754](https://github.com/STAIxBWLB/agent-hub/issues/754)).
171
+
172
+ ## Owned print input admission (#1056)
173
+
174
+ A trusted owned adapter can accept an envelope before it has chosen the actual model-input session. Reviewer print children choose a fresh isolated context per review; the Claude implementer selects a persisted task-session UUID and may replace it after verified missing-transcript evidence. Their host-only input guard runs with that final target identity before prompt data can reach the backend or native child. It persists quote-bearing provenance before exposure. A failed guard releases no prompt and retains the adapter's owned teardown rules.
175
+
176
+ Owned observer-context IDs identify the history the host controls. They do not populate `nativeSession` or grant budget, resume, permission or task authority; an unrelated title or socket identity is not a substitute. Local's observer identity belongs to its in-memory history. Retained external native sessions keep their confirmed native identity, including across a launch change. Persistent implementer input keeps the same observer identity when its task session is resumed.
177
+
178
+ A successful stdin handout may confirm ordinary-input replacement for the matching context and input token. This is metadata about handing over bytes, not native handling, task completion or a new transport ACK. Old callbacks cannot release a newer input hold. The bus journal keeps its original transport receipts separately from this actual native-input observation. A fresh reviewer context may discard its own observer receipt only after its process group is confirmed gone, its task MCP bridge is closed and launch cleanup succeeds. The host retains the turn exclusion through remaining diagnostics. Persistent implementer and retained native histories do not acquire that disposal rule.
179
+
180
+ When a quote-bearing review reaches a peer without a stable native session ID, the host commits a peer-scoped incomplete marker before handoff and keeps the observer exclusion in daemon memory. A failed marker write refuses the handoff. An ordinary input within the review's active turn is staged and cannot release that turn's final outputs; the host clears only the peer marker at the next turn boundary while that ordinary input remains current. An ordinary input in a later turn, or while no turn is active, clears only that peer marker after verified handoff when the peer has no earlier SID-specific hold or incomplete record. If any earlier positive hold or SID-specific incomplete marker exists and the current SID is unknown, ordinary input cannot release the hold; status asks the person to reconnect so the native session ID can be confirmed. A confirmed native session change clears only the peer marker, then the host checks the exact-session receipt. If the current daemon has RAM evidence of a quote handed out while unbound, it first stores a positive receipt under the newly confirmed SID. Other SID receipts remain intact. The daemon keeps the quote's admission-turn fence in RAM separately from the active turn id, which adapters may rebind. No launch, attachment, socket or runtime UUID is persisted as a substitute session ID. Positive held receipts for a known pull-only session remain sticky until a confirmed session change. Positive held receipts are expected state, not a health failure. Status reports incomplete or corrupt provenance with a bounded cause and next step; repair guidance preserves delivery and task state.
181
+
182
+ ## Local review turns (#1056)
183
+
184
+ The local worker binds review observer exclusion to its actual input envelopes before awaiting model or tool work. An anchored `hub_review` submission latches that exclusion for the rest of the batch/turn, including sibling and later tool calls. While latched, `hub_remember` and `hub_task_propose` are refused before reaching the host task tools; the caller can retry them in a later ordinary turn. Such turns do not call capture, commit review messages into later model history, or put provider error text and partial model text in failure logs. A later ordinary local turn resumes ordinary history and capture without inheriting the review payload. This is distinct from the PII policy: a public review package retains ordinary task authority and does not acquire an on-campus-only model route. Vendor-native histories remain owned by their adapters; the hub's current-turn and named-parent observer provenance is described in [the bus contract](bus.md#review-observer-provenance-1056).
@@ -44,6 +44,27 @@ Authority is role-scoped: the person retains decisions requiring judgment or con
44
44
 
45
45
  Native tools keep their vendor permission and execution boundaries. The local worker applies path and permission checks to its tools, and runs command execution through a host-supported sandbox; the hub does not sandbox vendor-native tools or claim all ACP commands are isolated. This avoids confusing a hub permission mode with execution confinement ([#39](https://github.com/STAIxBWLB/agent-hub/issues/39), [#270](https://github.com/STAIxBWLB/agent-hub/issues/270)).
46
46
 
47
+ Channel takeover keeps the same separation: optional adapter-owned takeover and
48
+ socket-ownership ports hold transport state, while the daemon supplies person
49
+ admission and current roster/launch/generation fences. A live standby has no peer
50
+ tool or delivery authority. Its public connection id is not a native conversation
51
+ id and cannot release observer provenance or establish execution isolation. A
52
+ person takeover detaches ownership without terminating the process, and normal
53
+ delivery uncertainty remains independent ([#1174](https://github.com/STAIxBWLB/agent-hub/issues/1174)).
54
+
55
+ ## Console presentation surfaces
56
+
57
+ The floating pet belongs to acon Mini (#1012). The compatibility command
58
+ `ahub pet --window [--close]` prints the `acon ui` pointer and succeeds for
59
+ person, agent and live-test identities, without starting a host or window.
60
+ The retired `ahub pet --palette <value>` validates its argument and prints the
61
+ same pointer without writing configuration. Legacy `pet.palette` remains
62
+ validated compatibility input with no rendering effect.
63
+ Legacy `pet.window` input is ignored with one notice and cannot change authority
64
+ or re-enable an inherited disabled terminal pet by itself. Terminal pet frames
65
+ remain local TUI presentation. The acon plugin's existing **Console message**
66
+ view owns the composer through its established `send` action (#935).
67
+
47
68
  ## Board, assignment, and delivery
48
69
 
49
70
  `Tasks` is the task state machine and policy boundary; `Board` is its SQLite persistence. Proposals, assignments, acceptance, completion, and review flow through `Tasks`, which checks roles, state, privacy, and revisions before writes. Pure candidate selection keeps route explanations aligned with actual assignment. This prevents adapters or CLIs bypassing invariants with direct board writes ([#424](https://github.com/STAIxBWLB/agent-hub/issues/424), [#481](https://github.com/STAIxBWLB/agent-hub/issues/481)).
@@ -52,12 +73,31 @@ The message bus is separate from task state. `Bus` frames and routes envelopes;
52
73
 
53
74
  Encrypted rooms are an optional message transport between project hubs. They do not federate task boards, roles, approvals, or memory; each receiving project makes its own local admission decision. This prevents a remote room membership from becoming local authority ([#446](https://github.com/STAIxBWLB/agent-hub/issues/446)).
54
75
 
76
+ Room applications add bounded at-least-once sending with receiver admission
77
+ deduplicated by immutable application id under the existing retention and cap
78
+ contract. Retry and restart retain the original queue
79
+ TTL and revalidate current membership and privacy. ACK deadlines trigger resend;
80
+ late signed recipient receipts may settle unresolved ordinary remote holds but
81
+ never a person-disposed row. Invocation uncertainty keeps its person-only ledger hold. Receipt ciphertext is locally durable before chat transport
82
+ ACK, and remains in its outbox until broker storage confirmation. These room-only
83
+ transitions do not grant native completion or task-offer read evidence ([#1204](https://github.com/STAIxBWLB/agent-hub/issues/1204)).
84
+
85
+ Claim evidence (#931 T1) uses the same Tasks/Board boundary and #901's canonical
86
+ claim identity and ordered-list revision. Evidence links are append-only
87
+ provenance proposals; they create no separate claim/verdict store and do not
88
+ establish entailment or synthesis readiness. The research-only record tool
89
+ returns an ids-only receipt and excludes its arguments/results from capture.
90
+ Its T1 authorization, revision and recovery boundaries are defined in
91
+ [the task-board contract](task-board.md#claim-evidence-records-931-t1).
92
+
55
93
  ## Models and memory
56
94
 
57
95
  Peer selection, hub-owned model routing, and provider access are distinct layers. Task routing chooses a peer; the in-process route selects a model for hub-owned inference; OmniRoute owns provider keys, provider fallback, and usage. Native CLIs retain their own account and provider configuration; subscription OAuth credentials are not pooled through the gateway. The former Switchyard sidecar is retired; routing now runs in process. This keeps provider credentials and fallback policy in one gateway while avoiding a second routing service ([#553](https://github.com/STAIxBWLB/agent-hub/issues/553)).
58
96
 
59
97
  The hub's portable memory storage, sync, indexing, and project helpers come from the pinned first-party `@staix/agent-mem` package. `src/memory/` and `Tasks` provide hub-specific capture, recall, privacy screening, task briefs, and shared notes. Runtime memory is in-process: there is no claude-mem worker HTTP client or model summarizer. claude-mem is an off-by-default, import-only source. This removes a second capture path while keeping caller authority and task privacy in the hub ([#752](https://github.com/STAIxBWLB/agent-hub/issues/752), [agent-mem#13](https://github.com/STAIxBWLB/agent-mem/issues/13)).
60
98
 
99
+ Long-term hub writes and core promotion callbacks use the configured standalone sync policy plus hub narrowing. Profile readiness is independent of the current daemon's last automatic sync result. Public status exposes safe cause/count/action metadata; person-only declined inspection exposes IDs and rule categories, with explicit incomplete state when the portable core cannot supply a bounded physical view. Portable row placement and physical sync inspection remain agent-mem responsibilities.
100
+
61
101
  ## Privacy and recovery
62
102
 
63
103
  Session titles for the two hub-owned title slots (#1055) are conditional writes
@@ -77,3 +117,9 @@ Privacy decisions are made at hub boundaries, not delegated to model routing. A
77
117
  Recovery is a coordinated daemon operation over separately persisted board, delivery, and native-session evidence. A restart or upgrade can continue only with compatible feature evidence from the verified target and host-held proofs that still match the project and actor. Adapters report native evidence; they do not certify their own recovery authority. This avoids treating a stale status file or an unverified receipt as proof that a native process is safe to resume ([#37](https://github.com/STAIxBWLB/agent-hub/issues/37), [#625](https://github.com/STAIxBWLB/agent-hub/issues/625), [#900](https://github.com/STAIxBWLB/agent-hub/issues/900)).
78
118
 
79
119
  The architecture does not claim that a fixture proves a real provider account, native session, campus network, or operating-system sandbox. Those are distinct runtime evidence and remain separate from source-level and automated checks ([#625](https://github.com/STAIxBWLB/agent-hub/issues/625), [#754](https://github.com/STAIxBWLB/agent-hub/issues/754)).
120
+
121
+ ## Person review packages (#1056)
122
+
123
+ The task-board module owns anchored findings and person-authored comment lifecycles. Transport carries one explicit review package only after `Tasks` admits the selected draft set; drafting and editing do not start peer turns. acon supplies the shared review kit and a server-verified person identity, while the standalone dashboard reads counts and bounded placements only. Neither surface writes the Board directly.
124
+
125
+ The host-only review package reference is an observer exclusion, not PII authority. Durable open comment/finding scope protects memory notes naming that task after restart or intervening input. Capture, progress and session-title exclusion follows the current review input, not unrelated board history. ID-only current-input holds survive daemon restart. An intact, readable, complete store with no matching held record admits a new session as clean. Unreadable or corrupt storage and unconfirmed coverage for the current identity remain excluded. A validated release receipt confirms that identity without declaring other legacy identities clean. For an identified pull-only session, a positive held row remains sticky across restart until confirmed native identity changes. Its own incomplete marker excludes until verified ordinary replacement or a confirmed new session clears that marker; legacy coverage alone does not create a sticky hold. Missing native identity permits handout under an in-RAM unknown-held scope. A never-identified peer can release it with verified ordinary replacement in that daemon run; earlier positive or incomplete SID rows instead keep unidentified observer state held until a real SID is confirmed. First confirmation persists any current RAM quote under that SID before clearing the peer marker, without discarding other SID rows. It does not invent a native identity or weaken write-failure refusal. Legacy coverage completes from settled journal evidence, after which NULL clean receipts can be pruned; corrupt state remains excluded and is named in status with a repair next step. The hold grants no authority; the task-bound recipient summary and check output remain distinct from observer projections. Explicit publication recovery uses the saved immutable envelope and retained journal evidence, never automatic effect replay. Its intended recipient may read a public package, but memory capture, operational views, delivery logging and incoming-message labels exclude its body. Local-worker capture latches exclusion before any tool in a batch that submits an anchored verdict, including a refused verdict, and applies it to subsequent capture, final summaries and retained history. A delayed response cannot become capturable when another turn starts. Recovery advertises the conditional `review_comments` feature when retained comment/package records require it. It also requires `review_observer_input_state` when held or unknown session-input provenance could otherwise be lost. See [the task-board contract](task-board.md#anchored-review-packages-1056) for bounds, immutable snapshot placement and lifecycle authority.
@@ -33,6 +33,11 @@ The bus applies limits to a sender, sender-recipient pair, important messages, a
33
33
 
34
34
  The in-memory bus schedules messages; `DeliveryJournal` durably records an individual transport attempt and the original envelopes behind any rendered output. A peer receipt such as `accepted` or `completed` describes transport, not the corresponding task's acceptance, completion, or approval. That separation lets a timeout or restart remain explicitly uncertain without inventing a task outcome ([#37](https://github.com/STAIxBWLB/agent-hub/issues/37)).
35
35
 
36
+ A closed bus returns an empty publication outcome before provenance, privacy,
37
+ journal preflight, observers or queue mutation. Deferred publication has the same
38
+ fence. A storage failure while the bus is open still fails rather than becoming
39
+ an apparently successful empty send.
40
+
36
41
  For #1055, `Bus.onDelivered(peer, originals, deliveryId?)` exposes the existing
37
42
  journal delivery ID to the observer at the first positive transport ACK. The
38
43
  optional argument accompanies the original envelopes, including when the
@@ -41,16 +46,149 @@ and an observer exception cannot alter delivery settlement. This supports the
41
46
  [ID-only session-title restart binding](adapters.md#session-title-slots) without
42
47
  changing dispatch, retry, task acceptance or task-completion authority.
43
48
 
49
+ A successful nonempty inbox handout records current-input and bounded named-parent review provenance through `Bus.onPulled(peer, originals)`. This metadata hook does not invoke `onDelivered`, certify native consumption, start a session-title receipt or change completion authority. Presence-only handouts do not release review exclusion, and a mixed quote-bearing batch cannot seed a title binding from its ordinary member. A previously saved binding to an ordinary member of such a batch is also rejected on status and delivery recovery; a fresh ordinary input can establish a safe source. It follows the existing pull handout ceiling: the transport result may still be lost before the client reads it. The separate metadata-only `onSteering(peer, originals)` hook latches quote-bearing input immediately before an actual steer attempt, without creating a native ACK or task/completion evidence. Ordinary additional steer input cannot clear an active review turn's exclusion.
50
+
51
+ The latest pulled input is kept separately from the last pushed delivery. Observer reads use the pull slot while it is current, but invocation answer matching retains its pushed request through an intervening inbox read; an explicit reply can still name the correct pushed or pulled invocation. A later push replaces the pull slot. Only `resetLastDelivery` after a confirmed native-session change clears both slots; adapter metadata reset/removal preserves the latest pulled input.
52
+
44
53
  Push peers receive bounded batches. Condensation is optional, excludes important, remote, and invocation messages, and keeps `originals` separate from `out` so replies, safe failures, and recovery still refer to what the peer was actually given. On an eligible safe retry, a failed batch falls back to individual envelopes, and a previously failed envelope goes alone so a poison item cannot block its neighbors. These bounds preserve queue progress when summarization or one recipient fails ([#713](https://github.com/STAIxBWLB/agent-hub/issues/713), [#553](https://github.com/STAIxBWLB/agent-hub/issues/553)).
45
54
 
46
55
  Pull-only peers catch up oldest-first under a byte budget, but the first envelope always goes even when it exceeds that budget so one oversized item cannot starve the queue; push peers use the digest batch limit. Pull records returned envelopes as completed at handout because the tool result is the readback, not a later native receipt. A busy Claude channel can also pull its own messages through `hub_inbox`; that avoids making a long-running turn an unbounded queue lock ([#493](https://github.com/STAIxBWLB/agent-hub/issues/493), [#713](https://github.com/STAIxBWLB/agent-hub/issues/713)).
47
56
 
48
57
  Delivery uncertainty normally holds the original and is never an automatic replay. The narrow exception is a hub-authored presence or workflow notice proven moot by current task evidence; mixed, unknown, or requested-stop rows remain held. This removes stale notices without treating arbitrary peer messages as seen ([#434](https://github.com/STAIxBWLB/agent-hub/issues/434)). Safe pre-acceptance failures may retry within a bounded attempt policy; uncertain results remain `needs_review` until an explicit resolution. A peer may retry only its own eligible row and only once per immutable retry lineage; an invocation uses its own ledger and is not replayed as an ordinary message retry ([#37](https://github.com/STAIxBWLB/agent-hub/issues/37), [#991](https://github.com/STAIxBWLB/agent-hub/issues/991)).
49
58
 
50
- A peer may settle its own local `needs_review` row as completed or discard while busy, with the observed revision and a public-safe reason; this records a delivery decision and never changes task state or redelivers the original. Another peer, a remote-room actor, or an active `dispatching`/`accepted` row remains refused. Invocation-bearing rows also remain person-settled because a transport action cannot assert the outcome of an invoked operation. `hub_inbox` may pass a `needs_review` hold to return newer queued messages and bounded id/revision metadata naming the held row, while leaving the uncertain originals held. Pause, recovery, lifecycle, storage, and active-delivery fences still apply. This lets the current recipient make progress without treating uncertain work as delivered ([#1079](https://github.com/STAIxBWLB/agent-hub/issues/1079), [#445](https://github.com/STAIxBWLB/agent-hub/issues/445), [#991](https://github.com/STAIxBWLB/agent-hub/issues/991)).
59
+ Failure diagnostics use the bounded, public-safe `failureCause` classes `peer
60
+ offline`, `adapter refused`, `push error`, `turn aborted`, and `timeout`, never
61
+ message text. A legacy absent cause remains unknown. Queue inspection exposes
62
+ this metadata without changing the receipt's state or proving that retry is
63
+ safe. Status counts only unresolved failed rows per peer, excluding rows with
64
+ an explicit resolution even when a retry leaves its parent in `failed`; the
65
+ metadata-only aggregate does not hydrate delivery payloads ([#1178](https://github.com/STAIxBWLB/agent-hub/issues/1178)).
66
+
67
+ A peer may settle its own local `needs_review` row as completed or discard while busy, with the observed revision and a public-safe reason; this records a delivery decision and never changes task state or redelivers the original. Another peer or a remote-room actor remains refused. Active `dispatching` rows remain refused; an `accepted` row is settleable only by its explicit push channel owner as described below. Invocation-bearing rows also remain person-settled because a transport action cannot assert the outcome of an invoked operation. `hub_inbox` may pass a `needs_review` hold to return newer queued messages and bounded id/revision metadata naming the held row, while leaving the uncertain originals held. Pause, recovery, lifecycle, storage, and active-delivery fences still apply. This lets the current recipient make progress without treating uncertain work as delivered ([#1079](https://github.com/STAIxBWLB/agent-hub/issues/1079), [#445](https://github.com/STAIxBWLB/agent-hub/issues/445), [#991](https://github.com/STAIxBWLB/agent-hub/issues/991)).
68
+
69
+ An explicit push channel may settle or discard its own `accepted` row with the observed revision and a public-safe reason. `accepted` means the channel transport accepted the notification, not that the session handled it. If neither a native completion nor an explicit disposition arrives within the configured `bus.accepted_settle_ms` window, the host moves the row to `needs_review` with a fixed timeout cause and never replays it. The channel's `hub_inbox` metadata includes its own accepted and held rows, with ids and revisions, even while an active accepted delivery blocks pulling. An important, task, or review timeout adds one ids-only status notice when a conductor is configured and its existing `conductor.feed` policy is on; `feed: off` remains an explicit opt-out. Timed-out rows stay held until a person or the explicit channel session records a disposition; task state does not settle them ([#1172](https://github.com/STAIxBWLB/agent-hub/issues/1172)).
70
+
71
+ An `answer` envelope (#1053) is a person's late answer to a question a peer asked with `hub_ask`. The hub builds it: `kind: "answer"`, `from: user`, addressed to the asking peer only, with the deterministic id `answer-<set>` (a republish after a crash is dropped by the bus's own dedupe) and `refs.question`. Code keys on `kind`, never on the sender, and an `answer` never supersedes the question it answers. Its body is the person's text, so the log line, the console tail and the dashboard feed show a fixed stub; only the recipient reads it. A question expiry is a hub `status` notice, never an answer.
51
72
 
52
73
  Invocation requests and results are single-envelope deliveries, not digest material. A reply is correlated to the request handed to that peer and is captured by the invocation path before ordinary publishing; uncertain outcomes stay with the invocation ledger because a missing result cannot be inferred from a transport receipt ([#445](https://github.com/STAIxBWLB/agent-hub/issues/445), [#991](https://github.com/STAIxBWLB/agent-hub/issues/991)).
53
74
 
54
75
  ## Cross-project transport
55
76
 
56
77
  Encrypted rooms add a screened message transport between local project hubs. Room identity, deadlines, and remote provenance stay bound to each delivery; traffic is not merged into a shared board. A separately pinned read-only invocation relay cannot transfer task or approval authority. This keeps remote delivery from acquiring the receiving project's roles or memory ([#941](https://github.com/STAIxBWLB/agent-hub/issues/941), [#992](https://github.com/STAIxBWLB/agent-hub/issues/992)).
78
+
79
+ ## Review observer provenance (#1056)
80
+
81
+ An admitted person comment package or anchored-finding review request carries the host-only opaque `refs.reviewPackage`. The intended recipient keeps its public body; observer projections show an IDs-only stub. A PII task still uses its existing private stub and authority rules. Peer-supplied review markers are discarded. The bus derives `refs.reviewResponse` for a response to the current delivered review input or a retained named parent, so a direct echo cannot enter logs, stream projections or memory observers as ordinary chat. This marker grants no task, console or PII capability.
82
+
83
+ The provenance cache retains at most 128 delivered parent IDs per peer and a globally bounded response cache; peer removal drops its parent binding. It holds identifiers only, not excerpts. This boundary attributes current turns and named parents. Arbitrary paraphrases from later vendor-native session history need a native session/turn provenance protocol; content matching is not used to guess them.
84
+
85
+ Person comment packages retain their terminal delivery evidence for explicit idempotent publication recovery. Reviewer requests use the distinct `reviewer-request-` marker and their terminal rows remain prunable. A saved person package whose latest delivery failed or was discarded refuses explicit resend with that state and queue inspection guidance; it never reports queued success or replays the effect. Owner-addressed review packages and verdicts stay uncertain until an explicit disposition; task or reviewer movement alone is not consumption proof. Durable open sent comments and anchored finding scope protect memory notes naming that task after a restart or intervening chat; they grant no task or PII authority. Capture and other turn-wide observers follow the current delivered review input or an anchored-verdict submission latched to that input, not peer-wide board history. Ordinary new input releases that turn exclusion only where the transport supplies replacement evidence; the restored pull-only rule below still applies. Separate delivery holds retain their own dispositions. The journal retains bounded peer/session hold and release receipts using session/input ID hashes, never text. Completeness and integrity metadata confirm whether absence is meaningful: no matching held record in an intact, readable, complete store is clean, including a new session after restart. Unreadable or corrupt storage remains unknown and excluded. A validated matching release confirms only its own session while legacy coverage is incomplete.
86
+
87
+ A missing stable native session id does not refuse a review handout. The host records a peer-scoped incomplete marker and keeps an unknown-held scope in RAM, without turning launch, attach or socket ids into a native identity. For a never-identified peer with no positive or incomplete SID row, the next verified ordinary replacement in that daemon run can release that RAM scope and its peer marker. If any earlier positive or incomplete SID row exists while the current SID cannot be matched, delivery still proceeds but the observer state stays unknown and held; an ordinary RAM proof cannot release it. Status names that peer as unidentified and directs reconnection so its real session id can be known. A newly confirmed SID uses its own state; another SID's retained rows are not cleared. Journal write failure still refuses before handout. Presence-only traffic and additional input within a known active held turn cannot clear exclusion.
88
+
89
+ Legacy coverage becomes complete only when journal evidence shows no unsettled pre-upgrade review-scoped delivery remains; this changes provenance metadata, never delivery disposition or replay. Its bounded candidate set is fixed from preexisting handed work at open/import; guarded deliveries created after that capture do not prolong the current daemon's fence. Reopen/import while coverage is incomplete conservatively recaptures every handed non-terminal row present then, including a newer guarded B; after the original A settles, coverage may therefore remain incomplete and a fresh SID without its own receipt remains unknown until B also reaches a terminal disposition. A timeout to needs_review still requires explicit resolution. This accepted recapture ceiling neither refuses handout nor creates a sticky hold from legacy coverage alone. If this window matters in operation, the upgrade path is a persisted ids-only legacy fence carried across recovery. Legacy coverage alone does not create a sticky restored pull-only hold. For an identified session, a positive held row or that peer's own incomplete marker can restore exclusion. A verified ordinary replacement or confirmed native-session change clears only that peer's incomplete marker; a session change does not discard another session's positive held row. On the first confirmed SID after a sessionless period, any current RAM quote hold is persisted under that SID before the peer marker is cleared. Positive session-bound restored holds remain excluded until confirmed native identity changes. Sessionless peers do not acquire an unbound sticky hold.
90
+
91
+ Once coverage is complete, NULL clean-session receipts may be pruned because complete absence preserves clean state. Positive and uncertain holds are not evicted to satisfy the 4096-row bound. Integrity metadata and counts are updated atomically with these changes. Public status names unavailable provenance and its next step without exposing note text or raw database errors. There is no automatic corrupt-state reset. Recovery preserves the receipts, integrity metadata and session-bound exclusion.
92
+
93
+ Role reassignment cannot clear the original recipient's observer exclusion while a review envelope remains queued, recovery-deferred or unsettled. The journal supplies recipient/task-scoped metadata independently of current task roles; explicit disposition ends that delivery scope. Sent person comments independently retain their saved package recipient until resolution or person dismissal.
94
+
95
+ ## Room presence and broker backpressure (#1204 T1)
96
+
97
+ Ordinary observations reuse an already scheduled coalesced republish instead of
98
+ cancelling it and sealing a new frame on every awaited state flip. Responses and
99
+ forced refresh requests retain priority. A chat flushes earlier deferred presence
100
+ and reserves its own serial slot before yielding, preserving concurrent chat and
101
+ durable-outbox FIFO. Receipts retain their existing durable-commit/ACK path and do
102
+ not wait behind an additional ordinary observation. Actual-send exposure,
103
+ recipient, currency and capacity fences remain unchanged.
104
+
105
+ Presence carries a signed optional `ephemeral: true` frame header. The broker can
106
+ read this metadata but cannot decrypt the presence body. It retains only the
107
+ latest ephemeral frame per sender for currently connected, ready recipients;
108
+ disconnect and broker restart remove ephemeral recipients. Durable frames keep
109
+ the existing queue caps, TTL and exactly-once receiver admission.
110
+
111
+ A link skips an unchanged local presence snapshot, except explicit peer refresh
112
+ requests/responses and a fresh connection. One sealed frame is sent at a time
113
+ until its broker storage response, so a refused sequence cannot be overtaken.
114
+ A correlated `queue_full` retains the frame (applications remain in the local
115
+ outbox) and retries after 250 ms, doubling to a 30 s ceiling. Stop/refresh clears
116
+ connection timers; no recipient receipt or application completion is inferred.
117
+ T1 does not change acknowledgement deadlines, restart replay or receipt outbox
118
+ persistence, which belong to T2. Existing layer caps remain authoritative.
119
+
120
+ ### Room wire compatibility (#1204 T1)
121
+
122
+ The link requests only the ephemeral feature with the WebSocket upgrade header
123
+ `x-agent-hub-ephemeral: 1`. A supporting broker includes `ephemeral: true` in its
124
+ ready response only to an opted-in client, preserving the exact legacy ready
125
+ shape for older hubs. Each new connection starts with the feature disabled.
126
+ Without the advertisement, presence retains the legacy frame shape; dedupe and
127
+ queue_full backoff still apply, but that broker retains presence for offline
128
+ members under its old queue rules. Upgrade the broker for ephemeral retention.
129
+
130
+ A supporting broker forwards ephemeral presence only to ready members that
131
+ opted in. Older members do not see upgraded members' presence until they upgrade. Application
132
+ chat frames keep the legacy shape. This is the one-bit compatibility bridge;
133
+ full codec feature advertisement belongs to #1204 T4.
134
+
135
+ Ephemeral replacement preserves a recipient's outstanding one-packet transport
136
+ slot until its matching ACK or the original TTL/grace bound expires. Only the
137
+ pending replacement coalesces; a newer observation never refreshes the handed
138
+ packet's deadline. Unsupported recipients never receive the new frame shape.
139
+
140
+ A refresh request
141
+ remains on non-reply snapshots until all asked members supply validated presence;
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.
145
+
146
+ ## Room application retry and settlement (#1204 T2)
147
+
148
+ Room sends retain one immutable application id and original queue expiry across
149
+ fresh ciphertext attempts. A distinct root delivery may reuse a retained application
150
+ only with the same canonical content hash and peer/recipient tuple, no person
151
+ disposition or permanent hold, and the original creation/expiry times. Legacy
152
+ NULL content hashes refuse cross-root reuse. Receiver admission keeps the existing application-id
153
+ dedupe horizon (30 days / 10,000 identities with settled-first eviction).
154
+ This is exactly once within dedupe ceiling. Automatic resend can reach that
155
+ ceiling after a settled identity is evicted, permitting duplicate admission under
156
+ a fresh ciphertext sequence. Pinning retryable identities or #737 receipt
157
+ confirmation is the upgrade path.
158
+ A typed refusal before durable enqueue requeues the original without uncertainty
159
+ or poison retry accounting. Online readiness and the ACK deadline trigger retries;
160
+ the ACK deadline is not a terminal disposition. Queue TTL or a permanent refusal
161
+ ends automatic sending. An ordinary remote chat uncertainty row does not block later queued work.
162
+ Invocation uncertainty retains its person-only ledger hold and is never automatically
163
+ replayed or settled from late transport evidence. Active invocation receipts update
164
+ only transport metadata, never native execution or task-offer read evidence.
165
+
166
+ Each retry rechecks current room/member identity, exposure and privacy against the
167
+ saved immutable application. A restart reconstructs active remote deliveries under
168
+ their existing ids rather than treating transport loss as native consumption. A
169
+ late authenticated recipient receipt can settle an unresolved remote hold once,
170
+ with an ids-only event. Person dispositions, stale adapters and changed membership
171
+ remain authoritative; native delivery and task-offer read evidence are unaffected.
172
+
173
+ Roster, pause, lifecycle and global recovery fences also apply to resends. A
174
+ readiness loss after durable enqueue retains that delivery's id and original TTL;
175
+ confirmed precommit absence can requeue. A restored active journal row with no
176
+ matching row in an intact RoomStore becomes `needs_review` for inspection and is
177
+ never replayed from missing evidence. A failed payload-proof read never schedules
178
+ an immediate retry: transient failures wait for an online or ACK-deadline wake,
179
+ and permanently unavailable ciphertext is held for review with a metadata-only
180
+ transition. Metadata settlement and quarantine remain available while sending is held.
181
+ Authoritative Bus person dispositions are reconciled into RoomStore before
182
+ startup replay and incoming receipt processing. A local transport result may
183
+ remain as evidence beside that disposition, but cannot override it or restart
184
+ sending. Status reads remain read-only.
185
+
186
+ A completed inbound admission commits its signed receipt ciphertext to the local
187
+ RoomStore outbox before transport ACK of the chat. Pending receipts replay on
188
+ reconnect until broker storage confirmation. Waiting for broker storage before
189
+ chat ACK would deadlock at capacity one. The durable outbox removes T1's RAM-loss
190
+ exception; broker storage and transport ACK still do not prove native execution.
191
+ ACK after durable local commit of either the receipt or the inbox entry.
192
+ Deferred application admission may ACK its durable crypto inbox entry so it does
193
+ not pin broker capacity; later admission commits and replays the same durable
194
+ receipt path before retiring its local work.