@staix/agent-hub 0.21.0 → 0.22.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 (39) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +2 -2
  3. package/docs/agent-notes/bus.md +1 -1
  4. package/docs/agent-notes/daemon.md +2 -2
  5. package/docs/agent-notes/tasks.md +1 -1
  6. package/docs/agent-notes/tests.md +1 -1
  7. package/docs/dogfood.md +3 -1
  8. package/docs/operations.md +45 -0
  9. package/docs/profiles.md +1 -1
  10. package/docs/quickstart.md +5 -1
  11. package/docs/smoke.md +6 -0
  12. package/docs/specs/2026-09-19-agent-hub-design.md +31 -0
  13. package/docs/verification/2026-10-30-0.22.0.md +49 -0
  14. package/docs/verification/2026-10-30-445-capability-probes.md +172 -0
  15. package/docs/verification/2026-10-30-bus-delivery-operational-settings.md +77 -0
  16. package/docs/verification/2026-10-30-jcode-ac4.md +24 -0
  17. package/docs/verification/2026-10-30-jcode-ac6.md +52 -0
  18. package/docs/verification/2026-10-30-recovery-wait-settings.md +141 -0
  19. package/docs/verified.json +12 -12
  20. package/package.json +1 -1
  21. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  22. package/plugins/agent-hub/server.js +1 -1
  23. package/src/cli/help.ts +1 -0
  24. package/src/cli/identity.ts +3 -1
  25. package/src/cli/learn-output.ts +38 -0
  26. package/src/cli/main.ts +20 -2
  27. package/src/cli/recovery-waits.ts +26 -0
  28. package/src/cli/smoke-pty-bridge.ts +67 -18
  29. package/src/cli/smoke-pty.ts +10 -2
  30. package/src/cli/upgrade-runtime.ts +16 -14
  31. package/src/cli/upgrade.ts +2 -1
  32. package/src/hub/bus.ts +18 -10
  33. package/src/hub/config-apply.ts +2 -1
  34. package/src/hub/daemon.ts +61 -14
  35. package/src/hub/delivery-journal.ts +78 -57
  36. package/src/hub/learning.ts +33 -1
  37. package/src/hub/operational-settings.ts +79 -1
  38. package/src/hub/settings.ts +21 -1
  39. package/src/hub/tasks.ts +78 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@ Issue and pull request numbers in the entries for 0.7.7 and earlier refer to the
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.22.0
8
+
9
+ - Five bus and delivery limits are settings (#489 H3, task 444, #975): `bus.max_attempts` (1-100, default 3), `bus.digest_max` (1-10, default 10), `bus.pull_budget_bytes` (1-65536, default 65536), `bus.notes_kept` (1-10, default 10) and `bus.terminal_history_limit` (1-2048, default 2048), live from `config.local.json` at their next use. Unset behaves as before; only `bus.max_attempts` may rise above what shipped, and a reload never trims queues, resets attempts or prunes history.
10
+ - Five upgrade-recovery waits are settings (#489 H4, task 445, Refs #489): `recovery.reconnect_wait_s`, `recovery.unmanaged_reconnect_wait_s`, `recovery.control_timeout_s`, `recovery.shutdown_wait_s` and `recovery.terminal_idle_wait_s`. Unset behaves as before; every person-visible wait can be raised and never cut below what shipped, a bad or unreadable file is the default, and a wait already running keeps its deadline.
11
+ - `ahub learn list [--all] | accept <id> | reject <id> [--json]` (#514b, person only, refused for agent shells and the configured conductor): the verdict on the recurring-unmet-criterion cards #514a files. `accept` adds that card's one `review_checklist` line to your current checklist in `.agenthub/config.local.json` through the settings writer (this machine only, never a tracked file, `ahub settings undo` puts it back, applies after a restart) and closes the card; `reject` only closes it. The line and its task count are rebuilt from the card's key line and the board, never parsed from the card's text. The #514a card sentence "nothing in the hub writes it" is amended to say the hub writes it only on `accept` (earlier cards keep the old sentence). The settings event and undo reply for this key carry a line count, not your lines.
12
+ - The macOS PTY bridge no longer hangs in `Bun.spawn` under load, and no longer outlives its caller (#560, Refs #559). Bun up to 1.4.3 can block for good in `Bun.spawn({ terminal })` when the child writes and exits before the call returns (oven-sh/bun#45300, fixed in 1.4.4); the bridge now starts its child behind a bounded gate it opens after the call returns, and stops the child when its caller is gone. The test retry helper `runStarted` is removed.
13
+
7
14
  ## 0.21.0
8
15
 
9
16
  - Tasks and routing operational settings (#489 H2, task 441): six more numbers use the H1 mechanism (fixed rows in `src/hub/operational-settings.ts`, the closed `config-apply.ts` schema, the settings table), all live and with the default equal to the constant they replace: `tasks.demote_at` (1.5), `tasks.demotion_half_life_h` (24), `tasks.contradiction_window_days` (7), `tasks.rescreen_max` (10), `tasks.split_min_observations` (5) and `tasks.split_max_failure_share` (0.3). Each is read where its constant was used, so a change applies at the next demotion read, verdict, re-screen or prediction without a restart, and unset behaves exactly as before. The split thresholds only feed the shadow prediction, which never changes assignment. Nothing here is a safety floor or a contract. `docs/operations.md` lists the other 35 H0 operational items of `tasks.ts`, `routing.ts` and `task-sweep.ts` that stay as they are, and why.
package/README.md CHANGED
@@ -5,7 +5,7 @@ Code, Codex, Kimi Code, Pi and a hub-owned local worker (plus Qwen Code, Jcode a
5
5
  Antigravity if you want them) share a message bus, a task board and a memory, each
6
6
  connected through its own native interface.
7
7
 
8
- Status: 0.21.0, control protocol 16. Bun + TypeScript, one loopback daemon per
8
+ Status: 0.22.0, control protocol 16. Bun + TypeScript, one loopback daemon per
9
9
  project. Site: [agent-hub.staix.net](https://agent-hub.staix.net).
10
10
 
11
11
  ## Quick start
@@ -94,7 +94,7 @@ uses the home must run a version that understands the new layout.
94
94
  [#258](https://github.com/STAIxBWLB/agent-hub/issues/258)).
95
95
 
96
96
  Live checks that still need a real account or a person are listed in the
97
- [release verification report](docs/verification/2026-10-29-0.21.0.md) and the
97
+ [release verification report](docs/verification/2026-10-30-0.22.0.md) and the
98
98
  [smoke checklist](docs/smoke.md).
99
99
 
100
100
  ## Documentation
@@ -15,7 +15,7 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
15
15
  - Limits admit what is sent: the envelope `newEnvelope` builds (a reply inherits its parent's sender, `digest` resolves to the originals, `capPriority` applies), never the raw `to` and priority. Admit first and build later, and implicit replies all count as broadcasts.
16
16
  - A reply is addressed, never broadcast. An adapter that answers a delivery passes `to: replyAudience(envs)`; anything else with an `inReplyTo` inherits that envelope's sender. Leaving `to` out or empty fans the message out to every peer. The control `send` takes a nonempty string `to` as that one peer and refuses any other non-array shape or bad item before publishing (#963); never turn a malformed `to` into a broadcast.
17
17
  - `digest` is not a peer: addressing a reply at the envelopes the peer was handed sends it nowhere once a delivery was condensed. The bus resolves `digest` back through `lastDelivery.originals`; anything else that reads a reply's `to` has to do the same.
18
- - A pull-only peer (a Claude session without channel pushes, Kimi in its own TUI, or agy) is never drained by a push; `Bus.pull` hands it its queue oldest first up to `PULL_BUDGET_BYTES` (64 KiB of `frame` text, #713; `takeBudget`), the first item always going so one huge envelope cannot stall it, and a poison one still alone. A push peer, and an attached channel session that is not pull-only, keep `take`'s `DIGEST_MAX`: never raise that for a pull. The daemon's `inbox` reply carries `waiting` and the plugin says `N more waiting: call hub_inbox again before you start work`. An attached channel session may also pull its own queue through `hub_inbox` while busy (#493); the same holds and journal completion apply. Drain and pull share `held()` (recovery, an uncertain delivery, any pause, the lifecycle `deliveryHeld` fence) and `remember()` (so `reply_to` resolves), and a drain rechecks `deliverable()` after condensing because such a session can take the peer meanwhile. A pull is journaled `completed` in the same transaction as the bus snapshot, never `accepted`; a failed one puts back only that peer's queue and preface and never reloads the snapshot.
18
+ - A pull-only peer (a Claude session without channel pushes, Kimi in its own TUI, or agy) is never drained by a push; `Bus.pull` hands it its queue oldest first up to `bus.pull_budget_bytes` (#489 H3; unset is `PULL_BUDGET_BYTES`, 64 KiB of `frame` text, #713; `takeBudget`), the first item always going so one huge envelope cannot stall it, and a poison one still alone. A push peer, and an attached channel session that is not pull-only, keep `take`'s `bus.digest_max` (at most `DIGEST_MAX`): never raise that for a pull. The daemon's `inbox` reply carries `waiting` and the plugin says `N more waiting: call hub_inbox again before you start work`. An attached channel session may also pull its own queue through `hub_inbox` while busy (#493); the same holds and journal completion apply. Drain and pull share `held()` (recovery, an uncertain delivery, any pause, the lifecycle `deliveryHeld` fence) and `remember()` (so `reply_to` resolves), and a drain rechecks `deliverable()` after condensing because such a session can take the peer meanwhile. A pull is journaled `completed` in the same transaction as the bus snapshot, never `accepted`; a failed one puts back only that peer's queue and preface and never reloads the snapshot.
19
19
  - Record exact task-offer reads only through `DeliveryJournal.completeDelivered` on an active-fenced adapter completion, correlated reply or actual pull, atomically with completion. Capture original offers before terminal pruning and record their ids, never condensed ids; accepted receipts, generic transitions, operator/hub dispositions and restored completed rows are not read evidence.
20
20
  - The `onImportantQueued` nudge for a pull-only peer (#398) is advisory: it never dequeues, steers, journals or settles, and a throw from it never stops an enqueue.
21
21
  - Merge recall and restored prefaces through `mergePrefaces`/`uniqueMemoryLines`; preserve distinct context and new notes, deduplicate memory identities, and keep colliding short prefixes distinct. Use `recallDisplayText`/`recallMemoryLines` before display caps: suppress only matching automatic approval outcomes, retain nonempty rejection details, preserve id headers and complete-word excerpts, and keep token trimming at line boundaries. Keep these changes display-only.
@@ -63,7 +63,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
63
63
  - Judge every recorded owner pid (hub manifest, registry claim, recovery runner, manager owner, launcher record) with `processLiveness` in `src/pi/process-signature.ts`, never a bare `process.kill(pid, 0)`: after a reboot a recorded pid can belong to another process, even one we may not signal, and the bare probe wedged recovery and `ahub up` (#226). A writer records `processSignature(process.pid)` beside its pid; a registry claim stores it as `<instance id> <signature>` because an older daemon's claim rewrites the pid but not that column. Records without a signature keep the bare-pid answer; unknown is never gone.
64
64
  - A restored daemon accepts a changed native session id (Kimi and local aside: they restart as fresh workers) in two ways only: the #64 rule for a Claude session that never persisted a transcript, and the operation-fenced `recovery-waivers.json`, which the coordinator writes wherever it accepts a session other than the planned one: a reconnect-only Claude (#206), an operator-chosen fresh session, the plan's `freshStart` of a Codex thread the hub saw start with no turn, or a zero-turn Claude it accepted (#215). Never waive on any other evidence. Re-preparation after an expired lease keeps every effect receipt, and `restored:<peer>` = `failed` never counts as restored. Judge a restoration launcher by its `terminal-recovery.json` record and process signature (and a `terminal_handle_stale` answer), never by the terminal's exit; an unreadable record is unknown, never "none".
65
65
  - A target that stopped after it started is restarted only through `recordRestart` (`src/cli/upgrade.ts`), recorded before the new one starts and at most once per project per resume: each done or waiting `restored:<peer>` binding becomes `retired:<peer>`, whose terminal is closed (`closedRetired:<peer>`) or found absent before that peer is relaunched, and is never revalidated against the new instance. Keep repaired-session provenance across every target restart, disabling automatic fresh/zero-turn replacement. A person may explicitly accept loss of a repaired Codex/Claude conversation only on typed positive missing transcript/rollout evidence and a disposal-time recheck; refuse live-test repaired overrides after read-only receipt classification and before runner/lock or coordinator mutation, fail closed on unclassifiable scope, and repeat in core before lock and after under-claim read, preserving ordinary #408 delegation; record the one-use grant and freshOverride evidence, and consume the grant on exact attachment. Pi stays excluded: positively missing accepted Pi files get stop-and-archive plus the normal project-scoped ahub pi command; bookmark retirement never falls back fresh in the same start. Clear the repair fence only for a done or waiting binding retired from that target; an uncertain creation without a durable binding keeps its fence. Launcher evidence is read for every instance in `restarts`, and a session attached without an id is unknown, never gone (#225).
66
- - After a managed terminal is created, persist its binding before waiting up to `RECONNECT_WAIT_MS` for authenticated attachment to the expected target instance, operation and conversation (#414). Resume waits on the saved binding without creating another terminal. Count only attached, non-offline session ids; when the chosen terminal has no current launch record, a confirmed-ended row for another handle or an id-less attachment retains the bounded wait after older-launcher checks. A late current record must match the saved terminal; unknown or live replacement evidence still blocks. Timeout keeps the receipt blocked and records only terminal/hub metadata; a last native connect attempt that was never recorded is unknown, never inferred from terminal readiness or offline state. Final identity and preservation verification stays strict. `createTerminal` re-reads the launch record and Orca binding of an idle replacement every `MAP_POLL_MS` until it maps to the original peer and session, a known identity or session contradicts it, or the deadline passes; only then does it report `ambiguous-create`. Missing identity alone is never a mapping (#599).
66
+ - After a managed terminal is created, persist its binding before waiting up to `recovery.reconnect_wait_s` (#489 H4, read by `readRecoveryWaits` at the start of the wait; unset 600 s) for authenticated attachment to the expected target instance, operation and conversation (#414). Resume waits on the saved binding without creating another terminal. Count only attached, non-offline session ids; when the chosen terminal has no current launch record, a confirmed-ended row for another handle or an id-less attachment retains the bounded wait after older-launcher checks. A late current record must match the saved terminal; unknown or live replacement evidence still blocks. Timeout keeps the receipt blocked and records only terminal/hub metadata; a last native connect attempt that was never recorded is unknown, never inferred from terminal readiness or offline state. Final identity and preservation verification stays strict. `createTerminal` re-reads the launch record and Orca binding of an idle replacement every `MAP_POLL_MS` until it maps to the original peer and session, a known identity or session contradicts it, or the deadline passes; only then does it report `ambiguous-create`. Missing identity alone is never a mapping (#599).
67
67
  - An operation receipt stays `schema: 1`. Only `abandoned()` writes 2, on an operation with a recorded `disposition` whose coordinator predates #215: in the same write as the disposition, or, for a disposition 0.12.20 recorded at 1, by the next command that holds the runner claim. `validateReceipt` accepts 2 only with a disposition (#227). Never write 2 on an operation that can still be resumed. Disposal clears `error` and `failure`, so each `stop-and-archive` audit entry keeps the operation's `error` at that moment as `cause` (#776): only text the receipt already held, never task or message text.
68
68
  - Four checks read another release's source text for an exact line: `coordinatorCurrent` (`export async function disposeRecovery` in `src/cli/upgrade.ts`), `targetReadsWaivers` (`export function readRecoveryWaivers` in `src/hub/restart.ts`), `coordinatorRestarts` (`export function recordRestart` in `src/cli/upgrade.ts`) and `supportsAction` (`export async function reattachRecovery` in `src/cli/upgrade.ts`). Renaming, moving or un-exporting one makes this release read as an older one: keep those lines until a capability list replaces the checks. The reattach CLI dispatch must use the same `supportsAction` check as its printed command; never redirect a fallback command back to a coordinator without that action. Recovery also reads a target's literal `OPTIONAL_PEERS` (`src/hub/peer-catalog.ts`, through `legacyTargetSupport`) and `START_MODE_PEERS` (`src/hub/start-mode.ts`, through `legacyHeadlessArgv`), declared in `src/adapters/definitions/native-recovery.ts`; keep both literal declarations until a published capability replaces them.
69
69
  - Publish terminal ownership only after the current native owner is proven through `terminal_owner` (#482); capture and start acknowledgement are pending evidence only. Preserve unrelated and invalid rows, scope current evidence to project/instance/peer, and recheck the same native generation after asynchronous Orca reads. Keep Claude's hook record pre-spawn. Compatibility and retention ceilings are described in Operations, controlled recovery.
@@ -80,7 +80,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
80
80
  - A headless Claude implementer's tools socket (#525-4) is admitted only with the launch id of that peer's current child (`ownedTools`) and a verified worktree root, and bound as `toolsOwner`: every request and every reply rechecks `currentImplementerTools` and fails closed. Its `task` ops are limited to `IMPLEMENTER_HUB_TOOL_NAMES` and run inside `childToolAuthority`, so `Tasks.requestCheck` refuses before an effect once the child changed. Its `inbox` refuses, before `bus.pull`, a batch holding a private item or one attributed to another task. Its person `permission_request` path is `classed: true`, offers allow-once and deny only, and denies on expiry, abort or a changed child. Automatic Bash permission uses only the fixed host-wrapper contract in `adapters.md`, including the explicit client protocol capability; a raw native allow is never an automatic grant. Its facts get `permission` only for the current child's launch id, session id and this instance.
81
81
 
82
82
  - Keep SQLite recovery contention in the existing blocked receipt as retryable `database_busy`, retaining bounded throw-site frames and the ordinary `ok: false` rid reply. Use a neutral control error; the receipt's next actions name resume or the partway disposition. Clear a superseded failure on abort, disposal, fresh-session choice or a refused resume of a disposition. After a commit request, treat busy inspection as unconfirmed and keep polling only inside the existing shutdown bound.
83
- - Recovery inspection uses its existing 30 s control cap for the first status read and consumes a structured busy reply without a second status probe; ordinary lifecycle probes keep their default. Treat `busy_timeout` as a ceiling for eligible waits, not a guarantee that every write waits: a WAL snapshot conflict may fail immediately. Account for synchronous 3 s hub-database waits and serial timer callbacks when choosing a timeout; several waits may consume the 30 s control margin. Never multiply the busy wait through retries or infer stopped from a shorter probe's timeout.
83
+ - Recovery inspection uses the recovery control cap (`recovery.control_timeout_s`, 30 s unset, never lower) for the first status read and consumes a structured busy reply without a second status probe; ordinary lifecycle probes keep their default. Treat `busy_timeout` as a ceiling for eligible waits, not a guarantee that every write waits: a WAL snapshot conflict may fail immediately. Account for synchronous 3 s hub-database waits and serial timer callbacks when choosing a timeout; several waits may consume the 30 s control margin. Never multiply the busy wait through retries or infer stopped from a shorter probe's timeout.
84
84
  - Every `new Database(` under `src/` is followed at once by its own `PRAGMA busy_timeout` from a named `*_BUSY_TIMEOUT_MS` constant (`HUB_DB_BUSY_TIMEOUT_MS` in `src/hub/db-timeout.ts` for `hub.db`), and nothing sets `busy_timeout` again later: `test/db-busy-timeout.test.ts` reads the source and fails on any other opening idiom, so a new one extends that guard first (#303).
85
85
  - The manager forwards a dashboard `start_peer` with the terminal provider cap (`MAX_COMMAND_MS`) plus 5 s and reports a missed deadline as `unconfirmed`, never as a failure; it recognises the deadline by the exact text `ControlClient.request` resolves with, so change the two together. The hub's terminal reopen guard and the dashboard's retry hold share `PEER_START_RETRY_MS` (`src/hub/start-mode.ts`): never give either its own number (#298).
86
86
  - `openManager` keeps only the manager that answered `/open`: when the answering pid is not the process this open spawned, it stops its own spawn with SIGTERM, so a losing concurrent open never leaves an unowned manager (#584).
@@ -67,7 +67,7 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
67
67
  - `Tasks.review` captures `reviewAuthority` once and rechecks it in `valid()` after every await, right before each write; a headless reviewer's verdict, and its `hub_task_show` (checked before and after the read), holds only while the same child runs for that task at its bound envelope head and review revision (`Tasks.reviewBindingCurrent`, host-only: `in_review`, same reviewer, non-PII, matching `reviewRequestRevision` and review head). Never cache the answer (#223, #533).
68
68
  - Review scope (#852, `src/hub/review-scope.ts`): a reviewer reads `full` (code, affected tests, CI) or `code` (code only). The default is data on the fixed reviewer variants (`AdapterDefinition.reviewer.scope`, `code` for the sandboxed codex and kimi variants, fixed-builtin-only like `instances.modelSelect`); a fresh-reviewer instance may override it with `review_scope` from `config.local.json` only (tier 3, never from a tracked file or the dashboard). `TasksDeps.reviewScope(peer)` is the one read. `sendReview` replaces the Hub-verified, check-result and no-check lines with `CODE_SCOPE_LINE` for a `code` reviewer, which requires every code defect as a structured finding with an id. Reserve findings for defects in the reviewed code, docs or tests; missing check/CI/test-run/PR-or-branch identity evidence goes in `unmet`, never `findings` (Amendment 2). The verdict rule is structural (amended AC3): a `code` reviewer's `changes_requested` is always recorded, never refused, and never classified by reading its unmet text; never add a text classifier (background: README). If its structured `findings` carry no item with disposition `open`, the history entry is `evidenceOnly: true`: it adds nothing to `rejections` (so it cannot escalate), records no failed outcome, contradicts no earlier approval, writes no recurring-finding candidate, is not a rework round in the SDLC metrics, still returns the task to its owner as usual, and sends the configured conductor one ids-only task notice (`evidenceOnlyNotice`; with no conductor, one console notice). A verdict with an open finding counts as today. A person's and a `full` reviewer's verdict are never evidence-only. A `code` reviewer's verdict history entry carries `reviewScope: "code"` and a `full` reviewer's carries nothing, so old rows are unchanged. Merge rule: a task approved by a `code` reviewer is merged by the conductor or the person only after exact-head CI is green and no review thread is unresolved, and the merge note names the CI run.
69
69
  - 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.
70
- - 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.
70
+ - 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.
71
71
  - 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`.
72
72
  - 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.
73
73
  - The review coverage gate (#520) pins `refs.commit` and the sdlc receipt and rechecks them in `valid()`; an unavailable brief is never an empty one (enforce refuses, advise notes). Read brief attributes and rules at the merge base (`--attr-source`, `check-attr --source`, `git show <base>:`), never from the head or the checkout.
@@ -20,7 +20,7 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
20
20
  - 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.
21
21
  - The macOS CI leg is four shards (#548): `check (macos-latest) shard i/4` runs `AHUB_CHECK_SHARD=i/4 scripts/check.sh` (`bun test --shard --timings=scripts/test-timings.json`), and the job named `check (macos-latest)` is only the aggregator over them; `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 (macos-latest) shard i/N`) as gate jobs, so a run with Linux and the seeded shards done and only macOS 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.
22
22
  - A test that proves a daemon answers while work is still running asserts ordering (the answer arrived before the producer finished), never an elapsed wall-clock bound: the daemon promises no millisecond figure for `status`, so a bound measures the runner, not the product (#531); a request timeout in such a test is a hang guard only.
23
- - A tight wall-clock bound in a test must be sized for the slowest CI runner, or replaced by a barrier (#559). Hosted macOS shards run each test about 1.5 times slower than the old single leg (and git-heavy tests about 7 times), so three unchanged tests tipped over once each after #954: `test/path-policy.test.ts` (the #541 hardlink loops took 10.6 s idle for three layouts in one test and 27 s on a shard, past the 20 s limit: the loops are now five parts of 1000 iterations per layout, 5000 per layout kept, about 0.8 s per part), `test/code-facts.test.ts` (a 400 ms deadline raced the fake CLI writing its child marker, which takes about 0.3 s idle and 1.4 s at 30-way load: the test now holds the deadline timers by the configured value, waits for the marker and fires the analysis's own deadline callback, a barrier with the real deadline code; because the held deadlines never fire by themselves, its `finally` must `close()` the reader and await the analysis before the fixture is removed, and a second test pins that teardown) and `test/smoke-pty.test.ts` (the macOS PTY bridge process sometimes never gets past `Bun.spawn` with a `terminal` under load: 13 of 60 at 12-way parallel on a Mac, 0 of 60 at 4-way, never for a plain no-op bun script, and a longer timeout cannot help a process that never starts, so a run that timed out with no output is run once more in the tests whose property is not the timeout, after asserting the first attempt's owned process group was confirmed stopped; that is a bounded second attempt, `runStarted`, which only lowers the odds (two hangs in a row still fail) and was measured on local macOS only, and the real fix is in the bridge or in Bun). When a test needs a bound, measure the slow tail under 30-way local load, size the bound from it with the number in a comment, and prefer a readiness signal (a marker file the fake writes before it hangs, with the deadline fired after it) over a shorter deadline or a longer margin: a margin still depends on the runner's speed, a barrier does not.
23
+ - A tight wall-clock bound in a test must be sized for the slowest CI runner, or replaced by a barrier (#559). Hosted macOS shards run each test about 1.5 times slower than the old single leg (and git-heavy tests about 7 times), so three unchanged tests tipped over once each after #954: `test/path-policy.test.ts` (the #541 hardlink loops took 10.6 s idle for three layouts in one test and 27 s on a shard, past the 20 s limit: the loops are now five parts of 1000 iterations per layout, 5000 per layout kept, about 0.8 s per part), `test/code-facts.test.ts` (a 400 ms deadline raced the fake CLI writing its child marker, which takes about 0.3 s idle and 1.4 s at 30-way load: the test now holds the deadline timers by the configured value, waits for the marker and fires the analysis's own deadline callback, a barrier with the real deadline code; because the held deadlines never fire by themselves, its `finally` must `close()` the reader and await the analysis before the fixture is removed, and a second test pins that teardown) and `test/smoke-pty.test.ts` (the macOS PTY bridge hung inside `Bun.spawn` with a `terminal` when its child wrote and exited before the call returned: Bun up to 1.4.3, oven-sh/bun#45300, fixed in 1.4.4; 26 of 96 bare-Bun runs at 12-way parallel, none of 180 on 1.4.4. A longer timeout cannot help a process that never starts and a retry only lowers the odds, so the bridge starts its child behind a gate it opens after `Bun.spawn` returns and the retry helper is gone (#560); the bridge also stops its child when its caller dies, and a test names a hung bridge with `runPtyWith` instead of relying on load). When a test needs a bound, measure the slow tail under 30-way local load, size the bound from it with the number in a comment, and prefer a readiness signal (a marker file the fake writes before it hangs, with the deadline fired after it) over a shorter deadline or a longer margin: a margin still depends on the runner's speed, a barrier does not.
24
24
  - A request that a test expects a stop or shutdown to cut gets its rejection handler where the request is created, never after the awaited teardown: a late `catch` races the reset and the run fails on an unhandled rejection.
25
25
  - Run `bun scripts/seeded-check.ts` before reporting the seeded-guard gate complete. After changing seeded guard logic, run its affected seeds even when search-anchor counts are unchanged. Put asynchronous calls inside `resolves`/`rejects` expectations so a mutated refusal fails a named assertion. Keep the `scripts/seeds.json` cases sequential; require the same named test green before its seeded assertion fails. Seed rot, survivors, setup/compiler failures, timeout/watchdog termination and process leaks fail the gate. A change that moves or rewrites a line a `scripts/seeds.json` case searches for updates that case's search and replacement in the same PR (seed rot fails the gate), keeping its mutant effect and named test. Never run the full seed runner from a unit test or mutate the source checkout; inspect preserved failed fixtures before removing them. CI runs it as four sequential Linux shards (`--shard i/4`) beside the ordinary check, separately from `scripts/check.sh`; the required `seeded guards` job passes only when every shard passed, or when the decision job reused a verified identical-tree full gate (`admission == 'reuse'`). Keep that job name: branch protection and `scripts/ci-gates.mjs` read it (#457). On seed rot the checker names the anchor's symbol at a base commit and where it is now (`seedRotContext`): `--base <commit>` sets that base (CI passes the PR base from a `fetch-depth: 0` checkout; keep both), and without it the base is `merge-base origin/main HEAD`. The context is evidence only: it never relaxes the one exact match, and an unavailable base says so instead of guessing.
26
26
  - `scripts/lib/ts-ast.ts` is the one TypeScript AST helper for scripts and source-scanning tests (`parseSource`, `callSites`, `symbolSpans`, `enclosingSymbol`, `findSymbol`; inline HTML scripts keep their offsets). It gives lexical locations only, never resolved call edges, and stays dev tooling: never import it from `src/`.
package/docs/dogfood.md CHANGED
@@ -33,7 +33,7 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
33
33
  | Same-family reviewer ban, `review.same_family` (#820) | `never` in this repository's `config.local.json` after the release and a hub restart: a Codex-owned task is never assigned to `codex-review`; the planner (claude) reviews, else the person | board history: tasks whose reviewer family equals the owner's (target 0), `ahub route explain <id>` showing the rule, reviews that went to the person because no planner was eligible |
34
34
  | Per-task and release planners, I-4 (#525-2) | off until this change is released and the running hub is upgraded; this PR enables no new project planner configuration | task/release planner bindings and selection reasons; wrong-planner pre-effect refusals; independent plan-review requests and `selfReview` receipts; queued notices rerouted after a planner change; `task_planners` recovery refusals |
35
35
  | Acceptance criteria ids in review (#507b) | on after the release: a task with a recorded `criteria` checklist lists its ids in the review request and `hub_review` unmet items that start with an id are recorded as `unmetCriteria`; tasks here rarely record a checklist, so most reviews are unchanged | the `Acceptance criteria` block in a review request and `unmetCriteria` in `ahub task show` history for tasks with a checklist; the share of changes-requested verdicts whose unmet items name an id; unmet items that name an id the checklist does not hold (a typo) |
36
- | Recurring unmet criteria candidates (#514a) | on after the release: three tasks (at least two separate pieces of work) of one profile and class whose reviews name the same criterion id unmet in 30 days file one backlog card with the `profile_overrides.review_checklist` line as text; the hub applies nothing. Tasks here rarely record a checklist, so cards will be rare. | cards filed and the share a person accepts into their checklist, from the backlog history |
36
+ | Recurring unmet criteria candidates (#514a) | on after the release: three tasks (at least two separate pieces of work) of one profile and class whose reviews name the same criterion id unmet in 30 days file one backlog card with the `profile_overrides.review_checklist` line as text; nothing applies it on its own; `ahub learn list|accept|reject` (#514b, person only) is the verdict: accept writes the one line to `config.local.json`, reject closes the card. Tasks here rarely record a checklist, so cards will be rare. | cards filed, and accepted versus rejected, read with `ahub learn list --all` |
37
37
  | Planner rulings in review (#835) | source/fixture coverage; active after release and hub upgrade | post-ownership planner notes in review requests and review-only views, 5 x 1000 bounds, withheld/PII exclusions, builder summary still withheld and departure accepted:false |
38
38
  | Deliverable records (#509a) | on after the release: `deliverables` on `hub_task_done` and `hub_sdlc_record`, hashed by the hub in this repository's folder; most tasks here are code with a PR and name none, so only document-shaped work (a report, a spec draft) uses it | the `Deliverables` block in a review request and `deliverables` in `ahub task show`; approvals refused with `deliverable changed since it was delivered`; records with `state: unknown` (a file the hub could not vouch for); claimed-hash refusals as a measure of agents trying to supply a hash |
39
39
  | `ahub deliverables` and the dashboard badge (#509b) | on after the release: `ahub deliverables [--check]` on this repository's board; only tasks that named deliverables appear, so it lists document-shaped work only | the person runs `ahub deliverables --check` after a document task is approved and the next time its file is edited: a `changed since review` row names the edit; `Deliverables n` chips on cards; `not measured now` in the sheet means nobody ran `--check` since the daemon started |
@@ -106,7 +106,9 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
106
106
  | Quota budgets and pauses | on | pauses, resets, time paused (#683) |
107
107
  | Token usage per peer and task | `ahub report` and, from the roll-up in `hub.db`, `ahub usage [--by peer\|task\|issue\|pr\|release\|day\|model]`, `usage limits` and `usage total` (#683 U2); console tab `7` (U3) and dashboard Usage view (U4) shipped | tokens per merged PR |
108
108
  | Operational settings mechanism (#489 H1) | available after release; this PR does not change this hub's running configuration; planner FYI interval defaults to the existing 10 minutes | shared descriptor/schema/UI/apply parity, independent legacy-default pins, real planner-notice boundary and settings inherit/undo/reload fixtures; native console/dashboard editing remains unverified |
109
+ | Recovery and upgrade wait settings (#489 H4, task 445) | available after release; this PR does not change this hub's running configuration: the five `recovery.*` settings default to the waits they replace (managed attach 600 s, unmanaged reconnect 90 s, control request 30 s, shutdown 30 s, terminal idle 120 s) and nothing is set in this repository's config | every default equals its old constant (pinned with independent literals), every person-visible wait refuses a value below what shipped, a bad or unreadable file is the default and never an error inside a recovery, the coordinator reads both layers and the file at each wait, and inherit and undo go through the local file; the waits' use inside the real recovery driver (a configured value changing an actual attachment, shutdown or control wait) is covered only as far as the tests in this PR reach, and no real upgrade was run with a configured value |
109
110
  | Tasks and routing operational settings (#489 H2, task 441) | available after release; this PR does not change this hub's running configuration: the six `tasks.*` settings default to the constants they replace (demotion threshold 1.5, half-life, contradiction window, re-screen cap 10, split minimum 5 observations, split failure share 0.3), and nothing is set in this repository's `config.local.json` | each setting's default equals its old constant (pinned independently of the descriptor), each reader is called by the code that used the constant, `ahub settings set`, inherit and undo for `tasks.demote_at`, and a reload that reaches the next demotion, re-screen and split read; 29 hand-made mutants, none surviving; inherit and undo are not yet looped over all six keys, and no live `ahub settings set tasks.*` against a running hub |
111
+ | Bus and delivery operational settings (#489 H3, task 444) | available after release; no running hub configuration is changed here; five defaults retain the previous retry/digest/pull/note/history constants | default pins, bounded H1 schema/UI/apply parity, next-operation runtime traces and real-daemon write/inherit/undo fixtures for all five keys; live native console/dashboard editing and production delivery performance remain unverified |
110
112
  | Context gate | off: `context.gate = 0` | context-pressure events |
111
113
  | Context headroom admission (#802) | off by default: `context.max_assignment_used = 0`; not enabled here by this PR | `ahub route explain` known/unknown context decisions and offered owners; pure/session-freshness, real Tasks/manual override and machine-local config regressions; native live admission remains unverified |
112
114
  | Always-ask action classes (#636) | on after release for managed Claude forms, hub-owned ACP in ask mode, local/Pi tools and received Codex command approvals; Codex native bypass stays not enforceable | classed permission requests (a `gh api graphql` query is a read, a mutation, `gh run` or `gh workflow` write and every command with a shell expansion in a classed position (`git push $B`, `rm -rf "$DIR"`) is asked about: #756, #762; `test/action-class.test.ts` holds the table), full local/Pi command visibility and denied effects for incomplete native views; actionPolicyInfo current-child enforceability, remaining class budgets and weakening attention; Codex native no-request and Kimi non-ask ceilings are explicit |
@@ -1964,6 +1964,16 @@ A guarded file edit that the daemon keeps at its accepted value remains visible
1964
1964
  | Switches | `tasks.auto_pull` (default off: pull the top explicitly reserved ready card when a planner slot frees) | `config.local.json` | live; the next freed planner slot uses it |
1965
1965
  | Routing | `tasks.queue_backpressure.max_queued` (0-10000), `.max_oldest_s` (0-604800); 0, the default, disables each ([backpressure](#queue-backpressure-on-automatic-task-offers-800)) | `config.local.json` | live |
1966
1966
  | Roles | `planner` (one or up to eight planner holders) | `config.local.json` | at once |
1967
+ | Routing | `bus.max_attempts` (1-100 attempts, default 3) | `config.local.json` | live; next safe-failure retry evaluation, retaining existing counts and quota pauses |
1968
+ | Routing | `bus.digest_max` (1-10 originals, default 10) | `config.local.json` | live; next push/attached-channel take, with poison-alone and remote-singleton rules |
1969
+ | Routing | `bus.pull_budget_bytes` (1-65536 bytes, default 65536) | `config.local.json` | live; next pull-only read, oldest first; the first oversized item still goes |
1970
+ | Routing | `bus.notes_kept` (1-10 notes, default 10) | `config.local.json` | live; next shared-note or failed-preface merge; existing prefaces remain unchanged on reload |
1971
+ | Routing | `bus.terminal_history_limit` (1-2048 rows, default 2048) | `config.local.json` | live; next normal terminal settlement; unresolved rows and retry ancestry stay protected |
1972
+ | Routing | `recovery.reconnect_wait_s` (600-3600 seconds, default 600) | `config.local.json` | next use; the next managed-terminal attachment wait of an upgrade recovery reads it once, at its start |
1973
+ | Routing | `recovery.unmanaged_reconnect_wait_s` (90-900 seconds, default 90) | `config.local.json` | next use; the next unmanaged Claude reconnect wait reads it once, at its start |
1974
+ | Routing | `recovery.control_timeout_s` (30-300 seconds, default 30) | `config.local.json` | next use; the next control request of the recovery coordinator to a hub reads it |
1975
+ | Routing | `recovery.shutdown_wait_s` (30-300 seconds, default 30) | `config.local.json` | next use; the next wait for a committed source to stop reads it once, at its start |
1976
+ | Routing | `recovery.terminal_idle_wait_s` (30-600 seconds, default 120) | `config.local.json` | next use; the next wait for a terminal to go idle before it is closed reads it |
1967
1977
 
1968
1978
  ### Operational numeric settings (#489 H1)
1969
1979
 
@@ -2021,6 +2031,41 @@ The H0 operational items of `tasks.ts`, `routing.ts` and `task-sweep.ts` that st
2021
2031
  freshness check. They bound a whole call, a refusal of a stale host window protects the release, and none is
2022
2032
  independently tunable without changing what a partial result means.
2023
2033
 
2034
+ #### Bus and delivery conversions (#489 H3)
2035
+
2036
+ The five `bus.*` rows above use the H1 descriptor, schema, settings writer and live
2037
+ reload path. Unset defaults equal `MAX_ATTEMPTS`, `DIGEST_MAX`, `PULL_BUDGET_BYTES`,
2038
+ `NOTES_KEPT` and `TERMINAL_CAP`. Only `bus.max_attempts` may rise; the other four
2039
+ can only tighten today's ceilings. A reload does not trim/reorder queues or
2040
+ prefaces, reset attempts, re-arm batch deadlines, prune history, or change a pause
2041
+ or in-flight delivery. Terminal history is a target: protected retry parents and the current mutation receipt may
2042
+ keep the count above it. The settings add no conductor authority.
2043
+
2044
+ The [H3 H0 ledger](verification/2026-10-30-bus-delivery-operational-settings.md)
2045
+ lists all 59 unique inventory identities (5 converted and 54 excluded). The
2046
+ persisted `SEEN_CAP`/dedupe contract and withdrawn cancellation evidence stay
2047
+ fixed; recovery fence polling goes to H4, expired-limit-record cleanup remains
2048
+ maintenance, existing limits/watchdog/batching knobs remain in their current
2049
+ configuration path, and wire/storage/safety/test-seam values remain constants.
2050
+
2051
+ #### Recovery and upgrade waits (#489 H4)
2052
+
2053
+ The five `recovery.*` rows extend the existing `recovery` block (`auto_resume_after_crash` is unchanged) and use
2054
+ the H1 descriptor, schema and settings writer. They are read by the upgrade coordinator, a CLI process and not the
2055
+ daemon, from the project it is acting on, at the start of each wait: the shared `config.json` first and the
2056
+ machine-local `config.local.json` over it (ignored when git tracks it). The apply class is `next use`: a wait that is
2057
+ already running keeps the deadline the receipt records in `waits`, and a resume reads the file again. Unset defaults
2058
+ equal `RECONNECT_WAIT_MS`, `UNMANAGED_RECONNECT_WAIT_MS`, `RECOVERY_CONTROL_TIMEOUT_MS`, the 30 s shutdown wait and
2059
+ the 120 s terminal idle wait. A value outside a row's bounds, or a file the coordinator cannot read, is the default:
2060
+ a recovery never fails on its configuration.
2061
+
2062
+ Every minimum is today's constant except `recovery.terminal_idle_wait_s`: a person-visible wait can be raised and
2063
+ never shortened below what ships, and a control request that times out may already have applied. A shorter terminal
2064
+ idle wait only ends in manual-required, which retains the terminal. The 600 s wait for the source to become quiet is
2065
+ not a setting: the source's preparation lease is a fixed ten minutes, so no other value is valid. The 180 s global
2066
+ install timeout has no project to own it, and the 610 s Orca command timeout follows terminal recovery's own ten-minute
2067
+ ceiling. See the [H4 ledger](verification/2026-10-30-recovery-wait-settings.md) for every item in the group.
2068
+
2024
2069
  - Writes go only to `.agenthub/config.local.json` and `.agenthub/routing.local.toml`,
2025
2070
  the machine-local files (`ahub init` ignores both in git). The shared
2026
2071
  `config.json` and the hand-written `routing.toml` are never rewritten, and a
package/docs/profiles.md CHANGED
@@ -51,7 +51,7 @@
51
51
  # Project profile overrides
52
52
 
53
53
  - Acceptance criteria are profile-independent (#507b): a task's recorded criteria checklist (docs/sdlc.md, Acceptance-criteria checklist) is listed in every profile's review request, ahead of the profile's `review_checklist` lines, and an unmet item that starts with a criterion id names it; a profile only changes the wording of its checklist, never the criteria or their ids.
54
- - Recurring unmet criteria become a candidate, never a change (#514a): when reviews name the same criterion id unmet on at least 3 tasks of one profile and class within 30 days, and those tasks make up at least 2 separate pieces of work (tasks sharing a PR or a branch count once), the hub files one backlog card with the `profile_overrides.review_checklist` line to consider. The line is text; the hub writes no config. `profile_overrides` is not machine-local, so you choose the shared `.agenthub/config.json` (every checkout) or `config.local.json` (this machine), and add the line to your current checklist because `review_checklist` replaces as a unit. PII tasks and evidence-only verdicts never count.
54
+ - Recurring unmet criteria become a candidate, never a change (#514a): when reviews name the same criterion id unmet on at least 3 tasks of one profile and class within 30 days, and those tasks make up at least 2 separate pieces of work (tasks sharing a PR or a branch count once), the hub files one backlog card with the `profile_overrides.review_checklist` line to consider. The line is text, and nothing applies it on its own. By hand: `profile_overrides` is not machine-local, so you choose the shared `.agenthub/config.json` (every checkout) or `config.local.json` (this machine), and add the line to your current checklist because `review_checklist` replaces as a unit. With the command (#514b): `ahub learn list` shows the open cards, `ahub learn accept <id>` adds that one line to your current checklist in `.agenthub/config.local.json` (this machine only, never the shared or any tracked file) through the settings writer, closes the card, and says it applies after a restart; `ahub settings undo` puts the previous file back; `ahub learn reject <id>` only closes the card. The line is rebuilt from the key line of the card and the board's own evidence, never parsed from the card text. Person only: an agent shell and the configured conductor are refused. PII tasks and evidence-only verdicts never count.
55
55
  - `profile_overrides` in shared/local config selects project data over the chosen shipped profile. `classes`, `role_text` and `catalog_roles` maps merge by key across layers; `plan_keys`, `agents`, `example_checks` and `review_checklist` replace as units. A local `profile` selects the active base for both init and native contracts.
56
56
  - Classes may add or remap profile aliases to existing base classes. Existing base names keep their meanings; class maps are bounded to 32 override entries. Task storage and route explanations preserve the alias, including a dev custom alias, while routing/budgets/checks use its base. Other tasks under custom dev data retain the selected profile semantics too, so document sections do not become whole-path overlaps. Unmodified dev rows retain their old shape.
57
57
  - `plan_keys` selects distinct existing plan fields, not new board fields. Task proposals and their schemas use the selection. Accept schemas retain legacy fields for already existing tasks/profile changes.
@@ -17,7 +17,7 @@ ahub setup # installs the Claude Code channel plu
17
17
  Or use the matching GitHub release:
18
18
 
19
19
  ```bash
20
- bun add -g github:STAIxBWLB/agent-hub#v0.21.0
20
+ bun add -g github:STAIxBWLB/agent-hub#v0.22.0
21
21
  ahub setup
22
22
  ```
23
23
 
@@ -30,6 +30,10 @@ and upgrade or crash recovery, see the [operations guide](operations.md). To
30
30
  move running projects to a newer release later, run `ahub upgrade` in a
31
31
  terminal: it reviews the plan with you, applies it and follows it.
32
32
 
33
+ ## Upgrading to 0.22.0
34
+
35
+ Nothing changes until you set something. Ten operational numbers became settings: five bus and delivery limits (`bus.*`, #975) and five upgrade-recovery waits (`recovery.*`, #979). Unset, each keeps the value it had. `ahub learn list|accept|reject` (#514b, person only) is your verdict on the recurring-unmet-criterion cards. The macOS PTY bridge no longer hangs under load or outlives its caller (#560). The `@staix/agent-mem` pin stays 0.13.0. Upgrade running hubs with `ahub upgrade` and restart Claude sessions so they load the 0.22.0 plugin.
36
+
33
37
  ## Upgrading to 0.21.0
34
38
 
35
39
  The configured conductor may now run more operational commands from its own shell (#933): `board import`, `intake approve`, `schedule run|pause|resume`, `research backfill`, `bench run`, `source add`, `goal link`, `task unassign`, starting Jcode, and `settings set tasks.escalate_after`; the hub rechecks the role and records each run. Trust, money, attribution and interactive terminals stay the person's: `permission`, `agents trust`, `role assign`, budget changes, `--as-user`, `pet --window`, `watch`, `recovery reattach`, memory changes and every goal change except `link`. Any agent may `task cancel`, `task label` and `task role` a task it owns and list `ahub deliverables` for the tasks it can see. A conductor's `restart --yes`, `upgrade --yes --to <exact-version>` and `recovery resume|abort` need a source-issued continuation bound to that project, and a handoff to a newer release's coordinator stays the person's (#900; see [conductor operations](operations.md#conductor-operations-812-t2-933-900)). `ahub sdlc merge` and the conductor's `ahub doctor --orphans --kill` now print readable output: a script that parses them passes `--json`, which prints what they printed before (#899). `ahub down` stops the hub like `ahub kill`. New and opt-in: `ahub source add|list` (#508), machine-local `schedules` and `folder_triggers` with `ahub schedule` (#512), `ahub intake` draft backlog cards (#511), domain packs with `ahub init --profile <pack>` (#513a), `local.web_fetch` and `local.doc_read` for the on-prem worker (#515), queue backpressure and context headroom admission for new work (#800, #802), and person-only rooms between machines through `ahub broker` and `ahub room` (#446; see [outbound room membership](operations.md#outbound-room-membership-446-x3)). `ahub turns` and `ahub undo` name a dirty git submodule instead of hiding it (#798). A `hub_send` whose `to` is one peer id given as a string now reaches only that peer instead of every peer, and any other malformed `to` is refused before anything is sent (#963). The hub now exact-pins `@staix/agent-mem` 0.13.0, with no storage change for the hub. Upgrade running hubs with `ahub upgrade` and restart Claude sessions so they load the 0.21.0 plugin. Coming from 0.19.0 or older, the notes below apply too.
package/docs/smoke.md CHANGED
@@ -1763,6 +1763,8 @@ Qwen recovery source/fake proof: saved ids pass through the driver and CLI to AC
1763
1763
 
1764
1764
  Source/fixture proof is recorded in verification/2026-10-11-jcode-peer-contract.md; the live owned-daemon and ACP handshake on 0.94.0 and 0.98.1 in verification/2026-10-19-jcode-0.98.1.md. Unverified: real provider/model, native project MCP discovery and hub-tool receipt, native tool policy and stop/child teardown. Native automatic restoration is withheld because the pinned ACP load does not prove the id. No benchmark comparison or default replacement ran.
1765
1765
 
1766
+ Historical native-owned hub-tool effects are now recorded in the [ids-only AC4 receipt](verification/2026-10-30-jcode-ac4.md). The [fresh isolated 0.99.1 AC6 re-probe](verification/2026-10-30-jcode-ac6.md) still finds no `session/load` identity echo, so automatic recovery remains withheld; no fresh model turn or persisted-conversation restoration is claimed.
1767
+
1766
1768
  ### Extra Jcode instances (#685, #368)
1767
1769
 
1768
1770
  Source and fixture coverage: `test/jcode-instances.test.ts` (declaration refusals, the bound of 8, home uniqueness, no relay or credential fields), `test/jcode-instance-peer.test.ts` (separate groups, sockets and homes, stopping one leaves the other, argv is the base argv plus the instance's `-p`, `--model` and `--provider-profile`, launch id per owned process, launcher, doctor) and `test/jcode-instance-daemon.test.ts` (home preflight, no file written under the home, status, hub-tools admission only for an owned launch, a forged `AGENTHUB_PEER_ID` refused, the MCP server with no peer id refusing every tool).
@@ -2100,6 +2102,10 @@ The new receipt must be a previously absent file directly under the fixture root
2100
2102
 
2101
2103
  Real-account Q2 receipt: **UNVERIFIED**. The conductor runs this leg with the person after merge. Fake PTY/protocol tests are implementation evidence only. Q3 approvals, Q5 FIFO and Q6 resume are later executable legs. Q4 remains the known native Qwen turn-stop limitation documented in the 2026-10-25 T0 receipt; the runner's owned-group timeout cleanup does not claim a supported in-session turn interruption.
2102
2104
 
2105
+ ## Capability source probes (#445 C0)
2106
+
2107
+ The 2026-10-30 [receipt](verification/2026-10-30-445-capability-probes.md) records what Kimi 2.3.2, Qwen 0.26.1 and Codex 0.166.0 expose for capability discovery, probed in isolated, credential-free homes with a network-denied profile and real config files checked unchanged. Listing legs are live; skill execution on each agent needs an account and stays **UNVERIFIED** until #445 C4. Design consequences: [Capability invocation (#445)](specs/2026-09-19-agent-hub-design.md#capability-invocation-445).
2108
+
2103
2109
  ## Model and effort capability verification (#266 AC1)
2104
2110
 
2105
2111
  The 2026-10-29 [version-bound capability table](specs/2026-09-19-agent-hub-design.md#vendor-model-and-effort-controls-266-ac1)
@@ -3496,6 +3496,7 @@ the queue.
3496
3496
  - Flow authority remains the separately validated `roles` map; executable checks remain trusted `checks`. Example checks are display data and do not execute. Managed instruction content keeps surrounding text and quotes forged headers.
3497
3497
  - Acceptance criteria with ids (#507b) reuse the #481 S8 `criteria` checklist, the only criteria model: `sendReview` lists the recorded criteria (`- <id>: <result>`, `(optional)` for a non-required one) ahead of the profile's checklist lines in every profile, and `Tasks.review` records on the verdict the ids that the reviewer's `unmet` items name, through the pure `unmetCriteriaIds` (an item names a criterion only by starting with its exact recorded id, followed by the end of the item or a delimiter; each id once, in first-named order). The ids come from the board's checklist at the verdict, never from the caller, and are stored as `HistoryEntry.unmetCriteria` (validated: at most 32 distinct bounded ids, on a saved review verdict only) and shown in the public history. The note text is unchanged. A PII task lists and records none; a task with no checklist, a verdict with no unmet items and an item with no matching id add nothing, so every existing request and verdict is byte-identical. #513 packing and #514 learning refer to these ids.
3498
3498
  - Recurring unmet criteria (#514a, `src/hub/learning.ts`, `Tasks.proposeCriterionCandidates`) reuse the #481 S3 recurrence path: after a saved verdict that named criteria, the pure `groupUnmetCriteria` groups the ids reviews recorded as `unmetCriteria` in the last 30 days by profile, profile class (the alias when the task has one) and criterion, over rows that are not PII and whose text the candidate screen allows. A group that this verdict named, that has at least 3 tasks forming at least 2 separate pieces of work (`workUnits`) and no card yet (`Criterion candidate key: <profile>:<class>:<id>` in a hub-filed backlog card's detail, the same dedupe as `Review finding key:`) files ONE backlog `plan` card for a person: ids and counts only, plus the `profile_overrides.review_checklist` fragment as text (`overridesDiff`). Nothing is applied: `profile_overrides` is not machine-local (it is read from the tracked `.agenthub/config.json` and the untracked `config.local.json`, layers merged, and is in neither `MACHINE_LOCAL` list), so the person chooses the layer and merges the line into their own checklist, which replaces as a unit. `selectGroups` applies those filters before the per-pass cap of 64 cards, so groups that are settled (already carded, unnamed, under threshold) never use up the cap and starve a new one. A withheld verdict, an evidence-only verdict (#852), a private turn and a PII task file nothing and count for nothing. Per-profile `profile_overrides` application, thresholds as config and decay are not part of this step.
3499
+ - `ahub learn list|accept|reject` (#514b, `Tasks.learnCandidates|learnAcceptPlan|learnClose`, the `learn_*` task ops) is the person's verdict on those cards, and it amends the #514a card text: "nothing in the hub writes it" became "the hub writes it only when the person runs `ahub learn accept`" (cards filed earlier keep their old sentence, which is harmless: the command reads only the key line). Person only end to end: `classifyPeerCommand` gives `learn` no entry (so `console`, no `CONDUCTOR_*` entry), the daemon's generic console-op gate refuses every non-person caller including the configured conductor, and each `Tasks.learn*` method refuses any `by` but the person. `parseCandidateCard` reads ONLY the first line of the detail (`Criterion candidate key: <profile>:<class>:<criterion>`) from a `plan` card whose first history entry is the hub, re-checks the three parts against the shapes the loaders accept and the title against the key; the task count and the line are recomputed from the board (`learnEvidence`, `overridesDiff`), so no text of the card is parsed as a value or written. Accept refuses a card for another profile than the project's, a config that does not load, a list that would pass the loader's 16 lines, and a card with no evidence left; it extends the checklist the profile uses now (the person's, else the shipped one) because `review_checklist` replaces as a unit, skips a line already present, and writes through `settingsSet` with the one managed definition `LEARN_CHECKLIST` (`src/hub/settings.ts`): a function argument that no request field can supply, resolved by no ordinary lookup (`settingDef` does not know it; `undoableDef` does, for undo only), so `settings set`, a dashboard session and a conductor see an unknown setting. The write is to `config.local.json` only, through the same loader check, tracked-file refusal, atomic replace and single undo record as every setting, `restart` class. The card closes through `Tasks.cancel` after the write succeeded (a failed write leaves it open), the verdict in its history and its dedupe key kept, so the criterion is never filed again. The settings event, notice and undo reply for this key carry the line count, never the person's lines.
3499
3500
  - Override maps are bounded to 32 class entries and existing base classes; plan selections use existing fields. Role text is 2000 characters, agents 16000, examples 16 strings of 2000 characters, review checklist 16 lines of 500 characters (#507). Native ingestion/retention/measurement remains unverified; configuration changes apply after daemon/MCP restart.
3500
3501
 
3501
3502
 
@@ -6304,3 +6305,33 @@ sessions do not. A new row never grants conductor permission automatically.
6304
6305
  Safety floors and contract constants are not converted by H1. A later exposed
6305
6306
  safety floor must allow tightening only in schema bounds and have its own test;
6306
6307
  wire/storage/protocol contracts remain constants.
6308
+
6309
+ ## Capability invocation (#445)
6310
+
6311
+ Plan of record: the #445 C0 freeze (issue comment of 2026-10-17, owner decisions O1-O5 of 2026-10-18, go-ahead 2026-10-25). Phases C1-C4 implement it in order and amend this section when they differ. Nothing below is implemented yet; the [C0 receipt](../verification/2026-10-30-445-capability-probes.md) records what the native sources actually expose.
6312
+
6313
+ Delegated execution, never shared execution: a peer publishes capabilities, another peer asks for one, and the target runs it in its own turn with its own permissions and sandbox. The hub never calls a native tool itself (Codex `mcpServer/tool/call` and `command/exec` are excluded) and never hands out credentials or MCP connections.
6314
+
6315
+ ### Decisions
6316
+
6317
+ - **D1 manifest.** An adapter-definition field (`registry-types.ts`), one discovery path per adapter. Each `Capability` has `id` (`${kind}:${name}`, unique per peer id), `kind` (`command|skill|mcp_tool`), `name` (`/^[A-Za-z0-9_.:-]{1,64}$/`), a `sanitize()`d description of at most 300 characters, an optional flat `inputSchema` (absent means `{ input: string }`), the publisher's optional `sideEffect` (`read|write|external|destructive`), `source` (`acp|codex-skills|published`), `invoke` (`native|prompt`) and a `digest` over the canonical fields. Sources: ACP `available_commands_update` (`invoke: native`, the prompt `/name input`); Codex `skills/list` (`invoke: native`, the `turn/start` item `{ type: "skill", name, path }`); `hub_capabilities_publish` for Claude and others and Codex MCP tools (`invoke: prompt`). No cost hint.
6318
+ - **D2 opt-in and discovery.** `invocation.share` in `.agenthub/config.local.json` only (`invocation` is machine-local and never editable from settings): a positive allowlist keyed by peer id, each entry pinned by digest with the person's `class`. A changed digest unlists the entry until re-pinned; an instance id never inherits its kind's entries. `hub_capabilities { peer? }` lists shared entries only (id, schema, effective class, the target's permission mode); unshared ones appear only in the person's `ahub capabilities`.
6319
+ - **D3 authority.** Callers hold the new `invoke` hub capability; the conductor gets nothing extra. Effective class is the higher of the declaration and the person's `class` (O1); a side that names no class contributes nothing, and only when neither names one is the class `destructive` (O6). Gates: `read` needs no task and no approval; `write` and `external` need an open task whose owner or reviewer is the caller plus person approval on every call through `requestPermission` with a `personOnly` flag that bypasses unattended auto-allow (O2); `destructive` is refused (O3). Every hub-side refusal is a `PreEffectToolRefusal` with a precise reason before any envelope. A callee refuses with `[REFUSED] <reason>` (outcome `refused`); a native rejection is `failed`. Each invocation is a row in an `invocations` table in `hub.db` (ids, digest, class, task, approval id, args sha256, outcome, times, result bytes) plus an ids-only event; a task-scoped one adds history through `Tasks.recordInvocation`. Bodies are never logged.
6320
+ - **D4 PII.** `hub_invoke` is refused during a PII turn, for a PII task, and when the Tasks PII screen finds or cannot rule out PII in free-text args. The result is bounded to 8 KiB, `sanitize()`d, given a hub-written untrusted header, addressed to the caller only and never captured to memory.
6321
+ - **D5 transport.** New envelope kinds `invocation` and `invocation_result`, journalled, delivered alone and never condensed, priority `status`. `hub_invoke` returns `{ invocation, state: "queued" }` at once. Deadline `invocation.timeout_s` (default 600): still queued means `expired`; delivered without a result means `timeout` for `read` and `needs_review` otherwise. No caller cancel; the person uses `ahub queue resolve`. Offline, paused, held, off-roster and pull-only targets are refused (O4). Depth 1: no `hub_invoke` inside an invocation turn. `invocation` joins `DAEMON_FEATURES`; upgrade and restart refuse while invocation rows are open on a target without it.
6322
+ - **D6 failures.** An uncertain `write` or `external` outcome is `needs_review` and never retried; a `read` may be redelivered once, on a `failed_safe` receipt only.
6323
+ - **O5.** `invoke: prompt` capabilities are listed and invocable, labelled as model-mediated requests.
6324
+ - **O6 (2026-10-30).** `destructive` applies only when neither the publisher nor the person's share entry names a class. It relaxes the freeze's "undeclared reads as `destructive`", which with O1 would have refused every native capability, since no native listing declares a class.
6325
+
6326
+ ### What C0 found (binding on C1)
6327
+
6328
+ Probed on Kimi 2.3.2, Qwen 0.26.1 and Codex 0.166.0 in isolated, credential-free homes; the card's older versions were not probed.
6329
+
6330
+ - **Pins.** A source lists only on the version its receipt names. Any other version lists nothing until a new receipt covers it.
6331
+ - **ACP listings need a session.** Both agents refuse `session/new` without auth (`-32000`) and send `available_commands_update` only after a session exists, so C1 reads the list from an authenticated peer's session, never from a probe of its own.
6332
+ - **Builtins must be filtered.** Kimi and Qwen mix session builtins (Kimi `compact`; Qwen `clear`, `config`, `export`, `update`, `remember`, `forget`, `auth`) into the same list as skills. C1 lists only entries it can tell are skills: Qwen by `_meta.source`, Kimi by its `skill:` prefix. A builtin is never listed.
6333
+ - **No schema, no class.** No native listing carries an input schema or a side-effect class (Kimi and Qwen give only an `input.hint`), so native capabilities get `{ input: string }` and their effective class is the person's share `class` (O6). A share entry without a class leaves the capability `destructive`, so refused.
6334
+ - **Names.** Kimi names contain `:` and `.` and fit the regex. The id of a Kimi skill is `command:skill:<name>`; C1 keeps the native name unchanged rather than stripping the prefix.
6335
+ - **Native rejection.** Qwen rejects a command outside its ACP set with `-32603`; Kimi ends the turn normally with an "Unknown ACP command" text chunk (source reading). C2 maps Qwen's error code and matches Kimi's text to `failed`.
6336
+ - **Codex.** `skills/list` answers with no thread and no auth. The `skill` input item is in the stable schema; a live skill turn is unverified. `mcpServer/tool/call` has no approval gate in the 0.166.0 source, which confirms its exclusion.
6337
+ - **Unverified until C4.** Executing a skill through `/name input` on Kimi or Qwen and a Codex `skill` turn (each needs an account), and listings under a real login.
@@ -0,0 +1,49 @@
1
+ # 0.22.0 release verification
2
+
3
+ This record covers release preparation and source review. It claims no installation, no running-hub upgrade to 0.22.0 and no live TUI launch.
4
+
5
+ ## Candidate
6
+
7
+ Version 0.22.0, prepared from main 28acd10e (9 commits since v0.21.0, 3 of them `feat`; bump rule: minor). The release commits carry the version, the CHANGELOG section, the bundle and the documentation stamps.
8
+
9
+ ## Reviews
10
+
11
+ Pull requests named by the commits since v0.21.0: #981, #976, #980, #979, #978, #977, #975, #972, #973.
12
+
13
+ Each of these merged with `gh pr merge --squash --match-head-commit <head>` after its review, with exact-head CI green and no unresolved review thread at that head:
14
+ - #981 (jcode, docs only): the two nits from the #979 review (task 445). The conductor read the diff and merged on green CI; it has no hub task of its own.
15
+ - #980 (task 428): approved at 0045cdc5 after one change request that removed real config-file hashes from the AC6 receipt.
16
+ - #979 (task 445, #489 H4): approved at b06839f7 with two doc nits, fixed by #981.
17
+ - #978 (task 177, #445 C0): approved by the person at 4713fdb8, which includes owner decision O6.
18
+ - #976 (task 560): approved at ab8af600 after one change request (the caller captured before `Bun.spawn`, PTY tests within 20 s). Its first fix at 3d22f048 failed the ubuntu leg on a macOS-only assertion, which was then limited to darwin.
19
+ - #977 (task 564), #975 (task 444, #489 H3), #973 (task 563) and #972 (task 323) were approved at their merged heads.
20
+ No merge was bound to an approval at an earlier head.
21
+
22
+ ## Native acceptance receipts
23
+
24
+ Receipts added under `docs/verification/` since v0.21.0: `docs/verification/2026-10-30-445-capability-probes.md`, `docs/verification/2026-10-30-bus-delivery-operational-settings.md`, `docs/verification/2026-10-30-jcode-ac4.md`, `docs/verification/2026-10-30-jcode-ac6.md`, `docs/verification/2026-10-30-recovery-wait-settings.md`.
25
+
26
+ This list is read from the repository, never written by hand. Native acceptance and the person's release consent are separate records: consent is `ahub sdlc authorize-release 0.22.0`, person only, and is not recorded in this file. The tag run refuses without it.
27
+
28
+ ## Document review
29
+
30
+ `node scripts/check-docs.mjs --require-fresh` reported 12 stale documents. Each was checked against its source commits since its stamp (#972 `ahub learn`, #975 `bus.*`, #979 `recovery.*`, #976 the PTY bridge; #977 and #973 for tests.md) for statements those commits made untrue, then restamped at cb942e06:
31
+ - src/cli/help.ts (1): the `ahub learn` line matches the usage string in `src/cli/main.ts`.
32
+ - README.md (5) and docs/quickstart.md (5): the status line, the report link, the install pin and a new "Upgrading to 0.22.0" note.
33
+ - docs/operations.md (5): the `bus.*` and `recovery.*` rows and sections; the H3 ledger paragraph placement was fixed by #981. `ahub learn` is documented in docs/profiles.md and the help, not here.
34
+ - docs/agent-notes/bus.md and daemon.md: three statements named constants that are now settings (`PULL_BUDGET_BYTES` and `DIGEST_MAX`, `RECONNECT_WAIT_MS`, the 30 s recovery control cap). They now name the settings, in 0737fa19.
35
+ - docs/agent-notes/tasks.md and tests.md: already updated by #972 and #976 (the `runStarted` line). adapters.md, budget.md and docs/security.md: no statement changed by these commits. docs/agent-notes/README.md: stale only through daemon.md.
36
+ The CHANGELOG gained the missing #975 entry.
37
+
38
+ ## Checks
39
+
40
+ In the release worktree with its own frozen install (`@staix/agent-mem` 0.13.0): `node scripts/check-docs.mjs --require-fresh` printed `docs: OK (0 stale documents)`, `bun x tsc --noEmit` was clean, `bun scripts/build.mjs --check` exited 0 and `node scripts/check-package.mjs` printed `package: OK (645 files, @staix/agent-hub@0.22.0)`. The release PR's exact-head CI runs the full gate on both OSes.
41
+
42
+ ## Known gaps, not blocking
43
+
44
+ - The release adds no new live receipt. Still open: #912 AC1, #793 AC6 (task 468: the hub must be restarted with the remote runner), #418 AC8, and #332 AC6 (Jcode 0.99.1 still does not echo the loaded session id; the jcode-ac6 receipt).
45
+ - The ten new settings were exercised through fixtures and a real daemon in tests, never edited live against a running hub. No real `ahub upgrade` ran with a configured `recovery.*` value.
46
+ - The PTY start gate is a workaround for Bun bug #45300 (fixed in 1.4.4). The pin stays 1.4.2: 1.4.4 is under two weeks old until 2026-11-05 (task 565).
47
+ - #445 capability skill execution (Kimi, Qwen, Codex) is unverified; it needs accounts, in phase C4.
48
+ - The full `scripts/check.sh` was not run locally; the release PR's CI runs it on both OSes.
49
+ - Not in this release: #982 (task 446, #489 H5), approved and merging after the tag.