@staix/agent-hub 0.25.0 → 0.26.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 (72) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +10 -5
  3. package/docs/agent-notes/README.md +12 -0
  4. package/docs/agent-notes/adapters.md +1 -1
  5. package/docs/agent-notes/daemon.md +6 -4
  6. package/docs/agent-notes/tasks.md +5 -4
  7. package/docs/agent-notes/tests.md +1 -0
  8. package/docs/dogfood.md +4 -0
  9. package/docs/operations.md +131 -14
  10. package/docs/quickstart.md +14 -4
  11. package/docs/security.md +51 -8
  12. package/docs/specs/2026-11-01-research-campaigns-design.md +9 -6
  13. package/docs/verification/2026-11-01-0.26.0.md +54 -0
  14. package/docs/verified.json +28 -16
  15. package/package.json +1 -1
  16. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  17. package/plugins/agent-hub/server.js +182 -95
  18. package/src/adapters/channel-peer.ts +4 -2
  19. package/src/adapters/definitions/builtin-metadata.ts +6 -6
  20. package/src/adapters/definitions/qwen.ts +2 -0
  21. package/src/adapters/registry-types.ts +2 -0
  22. package/src/adapters/registry.ts +1 -0
  23. package/src/cli/conductor-recovery.ts +3 -2
  24. package/src/cli/help.ts +7 -7
  25. package/src/cli/main.ts +62 -23
  26. package/src/cli/recovery-package.ts +94 -4
  27. package/src/cli/reset.ts +14 -4
  28. package/src/cli/runtime-version-warning.ts +28 -0
  29. package/src/cli/start-agents-run.ts +125 -0
  30. package/src/cli/task-output.ts +5 -0
  31. package/src/cli/upgrade-interactive.ts +4 -4
  32. package/src/cli/upgrade-runtime.ts +105 -57
  33. package/src/cli/upgrade.ts +90 -28
  34. package/src/hub/autonomy-defaults.ts +32 -7
  35. package/src/hub/board.ts +129 -13
  36. package/src/hub/budget.ts +3 -4
  37. package/src/hub/child-process.ts +4 -3
  38. package/src/hub/conductor.ts +3 -4
  39. package/src/hub/config-apply.ts +13 -2
  40. package/src/hub/config-edit-host.ts +1 -0
  41. package/src/hub/config-tier.ts +3 -0
  42. package/src/hub/config-trust.ts +10 -2
  43. package/src/hub/control-client.ts +30 -10
  44. package/src/hub/daemon.ts +82 -17
  45. package/src/hub/db-schema.ts +143 -0
  46. package/src/hub/delivery-journal.ts +3 -4
  47. package/src/hub/execution-budget.ts +3 -4
  48. package/src/hub/folder-trigger.ts +3 -4
  49. package/src/hub/invocations.ts +3 -4
  50. package/src/hub/lifecycle.ts +87 -29
  51. package/src/hub/manager.ts +49 -33
  52. package/src/hub/orca-env.ts +19 -0
  53. package/src/hub/project-runtime-state.ts +139 -0
  54. package/src/hub/project-runtime.ts +62 -0
  55. package/src/hub/protocol-window.ts +19 -0
  56. package/src/hub/recovery-store.ts +102 -12
  57. package/src/hub/registry.ts +5 -0
  58. package/src/hub/research-experiments.ts +33 -4
  59. package/src/hub/research-runner.ts +172 -0
  60. package/src/hub/restart.ts +9 -0
  61. package/src/hub/runtime-capture-prune.ts +415 -0
  62. package/src/hub/runtime-prune-health.ts +112 -0
  63. package/src/hub/schedules.ts +3 -4
  64. package/src/hub/sdlc-merge.ts +7 -0
  65. package/src/hub/sdlc-queue.ts +13 -1
  66. package/src/hub/settings.ts +4 -0
  67. package/src/hub/snapshots.ts +3 -4
  68. package/src/hub/start-agents.ts +44 -0
  69. package/src/hub/task-approval.ts +23 -0
  70. package/src/hub/tasks.ts +133 -34
  71. package/src/hub/usage-rollup.ts +3 -4
  72. package/src/pi/tool-receipts.ts +3 -4
package/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@ 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.26.0
8
+
9
+ - Retain each project's verified runtime across stop/start, preserve other projects when upgrading one, prune unreferenced source captures with reference checks, and report stopped projects before source preservation (#1039 follow-up). Runtime selection protection begins with the 0.26.0 resolver.
10
+ - Bind signed external runner receipts to research attempts (#1018). Keep runner-attested evidence separate, show current key revocation status, preserve idempotent replay, and guard first-attestation format-2 storage and recovery. Registration is person-only; the hub runs no external job.
11
+ - Re-review approved, unmerged tasks at a new owner-reported head, or let the reviewer withdraw approval (#1047). Keep each verdict bound to its head, show stale approvals, and block stale-head merges.
12
+ - Upgrade one project while other projects keep working (#1039, #1045): negotiate protocol 17/16, use per-project recovery holds and verified versioned daemon captures, and update the global CLI and shared plugin separately. Doctor reports older shared tools.
13
+ - Refuse incompatible `hub.db` formats before daemon effects or database writes, with a named required-version message. Preserve legacy recovery contracts and require guard-aware targets for new controlled replacements.
14
+ - `ahub up` now also starts a default set of agents (task 608, #1040): `claude` and `codex` unless chosen, each through the hub's own start (its start mode, the `terminal.open` or Orca terminal, `already attached` for an attached peer), one line per agent, and a refusal names the command to run by hand without stopping the hub or the others. The set is the new machine-local setting `start.agents` (ignored from a committed `config.json`, a Start-group settings row, an empty list starts none); `ahub setup` asks once (`--agents <a,b>`, `--yes`, no terminal keeps the default) and writes `~/.agenthub/defaults.json`, which `ahub init` copies into a new project; `ahub up --no-agents` skips it for one run. The dashboard's Start counts as `ahub up`. A daemon restart, recovery, config reload or upgrade relaunch starts none from it, and qwen, a declared instance or a channel extra cannot be listed.
15
+ - A test run or `scripts/check.sh` started from an Orca pane no longer opens real Orca tabs and agents there (task 613, #1048). The test preload and the gate now remove every `ORCA_*` variable they inherit, `personEnv` drops the whole prefix, and the launcher's four names live in one list (`src/hub/orca-env.ts`). A test that needs an `ORCA_*` value sets it itself. `test/orca-env-guard.test.ts` runs a real `ahub up` with fake `ORCA_*` values and a stub Orca CLI and fails if the stub is called.
16
+
7
17
  ## 0.25.0
8
18
 
9
19
  - Pin `@staix/agent-mem` 0.18.0 (was 0.17.0), which adds forward-tolerant amem hook-spool handling, an amem console Recap panel and screened tool-input capture. The hub continues to use the library API and does not call the hook spool.
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.25.0, control protocol 16. Bun + TypeScript, one loopback daemon per
8
+ Status: 0.26.0, control protocol 17 (accepts 16). Bun + TypeScript, one loopback daemon per
9
9
  project. Site: [agent-hub.staix.net](https://agent-hub.staix.net).
10
10
 
11
11
  ## Quick start
@@ -14,10 +14,12 @@ You need [Bun](https://bun.sh) 1.4.2 or later on PATH.
14
14
 
15
15
  ```bash
16
16
  bun add -g @staix/agent-hub && ahub setup
17
- cd <your project> && ahub init && ahub up # daemon plus the operator console
17
+ cd <your project> && ahub init && ahub up # daemon, claude and codex, then the operator console
18
18
  ```
19
19
 
20
- Then start the agents you use, one terminal each, and talk to them:
20
+ `ahub up` opens the default agents (claude and codex; choose them at `ahub setup`,
21
+ skip them with `--no-agents`) through `terminal.open` or Orca. Where it cannot, it
22
+ names the command; run the agents you use by hand, one terminal each, and talk to them:
21
23
 
22
24
  ```bash
23
25
  ahub claude # Claude Code with the hub channel
@@ -97,7 +99,10 @@ become links. The scene carries labels and states only, never message bodies or
97
99
 
98
100
  ## Upgrading
99
101
 
100
- Run `ahub upgrade` while your hubs are running. Read the notes for your version in
102
+ Run `ahub upgrade` in a project while its hub is running (`--project` selects
103
+ another); other projects keep their version. Update the global CLI
104
+ (`bun add -g @staix/agent-hub@<version>`) and run `ahub setup` for the plugin
105
+ yourself. Read the notes for your version in
101
106
  the [Quickstart](docs/quickstart.md) ("Upgrading to ...") and the
102
107
  [Changelog](CHANGELOG.md) first: some releases need a one-time step, such as
103
108
  refreshing an agent's MCP entry.
@@ -121,7 +126,7 @@ uses the home must run a version that understands the new layout.
121
126
  [#258](https://github.com/STAIxBWLB/agent-hub/issues/258)).
122
127
 
123
128
  Live checks that still need a real account or a person are listed in the
124
- [release verification report](docs/verification/2026-11-01-0.25.0.md) and the
129
+ [release verification report](docs/verification/2026-11-01-0.26.0.md) and the
125
130
  [smoke checklist](docs/smoke.md).
126
131
 
127
132
  ## Documentation
@@ -65,3 +65,15 @@ Background moved out of the area notes for 0.21.0:
65
65
  - Web fetch transport (#553, tests.md): the daemon test's answer waited on iana.org for 99 percent of its time and failed about 3 of 100 runs at 30 parallel on a 4 s bound; a longer wait would only have measured the network.
66
66
  - Jcode usage (#801, budget.md): Jcode's `totalTokens` adds the cached read on top of an input that already holds it for OpenAI-style providers, and `jcode usage --json` names its limit with free text and no window length.
67
67
  - Claim verdicts (#901, tasks.md): review of #937 found `factCheck.agreement` counting person-verdict frequencies, which is why a person verdict alone is never an agreement.
68
+
69
+ - Registered runner integration (#600, daemon.md): the frozen runner contract narrowed issue #1018 from person-or-conductor registration to person-only trust-root changes. Generic machine-local stripping would silently ignore shared/tracked runner keys instead of refusing them, and recursive declaration discovery omitted whole-map scoped replacements after registration existed. The config/recovery fixtures exercise explicit source refusal, atomic whole-block publication, pending raw edits versus accepted revocation, and the separate recovery floor and integrity binding.
70
+
71
+ Issue #1047 records approved tasks that could not accept a new head or a withdrawn verdict on hub 0.23.1. The prior in-review-only rule left the board bound to an older commit while reviewers handled later PR heads in comments. The approval reopening rules in tasks.md replace that restriction; fixture coverage includes a legacy receipt-only approval, a configured check without a prior reviewer, and a PR merged while a screen is pending.
72
+
73
+ Task 610 follows the staged-upgrade review: ordinary startup forgot the scoped target, scoped refresh replaced the shared manager, verified captures accumulated, and an empty plan reached source preservation before its not-running refusal. Runtime selection now starts at the first resolver release (0.26.0); earlier published binaries do not read the pointer, so that ceiling is documented rather than enforced by a broader database floor.
74
+
75
+ Review of PR #1050 found that including accepted runner configuration and raw-key presence in release-equality integrity made pending config edits and legacy no-receipt snapshots fail the release step. Runner configuration is mutable startup policy; receipt rows are retained board state. The runner integrity rule now separates those roles, with two-daemon prepare/commit/release fixtures covering both transitions.
76
+
77
+ Review of PR #1052 found that full-index trust sweeps and hashing every retained capture under the registry writer lock made ordinary startup refuse large repositories or starve other project writers. Per-start scoped trust and bounded quarantine transactions separate execution authority and deletion safety from maintenance cost.
78
+
79
+ The ledger review of PR #1052 found that applying the startup admission wait to worker completion could strand the ledger in running state after contention or a crash. Signed process ownership, an atomic dead-owner replacement, a normal worker wait and read-only doctor paths keep deferred maintenance recoverable and visible.
@@ -8,7 +8,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
8
8
 
9
9
  - Keep per-kind native policy in `src/adapters/definitions/` and consume it through the registry. Private CLI implementations accept only fixed host-bound ports; keep identity, audit, lifecycle admission and effect ordering in the host. Except for the verified person-owned A4 startup-module contract, do not add config-selected callbacks or runtime imports from registry leaves into eagerly initialized hub/CLI modules; use type-only ports for existing host planners.
10
10
  - Adapter registry (#451): kind-specific policy lives in that peer's `AdapterDefinition` under `src/adapters/definitions/` (runtime definitions `NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN` in `builtin-metadata.ts` plus `qwen.ts`; the daemon constructs every native driver through `start.create` and `DAEMON_PORTS`). Outside `src/adapters/`, read a semantic field of `definitionOf(id)`: never a raw peer-id comparison, a resolved kind (`kindOf`, `definitionOf(...).kind`) or a constant alias compared with a native name, a vendor-keyed object, a peer-name array, or a closed-list import from `peer-catalog.ts`, `start-mode.ts` or `permission-mode.ts`; the guard resolves aliases and import/re-export bindings. `test/registry-guard.baseline.json` is `[]` and stays empty: a new site moves into a definition, never into the baseline. The only exceptions are the cross-release literals `KNOWN_PEERS`, `OPTIONAL_PEERS` and `START_MODE_PEERS`, which `test/registry-guard.test.ts` holds equal to the derived lists. `LaunchContext.nativeCommand` carries only validated config argv (or its redacted preview), never model input; a definition's `permission.grants` text is what `permissionGrant` shows, ahead of the built-in wording. Keep native mode-id maps in `definitions/*-native.ts`, free of registry factories (`test/adapter-import-order.test.ts`).
11
- - Registry load runs `assertDaemonEngine` and `assertNativeCapabilityScope` before publication. A definition that is not a fixed builtin (object identity in `fixedBuiltinDefinitions`, never a kind name) may not declare `start.acpCommand`, `attach.admission`, `budget.resetSoftTokensOnResume`, `session.crashIdentity`, `session.nativeRevert`, a `pi-rpc` or `claude-implementer` daemon port, `identity.currentChild`, `routing`, `projection`, `reviewerVariant`, `headlessVariant`, `reviewer`, `hooks.factsHook`, `cli.entry`, `cli.preview`, `cli.usage` or `instances.modelSelect`. Only `definitionOf(id, instances)` picks a kind's `reviewerVariant`, for a declared extra with `start_mode: "headless"`, roles exactly `["reviewer"]`, and either a variant whose `reviewer.declaration` is `"automatic"` (Claude, agy) or `review_mode: "fresh"` (Codex, Kimi); without that opt-in a reviewer-only Codex or Kimi instance keeps its native adapter. It picks a kind's `headlessVariant` (Claude, #525-4) for a declared headless extra whose roles include `implementer` and hold only `implementer` and `verifier`. Metadata-only reviewer variants never establish native peer support; `isSupportedPeer` shares the registry runtime/validated-instance predicate with the catalog and daemon. A new field that depends on fixed-builtin assumptions or host-global native state joins that list (`test/adapter-capability-scope.test.ts`). `trust.onPrem` admits PII work (rule: `tasks.md`).
11
+ - Registry load runs `assertDaemonEngine` and `assertNativeCapabilityScope` before publication. A definition that is not a fixed builtin (object identity in `fixedBuiltinDefinitions`, never a kind name) may not declare `start.acpCommand`, `start.upStart`, `attach.admission`, `budget.resetSoftTokensOnResume`, `session.crashIdentity`, `session.nativeRevert`, a `pi-rpc` or `claude-implementer` daemon port, `identity.currentChild`, `routing`, `projection`, `reviewerVariant`, `headlessVariant`, `reviewer`, `hooks.factsHook`, `cli.entry`, `cli.preview`, `cli.usage` or `instances.modelSelect`. Only `definitionOf(id, instances)` picks a kind's `reviewerVariant`, for a declared extra with `start_mode: "headless"`, roles exactly `["reviewer"]`, and either a variant whose `reviewer.declaration` is `"automatic"` (Claude, agy) or `review_mode: "fresh"` (Codex, Kimi); without that opt-in a reviewer-only Codex or Kimi instance keeps its native adapter. It picks a kind's `headlessVariant` (Claude, #525-4) for a declared headless extra whose roles include `implementer` and hold only `implementer` and `verifier`. Metadata-only reviewer variants never establish native peer support; `isSupportedPeer` shares the registry runtime/validated-instance predicate with the catalog and daemon. A new field that depends on fixed-builtin assumptions or host-global native state joins that list (`test/adapter-capability-scope.test.ts`). `trust.onPrem` admits PII work (rule: `tasks.md`).
12
12
  - Native branch ratchet (#625): `test/daemon-native-branches.test.ts` counts every call of a native class predicate (an `is*Peer` imported from an adapter module other than `registry.ts`) per enclosing function in `src/hub/daemon.ts` and `src/hub/claude-launch-proof.ts`, and the counts must equal `test/daemon-native-branches.json` (`predicate`, `function`, `count`, `reason`). A PR that adds, moves or removes such a call edits its row by hand with a concrete reason; never regenerate the table from a test. Bind a predicate alias with `const` only and never call one through a computed namespace key: the scan refuses both.
13
13
  - Native proof state (`frontendProxy`, builtin `statusNotice`, `revertTurn` with `session.nativeRevert`, the Claude launch proof and terminal execution root) is read in the daemon only after `isFixedBuiltinDefinition(definitionOf(peer))`: a same-name foreign definition gets none of it. A product of a definition that is not a fixed builtin may not have a `frontendProxy`, `statusNotice` or `revertTurn` member at all, even undefined (`assertNativeProofPorts`, run by `createFromDefinition` and the conformance kit). Use `frontendProxyState` for the daemon proxy reads: absence keeps the generic path, present undefined refuses with `frontend proxy state unknown`. Keep approval policies and restoration debt inside CodexPeer. The remaining Codex `actionEnforcement`/`actionGated` predicate is intentional until a separately specified action-policy port; never add it to `FrontendProxyPort`.
14
14
  - Adding a native peer: declare its `AdapterDefinition` under `src/adapters/definitions/` (or its own directory, as agy) and list it in `ADAPTERS` (`src/adapters/registry.ts`). `KNOWN_PEERS`, `OPTIONAL_PEERS`, `SUPPORTED_PEERS`, `START_MODE_PEERS` (a kind with both a TUI and a headless form), `ROLE_ORDER`, `HELP_ORDER` and `PLANNER_PEERS` are derived from it by `deriveAdapterLists`: set `optional`, `projection` (`roleRank`, `startRank`, `helpRank`, `plannerEligible`; a configured or external definition cannot) and, for an `ahub <kind>` launcher, `cli.entry`, `cli.usage`, `cli.preview` and `cli.startWords`, never a list edit (`builtinCliEntries(definitions)` projects the entries). The literal cross-release copies `KNOWN_PEERS` and `OPTIONAL_PEERS` in `src/hub/peer-catalog.ts` and `START_MODE_PEERS` in `src/hub/start-mode.ts` stay hand-kept for older coordinators: `test/registry-guard.test.ts` fails when one differs from the derived list. Adding a peer is the one change that updates their probe counts and `VENDORS` in `test/helpers/registry-inventory.ts` (`REGISTRY_COMPATIBILITY_PROBES`), the probe literal in `test/adapter-registry-a2.test.ts`, the `ADAPTERS` order in `test/adapter-registry.test.ts` and the literal in `test/adapter-catalog-a2.test.ts`; say so in the PR as an expected test change. `isSupportedPeer` answers from `ADAPTERS`, and for a declared instance from its kind's runtime definition unless the instance resolves to an unavailable reviewer variant; `isKnownPeer` and `defaultPeerRoles` answer from `definitionOf`; `STARTABLE_PEERS` reuses `SUPPORTED_PEERS`. It also adds the kind's `kinds` entry and its rows to `templates/presets/confirmation-matrix.json` (#999 T0): `test/confirmation-matrix.test.ts` fails without a tool-approval and a classed-action row per environment, and when the entry's forms, pull-only forms, conductor start or hub-mode fact differ from the definition. Add the same kind to `kinds` in `templates/presets/autonomous.json`, with a permission per form (`null` where the hub mode is `unmanaged`, or for `tui` under `unmanaged-in-tui`) and a start per environment: `test/autonomy.test.ts` fails when the preset and matrix kinds differ or a mode is unsupported.
@@ -18,7 +18,7 @@
18
18
 
19
19
  - Console attention navigation selects a delivery only by an exact visible row id. A missing id or row prints read-only guidance; never select by peer or fall back to the first Queue row.
20
20
 
21
- Scope: the operator console, control WS and its protocol, state files, the dashboard listener and settings, the Claude channel's reconnect, `ahub setup`, git snapshots, upgrade and recovery. Read before editing `src/adapters/definitions/native-recovery*.ts`, `src/adapters/definitions/native-policy-aux.ts`, `src/adapters/definitions/native-config-*.ts`, `src/adapters/definitions/native-cli-*.ts`, `src/cli/console.ts`, `src/cli/console-state.ts`, `src/cli/attention.ts`, `src/ui/needs.ts`, `src/hub/daemon.ts`, `src/hub/ui.ts`, `src/hub/settings.ts`, `src/hub/control-client.ts`, `src/adapters/claude-channel.ts`, `src/hub/restart.ts`, `src/hub/recovery-store.ts`, `src/hub/snapshots.ts`, `src/hub/manager.ts`, `src/hub/lifecycle.ts`, `src/hub/crash.ts`, `src/hub/start-mode.ts`, `src/hub/terminal-open.ts`, `src/hub/recovery-error.ts`, `src/hub/db-timeout.ts`, `src/hub/config-reload.ts`, `src/hub/config-apply.ts`, `src/hub/config-tier.ts`, `src/hub/config-edit.ts`, `src/hub/config-edit-host.ts`, `src/hub/claude-launch-proof.ts`, the session and provenance modules (`src/hub/session-identity.ts`, `src/hub/provenance.ts`, `src/hub/channel-project.ts`), `src/hub/recovery-continuity.ts`, `src/cli/recovery-presentation-options.ts`, `src/cli/setup.ts`, `src/cli/upgrade*.ts`, `src/cli/terminal-recovery.ts`, `src/cli/recovery-package.ts`, `src/cli/facts-hook.ts`, `src/hub/usage.ts`, `src/hub/memory-profile.ts`, the memory code modules (`src/hub/memory-symbols.ts`, `src/hub/memory-code-overlay.ts`, `src/hub/memory-code-head.ts`, `src/hub/memory-stale-anchors.ts`, `src/cli/memory.ts`), the task brief host (`src/memory/brief.ts`, `src/hub/memory-plan-guard.ts`), `src/hub/native-hook-launch.ts`, `src/cli/output.ts`, `src/ui/index.html`, the acon plugin (`src/ui/plugin.ts`, `src/ui/office-space.ts`, `src/cli/acon-register.ts`), `ahub study` (`src/cli/study.ts`, `src/cli/study-output.ts`), `src/hub/task-code-group.ts` (dashboard Code group), the agent drift sweep (`src/hub/agent-drift.ts`, `src/cli/agents.ts`) or the instance modules (`src/hub/instances.ts`, `src/hub/instance-config.ts`, `src/hub/instance-execution.ts`), or before adding a native peer.
21
+ Scope: the operator console, control WS and its protocol, state files, the dashboard listener and settings, the Claude channel's reconnect, `ahub setup`, git snapshots, upgrade and recovery. Read before editing `src/adapters/definitions/native-recovery*.ts`, `src/adapters/definitions/native-policy-aux.ts`, `src/adapters/definitions/native-config-*.ts`, `src/adapters/definitions/native-cli-*.ts`, `src/cli/console.ts`, `src/cli/console-state.ts`, `src/cli/attention.ts`, `src/ui/needs.ts`, `src/hub/daemon.ts`, `src/hub/ui.ts`, `src/hub/settings.ts`, `src/hub/control-client.ts`, `src/adapters/claude-channel.ts`, `src/hub/restart.ts`, `src/hub/recovery-store.ts`, `src/hub/snapshots.ts`, `src/hub/manager.ts`, `src/hub/lifecycle.ts`, the runtime selection and retention modules (`src/hub/project-runtime-state.ts`, `src/hub/project-runtime.ts`, `src/hub/runtime-capture-prune.ts`, `src/hub/runtime-prune-health.ts`, `src/hub/registry.ts`), `src/hub/crash.ts`, `src/hub/start-mode.ts`, `src/hub/terminal-open.ts`, `src/hub/recovery-error.ts`, `src/hub/db-timeout.ts`, `src/hub/db-schema.ts`, `src/hub/protocol-window.ts`, `src/hub/config-reload.ts`, `src/hub/config-apply.ts`, `src/hub/config-tier.ts`, `src/hub/config-edit.ts`, `src/hub/config-edit-host.ts`, `src/hub/claude-launch-proof.ts`, the session and provenance modules (`src/hub/session-identity.ts`, `src/hub/provenance.ts`, `src/hub/channel-project.ts`), `src/hub/recovery-continuity.ts`, `src/cli/recovery-presentation-options.ts`, `src/cli/setup.ts`, the default agent set (`src/hub/start-agents.ts`, `src/cli/start-agents-run.ts`), `src/cli/upgrade*.ts`, `src/cli/runtime-version-warning.ts`, `src/cli/terminal-recovery.ts`, `src/cli/recovery-package.ts`, `src/cli/facts-hook.ts`, `src/hub/usage.ts`, `src/hub/memory-profile.ts`, the memory code modules (`src/hub/memory-symbols.ts`, `src/hub/memory-code-overlay.ts`, `src/hub/memory-code-head.ts`, `src/hub/memory-stale-anchors.ts`, `src/cli/memory.ts`), the task brief host (`src/memory/brief.ts`, `src/hub/memory-plan-guard.ts`), `src/hub/native-hook-launch.ts`, `src/cli/output.ts`, `src/ui/index.html`, the acon plugin (`src/ui/plugin.ts`, `src/ui/office-space.ts`, `src/cli/acon-register.ts`), `ahub study` (`src/cli/study.ts`, `src/cli/study-output.ts`), `src/hub/task-code-group.ts` (dashboard Code group), the agent drift sweep (`src/hub/agent-drift.ts`, `src/cli/agents.ts`) or the instance modules (`src/hub/instances.ts`, `src/hub/instance-config.ts`, `src/hub/instance-execution.ts`), or before adding a native peer.
22
22
 
23
23
  - The plugin bundle is installed apart from the daemon. Breaking a control WS message contract bumps `PROTOCOL` in `control-client.ts`; a new request answered as unknown or an optional field preserves it, as does additive optional public task metadata in `ui_snapshot` that older clients ignore (such as `deps` in #246).
24
24
  - `hub_invoke` over the control WS (#445, #991): the daemon binds each admission to its socket and `rid` with an `AbortController` (`invocationRequests`, at most 64 per connection; a duplicate `rid` refuses). `request_cancel` and socket close abort it, and every check after an await confirms the socket is open and still maps that `rid` to the same controller before any effect. Clients go through `ControlClient.request`, whose `controlRequestTimeout` raises a `hub_invoke` wait to `INVOCATION_CONTROL_TIMEOUT_MS`; never call it with a shorter bound.
@@ -44,6 +44,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
44
44
  - `pet` is console-local presentation (#483): validate it only through `validatePetConfig`, which both `loadConfig` and the reload schema call, and keep it applied live with no authority (`authorizeConfigLeaf`).
45
45
  - Start modes (#269): every start the hub makes itself goes through `hubStart` (`src/hub/daemon.ts`): the dashboard's `start_peer`, the console-role `peer_start`, the conductor's `start` hook and `pi.auto_start`. It takes the mode from `startMode(peer)` and nothing else; never add a caller-chosen mode, flag or command to it, and never fall back to headless when a terminal cannot be opened (refuse with the manual command). The words a terminal runs are assembled there from `process.execPath`, the CLI entry, the project root and `startWords(peer)`. The only continuity exception is a validated Pi bookmark on `pi.auto_start`: append its recorded session file (or its session id when no file was recorded), backend and model; never pass both session selectors or accept caller-written replacements. Recovery and `startPeer` callers that pass a recorded mode keep it. A launcher with no `--mode` takes the daemon's accepted start mode from `status.json` `startModes` (`readControl`) before the file's value. A TUI start of an `owned-acp` peer (Jcode) from the console's `peer_start` opens its terminal even while the ACP owner is online: that terminal is a frontend on the owner. The conductor and the dashboard answer `already` for an attached one (#559). The real terminal providers are wired only in `src/hub/lifecycle.ts` (`DaemonOptions.terminal`): a daemon built without them, as in every test, opens nothing; a test injects a recording provider and never the real one.
46
46
  - A headless start that `hubStart` made for a peer with `capabilities.watch` (Kimi, Qwen) also opens `ahub watch <peer>` through `openWatchPane` (#286 AC10): the same provider, fixed words from `process.execPath`, `CLI_ENTRY`, the project root and the peer id, one per peer per `PEER_START_RETRY_MS`. It runs only after the start succeeded and never fails it: no provider or a failed open reports `watch.command` in the result and a notice, with the provider's own text in the log only. Never open a pane for an `already` result or a declared instance.
47
+ - Default agent set (#608): `start.agents` is read only by the CLI at a person's `ahub up` and by the dashboard manager's start (`afterStart` in `src/hub/manager.ts`), through the console `peer_start` and so through `hubStart` (`src/cli/start-agents-run.ts`). The daemon never reads it: do not add a start from it to daemon startup, restart, recovery, `reloadConfig` or upgrade, and keep the `AGENTHUB_RECOVERY_OPERATION`, `--live-test` and console-caller guards on the `up` body together (each overlaps another, and removing one is caught only because the test removes them all). It is in `ALL_MACHINE_LOCAL` with an empty list kept as a value (`EMPTY_IS_VALUE` in `config-trust.ts`): an empty list means start none, never the default. `src/hub/start-agents.ts` and `config-apply.ts` stay free of file-system imports because the schema reaches the Claude channel bundle. `defaults.json` gained `start.agents`; `writeAutonomyDefaults` and `writeMachineStartAgents` each keep the other half, and any other key still makes the whole file untrusted.
47
48
  - `ahub setup` reads Claude Code's state from `--json` listings and takes one step at a time, re-reading after each; never match paths or names by substring.
48
49
  - A copy of the git index keeps the original's mtime (`snapshot()`): git's racy-entry check compares entries with the index file's time in whole seconds, and a fresh copy makes a same-size edit look clean.
49
50
  - State files that clients read (`status.json`, `control-token`) are written after the port is bound, and `status.json` via temp file + rename.
@@ -56,7 +57,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
56
57
  - Adding a native peer requires testing the command emitted by the actual recovery driver, not only its inspection or launch helpers. Never let a generic non-Codex branch treat a new peer as Claude.
57
58
  - Cross-version recovery must use the authenticated source protocol for prepare/commit/abort and the target protocol after startup. Prove the transition against a real prior release before claiming compatibility.
58
59
  - Preserve `task_cancel` target-feature preflight before source holds, capture or commit. Reuse stored target features only after verifying the same retained root and digest; legacy receipts still capable of commit must obtain feature proof. Do not apply pre-commit compatibility checks to sources already past commit. Every prepare, including the lease renewal before a terminal close and after a resume, sends the operation's stored `targetFeatures` (#478).
59
- - A recovery target-feature floor (`task_cancel`, `invocation` while the invocation ledger has an open (`queued` or `delivered`) row, `peer_instances`, `role_stages` while a declared Claude-kind instance holds `implementer`, `task_planners` while a list/instance planner is configured or `Board.hasPlannerState()` finds a task/release binding or plan-review authorship marker, `release_trains` while a configured train is active or frozen, `reviewer_pool` while a fresh reviewer or `review.pool` is configured, `configured_acp` while the project declares `adapters`, `external_adapter_modules` while external module definitions are loaded, `external_adapter_instances` with `external_adapter_modules` and `peer_instances` while an external module-kind instance is declared, `conductor_recovery` while `Board.hasConductorManualState()` finds open conductor manual-assignment intent, `project_autonomy` while the daemon holds retained autonomy consent or a raw `config.json` or `config.local.json` has a top-level `_autonomy` key (an unreadable file refuses as unknown), `calendar_schedules` while a raw `schedules` entry has a `cron` or `tz` key (#1017: an older target refuses the key at load), `research_records` while `Board.hasResearchState()` or a raw `config.json` or `config.local.json` `research` object names `runners` (unreadable refuses as unknown), `research_dispatch`, `research_dropped_offers`, `research_experiments` and `research_comparisons` while `Board.hasResearchDispatchState()`, `hasResearchDroppedOffers()`, `hasResearchExperimentState()` and `hasResearchComparisonState()` find rows (each research digest joins `integrity()` only when its rows exist), `action_policy_floor` while the raw `config.json` or `config.local.json` names `action_policy.floor`, true or false, even where the key is ignored or normalized away: an older target parses the raw key and rejects it before any trust rule, so check raw file presence, never the effective config (#582, seen twice)) lives in `incompatibleRecoveryTarget`, which every existing check site calls, and its name in `DAEMON_FEATURES`; never check a floor at one site only.
60
+ - A recovery target-feature floor (`task_cancel`, `invocation` while the invocation ledger has an open (`queued` or `delivered`) row, `peer_instances`, `role_stages` while a declared Claude-kind instance holds `implementer`, `task_planners` while a list/instance planner is configured or `Board.hasPlannerState()` finds a task/release binding or plan-review authorship marker, `release_trains` while a configured train is active or frozen, `reviewer_pool` while a fresh reviewer or `review.pool` is configured, `configured_acp` while the project declares `adapters`, `external_adapter_modules` while external module definitions are loaded, `external_adapter_instances` with `external_adapter_modules` and `peer_instances` while an external module-kind instance is declared, `conductor_recovery` while `Board.hasConductorManualState()` finds open conductor manual-assignment intent, `project_autonomy` while the daemon holds retained autonomy consent or a raw `config.json` or `config.local.json` has a top-level `_autonomy` key (an unreadable file refuses as unknown), `calendar_schedules` while a raw `schedules` entry has a `cron` or `tz` key (#1017: an older target refuses the key at load), `research_records` while `Board.hasResearchState()` or a raw `config.json` or `config.local.json` `research` object names `runners` (unreadable refuses as unknown), `research_dispatch`, `research_dropped_offers`, `research_experiments` and `research_comparisons` while `Board.hasResearchDispatchState()`, `hasResearchDroppedOffers()`, `hasResearchExperimentState()` and `hasResearchComparisonState()` find rows (each research digest joins `integrity()` only when its rows exist), `research_runners` under the runner registration rule below, `action_policy_floor` while the raw `config.json` or `config.local.json` names `action_policy.floor`, true or false, even where the key is ignored or normalized away: an older target parses the raw key and rejects it before any trust rule, so check raw file presence, never the effective config (#582, seen twice)) lives in `incompatibleRecoveryTarget`, which every existing check site calls, and its name in `DAEMON_FEATURES`; never check a floor at one site only.
60
61
  - Project autonomy (`autonomyCommand` in `src/hub/daemon.ts`, `writeAutonomyPreset` in `src/hub/settings.ts`): the `autonomy` control message is console-only; its operations come only from `planAutonomy` and `autonomyRemovalKeys`, never from the request; `off` removes the fixed key list and clears consent whatever `_autonomy` says. `_autonomy` is inert metadata, never approval provenance. Retained consent is created only by `createAutonomyConsent(...).confirm` inside the daemon (console `on`, or startup under a trusted `readAutonomyDefaults` opt-in with an exact plan match), never by a CLI or offline writer; a config `never-ask` takes effect only for a peer in `autonomyConfirmed`.
61
62
  - Defer hub notices only for an authenticated recovery `prepare` or `commit` whose target features include `recovery_deferred_hub`, or for a restored daemon. Otherwise `setRecoveryHold(true)` moves the deferred notices into queues before commit records expected integrity, and a bare `fenceRecovery` defers nothing. Abort and lease expiry release the bus hold before clearing the operation; if that release fails, keep the operation and let expiry retry on its 1 s timer (#478).
62
63
  - Resume a prepared `commitSent` source that is unavailable or database-busy inside the existing bounded shutdown poll; never re-send commit, re-prepare or close terminals again on that path. Require confirmed `stopped` before target startup, refuse a replacement instance, and retain the prepared receipt on timeout.
@@ -67,7 +68,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
67
68
  - 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".
68
69
  - 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).
69
70
  - 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).
70
- - 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.
71
+ - An unscoped operation receipt stays `schema: 1` (a scoped one is written at `schema: 3` and never moves; Staged upgrades below). 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.
71
72
  - 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.
72
73
  - 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.
73
74
  - `reattachRecovery` (`src/cli/upgrade.ts`, #463) relaunches only the peer a blocked receipt's `blockedReattachment` names, once per its `reattachments` marker, after the lock, the runner claim and a re-read, and only against the same held target instance and version. The terminal proof is `inspectSavedTerminal` (`src/cli/terminal-recovery.ts`): only an `orphaned` pane without positive connected evidence or `missing` (a typed stale handle and a complete, untruncated inventory) counts as gone; `unknown`, `changed`, an orphaned-but-connected pane or a disconnected-only reading never authorizes a terminal. It counts as reattached only on a `done` restore receipt with the exact recorded session id, session file and project root, then leaves the operation blocked for `recovery resume`. It is person-only: `classifyPeerCommand` and `liveTestRefusal` refuse it from an agent shell.
@@ -137,9 +138,10 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
137
138
 
138
139
  - Campaign dispatch (#597): keep `research_dispatch` additive to `research_records`; any stored campaign requires it, because old campaign stop policies also need current dispatch enforcement. Preserve the conditional dispatch metadata/handoff integrity digest and every other recovery feature guard. A study-dropped owner offer (#603) is a `research_dropped_offers` row written through `Board.recordResearchDroppedOffer` from `Tasks.relevant`, never an in-memory map: a restart between the drop and the approval must still re-offer. It joins `researchDispatchIntegrity` only when rows exist (so a project without one keeps its digest), requires a `research_units` row for its task and a peer-shaped owner, and `studyReoffer` clears it only after the offer is published or the record is stale.
139
140
 
140
- - Experiment storage (#598): preserve `research_experiments` as an additive conditional recovery floor, the experiment-table integrity digest, and all previous research/calendar/autonomy guards. Read check evidence from this project's host reader only; no record input creates a measured field.
141
+ - Experiment storage and runner registration (#598, #600): preserve `research_experiments`, its conditional integrity digest and every earlier research/calendar/autonomy guard. Read check evidence only through this project's host reader; no record input creates a measured field. Accept `research.runners` only in untracked `.agenthub/config.local.json`; refuse shared or tracked raw keys even when empty. Keep registration person-only, with whole-block tier2 structural edits and atomic scoped reload; never admit conductor writes. Consume registrations through `TasksDeps.researchRunners`, revalidating the active accepted configuration and current source trust each call; never adopt raw disk edits through that getter. While stored runner state or an active/raw runner key exists (empty counts), require `research_runners`. Include `researchRunners` in release-equality integrity only when stored receipts exist, using their count and stored-row digest alone; never bind accepted registrations or raw config presence to board-state equality across restart.
141
142
  - `ahub study` (#601, `src/cli/study.ts`, `src/cli/study-output.ts`): `study.ts` is pure argv parsing (ids `rc-` and twelve hex digits, revisions as positive integers, `--reason` takes the rest of the words) and runs before any connection; `main.ts` reads the contract file against `invokedFrom` with a 64 KiB bound and sends it to the hub, which validates it. `show` is `allowed` in `classifyPeerCommand` through `peerReadForms`; draft, revise, approve, approve-protocol and close are `console`, and the audit label names at most the subcommand (approve-protocol is labelled `study`; a campaign id, a protocol hash and a contract path are the person's text). The default output is for a person and `--json` prints the hub's text byte for byte (`test/output-cli.test.ts`); a new `study` form needs its `--json` branch and a renderer, never a `TEXT_DEBT` entry.
142
143
 
143
144
  - Experiment comparisons (#599): preserve the separate conditional `research_comparisons` floor and integrity for plans, exact protocol/evaluator approvals and causal protocol-context rows. Never backfill legacy order from timestamps or carry approval to a changed hash. Keep the approval op under the console-only `study_` dispatch guard.
144
145
  - Doctor memory capture (#1008, #1025): the `memory capture: <tool>` rows come only from `amem setup <tool> --json`, a preview that writes nothing (`readMemorySetup` in `src/cli/main.ts`: no shell, 10 s, 256 KiB), judged by the pure `nativeMemoryCaptureRow` (`src/adapters/definitions/native-cli-doctor.ts`). A plan not in the documented shape, a non-zero exit, a timeout or unparsable output is unknown, never ready; never read dotfiles text or a client's config, and do not scrub the caller's identity before the call.
145
146
  - Handoff transport (#604): `handoffOut` connects to the target as a `console` role through `ControlClient` (same token file and machine user as every control message; the hello checks the target's project root and instance), and the target answers every handoff message, an unknown one included. `handoffNotifyOrigin` hangs off `board.onChange` inside its own try, so a task move never waits for, or fails because of, another project's daemon. A target is judged by its own `status.json` features (`checkTarget`), never by a guess from its version. The inbound `task_handoff` is refused while recovery is active, counts in `taskOpsInFlight` so a recovery commit waits for it, and hands `handoffIn` a fence (recovery or `stopping`) that it asks again right before the board write, after the PII screen's await. Keep `task_handoff` in `DAEMON_FEATURES` and its recovery guard beside `research_experiments`; no `PROTOCOL` bump, because a new message type is answered as unknown by an older hub and an optional field is ignored.
147
+ - Staged upgrades (#1039): retain `status.protocol` at the compatibility floor and publish `protocolMin`/`protocolMax`; negotiate the highest common control version and preserve each attached channel's actual negotiated protocol in recovery metadata. New scoped recovery receipts use schema 3 and per-project holds; keep legacy unscoped receipt behavior. Start ordinary project daemons from verified versioned release captures. Check `hub.db` with `db-schema.ts` before startup effects and every writable opener; advance its marker only with an incompatible format migration, never merely on opening with a newer binary. Before source replacement, controlled targets require `schema_guard`; preserve the retained target evidence of fully committed legacy receipts. Unguarded published binaries remain outside the bootstrap guarantee. Keep project runtime selection in bounded project-bound `runtime.json`; refuse Git-tracked or indeterminate repository state before using or replacing that authority. Updated 0.26+ up/manager/dashboard paths use its verified capture or a named version refusal. Preserve the explicit pre-0.26 published-CLI ceiling with a `ponytail:` note; never raise a DB/bootstrap floor for this selection. Compute path-scoped Git trust once per startup outside the writer lock; reuse its project-bound access for runtime reads and writes. Record selection and synchronous launch under the same registry lock used for capture publication and prune quarantine; confirm the unchanged selection and owner at readiness, and require the internal daemon to execute from that exact root. Scoped recovery must not refresh the shared manager. Prune only verified unreferenced versioned source captures, keep newest three/version, and defer on active recovery, unknown reference inventory or exhausted budgets. Schedule rate-limited maintenance after startup returns; keep only bounded metadata validation and atomic quarantine inside the registry writer lock, with candidate hashing and recursive deletion outside it. Refuse managed execution from reserved `.runtime-prune-*` paths; leave changed quarantine contents undeleted. Keep the 1 ms ledger wait on startup admission only; worker statements use the normal bounded wait. Record PID plus process signature and replace only a proven-gone running owner atomically under the admission transaction, with id/phase-fenced completion. Doctor names uncertain ledger state and leftover quarantine paths without repairing or deleting them. Reject empty recovery plans before source preservation.
@@ -1,6 +1,6 @@
1
1
  # Task board and hub tools
2
2
 
3
- Scope: the task flow, assignment, the state machine and the hub's MCP tools. Read before editing `src/hub/tasks.ts`, `src/hub/board.ts`, `src/hub/routing.ts`, `src/hub/task-sweep.ts`, `src/hub/settled-roles.ts`, `src/hub/conductor.ts`, `src/hub/supervision.ts`, `src/hub/tool-refusal.ts`, `src/hub/hub-tools.ts`, `src/hub/review-brief.ts`, `src/hub/reviewer-pool.ts`, `src/hub/reviewer-pool-config.ts`, `src/hub/release-admission.ts`, `src/hub/main-checkout.ts`, `src/hub/checks.ts`, `src/hub/task-repository.ts`, `src/hub/review-findings.ts`, `src/hub/review-scope.ts`, `src/hub/plan-conformance.ts`, `src/hub/sdlc-metrics.ts`, `src/hub/sdlc-merge.ts`, `src/hub/sdlc-mirror.ts`, `src/hub/no-writes.ts`, `src/hub/workflow.ts`, `src/hub/deliverables.ts`, `src/hub/deliverable-files.ts`, `src/hub/research-campaign.ts`, `src/hub/research-experiments.ts`, `src/hub/ci-watch.ts` or the cross-project handoff (`src/hub/handoff.ts`, `src/cli/handoff-args.ts`, `src/cli/handoff-output.ts`).
3
+ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Read before editing `src/hub/tasks.ts`, `src/hub/board.ts`, `src/hub/routing.ts`, `src/hub/task-sweep.ts`, `src/hub/settled-roles.ts`, `src/hub/conductor.ts`, `src/hub/supervision.ts`, `src/hub/tool-refusal.ts`, `src/hub/hub-tools.ts`, `src/hub/review-brief.ts`, `src/hub/reviewer-pool.ts`, `src/hub/reviewer-pool-config.ts`, `src/hub/release-admission.ts`, `src/hub/main-checkout.ts`, `src/hub/checks.ts`, `src/hub/task-repository.ts`, `src/hub/review-findings.ts`, `src/hub/review-scope.ts`, `src/hub/plan-conformance.ts`, `src/hub/sdlc-metrics.ts`, `src/hub/sdlc-merge.ts`, `src/hub/sdlc-mirror.ts`, `src/hub/no-writes.ts`, `src/hub/workflow.ts`, `src/hub/deliverables.ts`, `src/hub/deliverable-files.ts`, `src/hub/research-campaign.ts`, `src/hub/research-experiments.ts`, `src/hub/research-runner.ts`, `src/hub/task-approval.ts`, `src/hub/ci-watch.ts` or the cross-project handoff (`src/hub/handoff.ts`, `src/cli/handoff-args.ts`, `src/cli/handoff-output.ts`).
4
4
 
5
5
  - Scope every task-place comparison through `TaskRepositories` (`src/hub/task-repository.ts`, #476): explicit `refs.repo`, else a common outside-absolute-ref repository or parent, else this project. Preserve relative display spelling within that key and gate overlap, contradiction, replay and held evidence by equal keys. Warm/cache repository identity at task mutation and startup; never repeat Git reads for unchanged overlap inputs. Set `refs.repo` for work in another repository; repo-only accept metadata never grants execution or file access.
6
6
  - Tool callers are models: MCP `inputSchema` is not enforced on the way in. Normalize at the boundary (`cleanRefs`) before anything reaches the board, and never throw after a board write: a publish or notify that follows a `review()` or `done()` board write goes through the one `releaseNotice` guard, which turns a failed publish into a console notice, drops a failed notify and never retries (#231, #243). An assignment's delivery once its board write is saved (#276, #297): the move is fact, the failed publish is not retried in place (the journal's own recovery covers a storage error, and a retry may duplicate a publish that actually left), and a person looks for a queued or needs_review delivery with `ahub queue list --peer <owner>` (then `ahub queue show <id>`, or the accepted entry in `ahub task show <id>` if the journal is down) before re-sending with `ahub task assign <id> <owner>`. The distinction is made once, in `assignOwner`: a failure after the write prints the one console notice from `undeliveredNotice` right there, once, and throws `AssignmentUndeliveredError` carrying the saved task; callers (`review()`'s repeated rejection, `escalate` and its callers, the ready offer, both relays, the idle sweep, rescreen) decide control flow by the type and print nothing more, and `propose`, `assignTo` and `decline` additionally let the error reach the caller in its turn, which is a person or peer that can act. Two exceptions get no notice, by construction: a row superseded meanwhile, meaning a later history entry that moves or settles the task (another assignment, an unassign, the owner's accept, done, approval, close), while entries that do neither (the hub's quiet bookkeeping: answers, reviewer changes, idle sweeps, check results) leave the assignment undelivered and the notice stands, so the original error goes to the caller as before only on supersession; and a claim's own accepted write and "claimed by" line stay outside the delivery rule (a claim is its own hand-over; its overlap and routing announcements to earlier owners are deliveries and share the wrapper, so a failure there does read as one). A failure before the write stays a plain `Error` (#254), a failed write leaves the ready offer unconsumed (#231), and the notices before the write (waits, no-owner, the reserved-owner line) go through `releaseNotice` too, so those pre-write notices cannot fail their caller.
@@ -11,7 +11,7 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
11
11
  - `assign()` never defaults to the task's current owner, or a decline can only come back to the decliner. A reservation (#207) is the same risk: the reserved owner goes first only through `blocked()`, and a decline or an escalation for reason `rejections` or `manual` records the owner it left in the history entry's `from`, which `excluded()` reads, so no later reroute hands the task back through its reservation.
12
12
  - A named owner that `assign()` refuses is never a silent no-op (#601): `assignTo` passes `refuseNamed` so `assignOwner` throws with the routing refusal reason before any write (pre-effect for tool callers, a plain `Error` for the console), and a proposal's unmet named owner is reported in the propose result through `namedOwnerNote`, read from the stall the proposal recorded. The task stays where it was; never deliver to an offline peer to make the name stick.
13
13
  - Write-class owners (#735): `ownerRoleOfClass` (`src/hub/routing.ts`) is the one derivation of the role that owns a class (`plan` planner, `review` reviewer, everything else implementer); the role ceiling and the owner-admission gate both use it, so never test a fixed list of class names; `explain` and `assign` stay the same function.
14
- - The state machine allows `in_progress -> approved` only for classes without a reviewer; `review()` checks `in_review` itself.
14
+ - The state machine allows `in_progress -> approved` only for classes without a reviewer. Reopening an approval follows the owner/reviewer rules below.
15
15
  - Treat `cancelled` as closed through `isClosedTaskState`, but keep dependency release restricted to approval. Cancellation goes through `Tasks.cancel`, preserves the row and roles, clears its reservation/cohort, sends revision-bound stop notices and writes no outcome memory; never map it to approval or cancel a native turn or worktree. Planner cancellation stays limited to its own proposed/backlog work with no execution evidence (`taskHasWorkEvidence`, the `hub_backlog_return_untouched` test, #812 AC5): an accepted-then-declined untouched task is cancellable, anything with a branch, commit, PR, local check, done, verdict or reopen is person-only and keeps the refusal text. The configured planner may `hub_task_show` any non-backlog task through the conductor's public view (`ConductorHooks.isPlanner`); a staged backlog row stays hidden from agent task tools (#263) and a peer holding both roles takes the audited conductor path. That read grants no hold or assignment.
16
16
  - Capture `TaskOffer.pullOnly` and the exact offer envelope id at publication. Keep unread-offer attention independent of attachment and restart until acceptance, decline, supersession or `offerRead` evidence; never replace that evidence with a generic inbox timestamp or completion state.
17
17
  - Use the daemon's settled role map (`resolveSettledRoles`, `src/hub/settled-roles.ts`, the same resolver reload compares holders with) for planner authority and notice recipients. An explicit planner designation is exclusive; automatic fallback remains runtime-only. Keep ready-unassigned sweep admission consistent with person-unassignment suppression, planner-pull reservation/WIP limits and ordinary routing guards; persist each ladder step before its notice.
@@ -54,7 +54,7 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
54
54
 
55
55
  - 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`.
56
56
  - 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`.
57
- - 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, the board cannot produce an `approved` followed by a `done`. `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.
57
+ - 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.
58
58
  - 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`.
59
59
 
60
60
  - Lifecycle identity (#694): the hub links `sdlc.repo`/`issue` only through `Tasks.linkIssue` (the one issue `planIssueIdentity` resolves) at propose and accept (`linkOnWrite`, #723: a local parse, never a read, never a refusal, advise and enforce only), plan review, done and the groom backfill (`backfillIssueLinks`: first in `groomingDigest`, 32 links and 1 s per call, before the slow metadata reads), and stages a done's PR through `Tasks.identityLink`/`withLink` (the link carries the repository and the commit or branch it was read for, and is dropped when the done's effective repository or head changed during the read; the issue is re-resolved at apply time; `reviewRequestAdmission` compares the board copy through the same `stage` projection) (`openPrFor`: the one open PR whose head is the done's full commit, or with no commit its branch; a full 100-row page or a malformed row is unknown). Persist a done's link only in the admitted done update or `done (checking)`, never before a gate that can still throw `PreEffectToolRefusal` (#254); reapply it after every re-read in `complete()`. Write a first link with `linkedReceipt`, never `sdlcReceipt` (its relink rule would drop red-first and local-gate records); never relink an existing or foreign identity, never parse agent text for a PR, skip PII tasks before any read, and run the groom backfill after its authority checks with its own bound.
@@ -66,7 +66,7 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
66
66
 
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
- - Owner review re-file (#1000): admit a replacement `hub_task_done` while `in_review` 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.
69
+ - 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.
70
70
  - 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.
71
71
  - 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.
72
72
  - 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`.
@@ -111,6 +111,7 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
111
111
  - Research campaign dispatch (#930, #597; `docs/specs/2026-11-01-research-campaigns-design.md` "T2 dispatch and coverage implementation boundary"): a task bound to a campaign unit reaches an owner only through the paths that call `studyDispatchReason` / `studyDispatchCheck` (offer, assign, planner pull, owner change, acceptance, and a queued owner offer re-checked at delivery). Each reads the stored campaign, its deadline and stop limits, and for a hard cap the named budget, synchronously, and `assignOwner` asks again immediately before the owner write with nothing awaited between that read and the write; a new owner write asks the same read there rather than checking earlier. An explicit request refuses before any effect; the hub's own offer holds with `holdReady`. Budget enforcement is the named budget by id (`ExecutionBudget.covers`), never `applies` (any budget), and it only reads: a campaign never resets, copies or writes a counter, and a `kind: "run"` budget stays peer-wide. A read that fails holds, never dispatches, and a hold never touches a native turn already running. `studyShow` returns ids, enums, counts and times, and strips source ids from a private campaign. A later slice that exposes a new dispatch or wire requirement adds its own `DAEMON_FEATURES` label and recovery guard.
112
112
  - 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.
113
113
  - Experiments (#932, #598, #599; spec "Experiment T1 implementation boundary" and "T2 comparison implementation boundary"): `Tasks.experimentRecord` and `experimentShow` have no hub tool or CLI yet; `study_approve_protocol` (`experimentApproveProtocol`, person only) is the one wired op, and a new tool or form needs its 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.
114
+ - 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.
114
115
  - 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.
115
116
  - 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`.
116
117
  - 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.
@@ -11,6 +11,7 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
11
11
  - 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`; native peer children inherit the hub's environment minus the agent markers (`peerChildEnv`).
12
12
  - 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.
13
13
  - 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.
14
+ - 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.
14
15
  - `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`.
15
16
  - `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.
16
17
  - 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).
package/docs/dogfood.md CHANGED
@@ -12,6 +12,9 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
12
12
  | --- | --- | --- |
13
13
  | Distinguishable sessions and sender provenance (#1036, `session_identity`) | on after release and upgrade: a connection that names a peer id but is not that peer's current session is a distinct `<peer>~<origin>` session that can send and read, holds none of the peer's roles and replaces nobody; the channel plugin binds to its own project; every envelope and attach event records the connection. Nothing is refused to a connection that worked before. The recovery label `session_identity` applies once an unsettled delivery holds a session's message | `grep '"session":true' .agenthub/state/events.jsonl` lists every message a foreign session sent, with its minted id and no path or text; a recipient's header shows `from "claude~<id>"` for it; test/identity-provenance.test.ts (AC1, AC3, AC4, recovery label) and test/identity-channel-project.test.ts (AC2, real plugin against a new and an old hub) hold it. Not authentication: a same-user process can always act as the user |
14
14
  | Cross-project task handoff with state follow-back (#604, `task_handoff`) | on after release and upgrade: `ahub task handoff` and `hub_task_handoff` register work for another registered project that runs agent-hub on this machine as an ownerless proposal on its board; the requesting peer is told each state change as a `kind: task` message. Nothing is queued, and the origin has no authority there | `ahub board --handoffs` lists what this project handed over with the last state each target reported; `ahub board` and `ahub task show` in the target name the origin project and peer; test/handoff-daemon.test.ts runs two real daemons (AC1 to AC5 and the recovery label), test/handoff-core.test.ts and test/handoff-board.test.ts hold the pure core and the tables |
15
+ | Staged project upgrades (#1039) | off: available for the person to upgrade projects independently; this development hub is not upgraded by a fixture | test/published-protocol-compat.test.ts uses the published 0.25.0 client/plugin and reverse daemon; test/staged-upgrade.test.ts checks independent holds and scoped effects; test/staged-runtime-daemon.test.ts checks a real release-root daemon after mutable install removal; test/board-schema-guard.test.ts checks named pre-effect refusal and WAL metadata; `test/project-runtime.test.ts` and `test/project-runtime-lifecycle.test.ts` cover updated resolver retention/refusal and real lifecycle/dashboard routing with synthetic captured daemons; `test/runtime-capture-prune.test.ts` covers references, bounded retention and publication races; `test/upgrade-stopped-project.test.ts` verifies no preservation for stopped projects. These fixtures do not prove published pre-0.26 CLI behavior |
16
+ | Approval head binding and re-review (#1047) | off until release and project upgrade; fixture evidence only, this PR does not change the running hub | `test/task-review-refile.test.ts` covers owner re-submit, reviewer withdrawal, legacy verdict heads, merged/cancelled refusals and held-screen races; `test/sdlc-queue.test.ts` and `test/sdlc-merge.test.ts` reject stale approval at queue and dispatch. `ahub task show <id> --json` exposes `approval.head`, `latestHead`, `mismatch` and `current`; verdict history retains `reviewedHead` |
17
+ | Default agent set started by `ahub up` (#608, `start.agents`) | off here: this repository's agents are started by their own terminals; it is a machine-local setting and a committed config cannot turn it on. On for a person who runs `ahub setup` and `ahub up` on a project that lists agents. | each `ahub up` prints one line per agent (started, already attached, or the refusal with the manual command); the `peer_start` replies in `hub.log` and the terminal provider's recorded starts show whether a listed agent attached, and `ahub status` shows its state after |
15
18
  | `ahub down` (counterpart of `ahub up`) | on: an alias of `ahub kill` for stopping this project's hub | the person stops the hub with `ahub down` and `ahub status` then reports it stopped; test/identity.test.ts keeps it person-only (`console`) and labelled `down`, and test/help.test.ts holds its help entry |
16
19
  | Encrypted rooms (#446 X1) | off; source-only crypto/wire, with no broker, room link, invitation CLI or delivery enabled | real WebCrypto vectors and negative wire fixtures; atomic memory fake ports establish ordering only; durable X3 state and the X7 two-machine person receipt remain unverified |
17
20
  | Blind room broker (#446 X2) | off; standalone source/fixture work, no public TLS service or hub room link configured here | fake-client auth/fan-out/revocation/restart, ciphertext-only child-process canary, owner admission/offline room reset, queue caps/TTL/FIFO/migration, safe directory modes, and person-only CLI tests; live proxy/account/X7 receipts remain unverified |
@@ -147,6 +150,7 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
147
150
  | Research dispatch and coverage (#930 T2) | Source/isolated fixtures only; no live research campaign | `test/research-dispatch.test.ts` and projection/recovery fixtures: no expired/revised dispatch, named-budget coverage, handoffs and shared ledger |
148
151
  | Experiment records (#932 T1) | Source and isolated fixtures only; no live research attempt | Schema, storage, Tasks and recovery fixtures: declared/host provenance stays distinct, all attempts counted, replay and privacy fences |
149
152
  | Experiment comparisons and protocol approval (#932 T2) | Fixture only; no scientific reproduction claimed on this repository | `test/research-comparison.test.ts`, `test/research-comparison-tasks.test.ts`, `test/research-protocol-cli.test.ts` and recovery tests verify incompatibility, fresh exact-hash approval, causal post-hoc context and restart identity; failed/cancelled attempts stay counted. |
153
+ | Registered runner receipts (#1018) | source and test-key fixtures only; no external runner or live scientific attempt has been admitted here | `test/research-runner-tasks.test.ts` covers signed binding, immutable replay, revocation/readback, privacy and schema-2 rollback; parser/projection fixtures cover missing fields and sealing negatives; config/recovery fixtures cover person-only trust roots, raw-key floors and integrity. `evidence.runnerAttested` stays separate from declared/host-measured values and reports current key status |
150
154
  | Dirty-submodule notices (#798) | source/fixture coverage; the live writing/general pilot (a workspace with 26 submodules) remains unverified | `ahub turns` shows `<path> (changed, contents unobserved)` for a turn that only edited a submodule; the D1 notice at `hub_task_propose` and `hub_task_accept`; `ahub undo` names what it cannot restore; a project without `.gitmodules` makes the same git calls per turn (counting-shim test) |
151
155
  | Profile scorecard (#799) | source/fixture coverage; live writing/general/research pilot remains unverified | ahub report --dogfood profile rows; pure and reader dev byte goldens, private-text exclusion, research declarations/unknown verdicts and per-source unknown tests; anonymous weekly receipt templates |
152
156
  | Domain packs (#318/#322, #513) | off here: this repository has no pack in `.agenthub/roles`, so `ahub init --profile <pack>` is not used on it; live adoption in another project is unverified | the `ahub init --profile <pack>` preview and apply, the pack validator and the shipped-pack catalogue, covered by test/packs.test.ts (40 mutants killed); adoption would show as a `.pack.json` under `.agenthub/roles` and a `profile` in `.agenthub/config.json` of the adopting project, read from `ahub init --profile` runs. The writing-pack, analysis and ops packs load with shipped roles; test/domain-packs.test.ts executes all three standalone checks on synthetic passing/refusing fixtures. Their notes remain advisory and no check is auto-selected; live domain-workflow use remains unverified |
@@ -1,6 +1,6 @@
1
1
  # Operations guide
2
2
 
3
- This guide describes ahub 0.21.0 and control protocol 16. Live verification
3
+ This guide describes ahub 0.26.0 and control protocols 16 through 17. Live verification
4
4
  results and remaining prerequisites are recorded separately in [the smoke ledger](smoke.md).
5
5
 
6
6
  ## Agent-operated live verification
@@ -1168,14 +1168,18 @@ and start): a committed file must not choose commands the hub runs. Keys that
1168
1168
  are not task classes are ignored with a log line. A hub stopped mid-check leaves
1169
1169
  the task in progress (`check interrupted`); the owner marks it done again.
1170
1170
 
1171
- The owner may call `hub_task_done` again while the task is in review, with a
1172
- different full lowercase commit SHA in `refs.commit` (#1000, #1021). It replaces the
1171
+ The owner may call `hub_task_done` again while the task is in review, or approved with its PR not merged, with a
1172
+ different full lowercase commit SHA in `refs.commit` (#1000, #1021, #1047). It replaces the
1173
1173
  delivered artifact, not a verdict: the reviewer gets a fresh request for the new
1174
1174
  head, the old CI watch is replaced, stale observed, reviewed and merge evidence is
1175
1175
  cleared, and open Important findings, spec acceptance and the rejection count stay.
1176
1176
  A configured completion check first returns the task to in progress until it passes.
1177
1177
  The same head, a malformed SHA, another owner or a running check refuse without
1178
- effect, and a verdict started on the old request no longer applies.
1178
+ effect, and a verdict started on the old request no longer applies. Reopening an
1179
+ approval first reads the PR state with `gh`: a merged PR, an unreadable state or a
1180
+ private task with a linked PR refuses without effect, and an approved task with no
1181
+ reviewer goes to the person. The reviewer may instead withdraw an approval of an
1182
+ unmerged PR with `hub_review` `changes_requested`, which returns the task to its owner.
1179
1183
 
1180
1184
  While the project root checkout is on a branch other than its default
1181
1185
  (`origin/HEAD`, else `main`), finishing a task adds `the main checkout is on
@@ -1520,7 +1524,8 @@ ahub study approve-protocol <64-hex hash> # person only: approve one exact ex
1520
1524
  ids, counts, states and provenance and stubs its text as `[pii]`. Study tool arguments and results are kept out
1521
1525
  of hub-managed memory capture.
1522
1526
  - **Upgrades.** The daemon advertises `research_records`, `research_dispatch`, `research_dropped_offers`,
1523
- `research_experiments` and `research_comparisons`. While the matching rows exist, a restart, upgrade or
1527
+ `research_experiments`, `research_comparisons` and `research_runners`. While the matching rows exist (for
1528
+ `research_runners`, stored runner receipts or any `research.runners` block), a restart, upgrade or
1524
1529
  recovery onto a release without the feature is refused.
1525
1530
 
1526
1531
  ## Hub memory
@@ -2146,6 +2151,7 @@ A guarded file edit that the daemon keeps at its accepted value remains visible
2146
2151
  | Permissions | `permission_modes.<peer>` (the default mode) | `config.local.json` | next peer start |
2147
2152
  | Start | `peers.pi.start_mode`, `peers.codex.start_mode`, `peers.kimi.start_mode`, `peers.jcode.start_mode` (`tui` or `headless`) | `config.local.json` | that peer's next start |
2148
2153
  | Start | `pi.auto_start` | `config.local.json` | next startup/recovery auto-start decision; never starts a peer merely on reload |
2154
+ | Start | `start.agents` (peer list; `none` for an empty list) | `config.local.json` | next use; the next person's `ahub up` reads it |
2149
2155
  | Routing | `routing.stay_switch`, `routing.classes.<class>.peers`, `.escalate_to`, `.route`, `.pi_backend` | `routing.local.toml` | at once |
2150
2156
  | Switches | `research.enabled`, `approvals.notify`, `snapshots.enabled` | `config.local.json` | live, without disturbing an active turn |
2151
2157
  | Switches | `coordination` | `config.local.json` | newly created tasks only |
@@ -3025,6 +3031,95 @@ Agent CLIs update themselves often, and a hub release cannot follow every one. O
3025
3031
 
3026
3032
  ## Upgrade and crash recovery
3027
3033
 
3034
+ ### Side by side project versions (#1039)
3035
+
3036
+ `ahub --project <path|id> upgrade --to <version>` reviews and changes only that
3037
+ project. With no selector, the current project is selected. Its recovery hold is
3038
+ per project, so another project can keep working, start, or perform its own
3039
+ upgrade. The registry and manager still serialize their short ownership writes.
3040
+ Legacy unscoped recovery receipts retain their machine-wide hold and original
3041
+ shared installation steps; finish or dispose those before a new project upgrade.
3042
+
3043
+ From 0.26.0, `ahub up`, manager Start and dashboard Start honor the selected
3044
+ project's `<state dir>/runtime.json`. It binds the project identity to a verified
3045
+ runtime version, root and capture digest. A caller at that version or later uses
3046
+ the retained runtime; an older caller refuses with `this project runs X; use X or
3047
+ later / ahub upgrade`. A scoped upgrade changes only that project's selection and
3048
+ does not refresh the shared manager. The daemon records its claimed instance,
3049
+ and readiness confirms the same selection before returning.
3050
+
3051
+ With no runtime record, the first managed startup captures the invoking package
3052
+ and its exact first-party memory core under
3053
+ `~/.agenthub/releases/<VERSION>/source-<digest>/`. Existing records with invalid
3054
+ bindings or missing/changed captures refuse rather than selecting another
3055
+ package. Runtime selection is machine-local state: a Git-tracked runtime record
3056
+ or indeterminate Git tracking check refuses before launch. Each startup checks
3057
+ only the runtime file's containing paths in Git once, with a bounded deadline;
3058
+ unrelated index entries are not enumerated. Plain directories without a
3059
+ repository remain supported. Capture reuse does not depend on a mutable global installation. Scoped
3060
+ upgrade targets use the same version directory, with the verified registry
3061
+ install kept separately in `registry/`. Recovery uses its authority receipt's
3062
+ exact target path and records that selection through the updated startup path.
3063
+
3064
+ This selector/refusal guarantee starts with 0.26.0. Published CLIs older than
3065
+ 0.26.0 ignore the runtime record. After a scoped upgrade, invoke `ahub up` with a
3066
+ 0.26.0-or-later CLI that is at least the selected project's version; keeping an
3067
+ older global CLI does not retrofit this protection. No database schema or
3068
+ bootstrap floor is raised for runtime selection. Tests of the updated resolver
3069
+ are not receipts from those older published binaries.
3070
+
3071
+ After a confirmed new startup, a separate, rate-limited maintenance worker keeps
3072
+ the newest three source captures per version plus every registered runtime, live
3073
+ manager runtime and retained recovery reference. Metadata snapshots and atomic
3074
+ quarantine use short registry writer transactions. Only removal candidates are
3075
+ hashed; package verification and recursive deletion run outside that lock and
3076
+ outside the startup result. Active recovery, uncertain owners, exhausted
3077
+ inventory budgets or an unreadable/incomplete inventory defer deletion.
3078
+ Only verified versioned `source-<digest>` directories are eligible;
3079
+ registry installs, unversioned coordinator sources, unknown layouts and symlinks
3080
+ remain. Reserved `.runtime-prune-*` quarantine paths cannot launch a managed
3081
+ runtime. Changed or unknown quarantined contents remain undeleted for inspection.
3082
+ Admission is home-wide, at most once per hour with at most three removals per
3083
+ run. `~/.agenthub/runtime-prune.db` records admission and the bounded outcome.
3084
+ Admission stores the worker's PID and process signature. The next admission
3085
+ atomically replaces a worker proven gone; live or uncertain owners remain
3086
+ fenced. Worker completion uses the normal database wait bound, while startup
3087
+ admission keeps its 1 ms contention bound. `ahub doctor` names an uncertain or
3088
+ stuck ledger and leftover quarantine paths without changing either. Leftover
3089
+ containers remain available for inspection even after a dead worker is replaced.
3090
+ Startup still succeeds when maintenance defers. A damaged runtime
3091
+ record/capture requires inspection or restoration.
3092
+ Archiving the record deliberately resets first-use selection; archived copies
3093
+ are not active pruning references.
3094
+
3095
+ A scoped upgrade leaves the global CLI and shared Claude plugin unchanged.
3096
+ Update the CLI separately with `bun add --global @staix/agent-hub@<version>`, then
3097
+ run `ahub setup` for the plugin. `ahub doctor` reports either tool when its known
3098
+ version is older than the selected running daemon; it installs nothing.
3099
+
3100
+ | Runtime | Control protocol window | Legacy status field |
3101
+ | --- | --- | --- |
3102
+ | 0.12.20 through 0.25.0 | 16 only | `protocol: 16` |
3103
+ | 0.26.0 | 16 through 17 | `protocol: 16`, `protocolMin: 16`, `protocolMax: 17` |
3104
+
3105
+ New clients choose the highest common protocol. The runtime supports N-1, so the
3106
+ published protocol-16 client and channel plugin remain usable; unrelated feature
3107
+ capabilities still require their own labels. A disjoint window refuses with 4426.
3108
+ The compatibility-floor `protocol` field remains readable by old clients.
3109
+
3110
+ Guard-aware runtimes read `hub.db` format metadata before startup effects and
3111
+ before writable database opens. A newer incompatible format refuses with
3112
+ `this project was opened by agent-hub X; use X or later`. The format counter
3113
+ advances for incompatible storage changes, not every package release. Controlled
3114
+ recovery refuses a target without `schema_guard` before source replacement. A
3115
+ fully committed legacy receipt continues from retained target identity evidence;
3116
+ new staging gates are not applied retroactively to a target already started.
3117
+ Published binaries from before this guard cannot enforce it; use their matching CLI for bootstrap and never open
3118
+ a newer database directly with an unguarded old binary. New scoped receipts use
3119
+ schema 3 so an older recovery coordinator refuses them rather than applying its
3120
+ machine-wide upgrade steps.
3121
+
3122
+
3028
3123
  On a terminal, one command reviews, applies and follows an upgrade:
3029
3124
 
3030
3125
  ```bash
@@ -3042,8 +3137,8 @@ section is what they run.
3042
3137
 
3043
3138
  Upgrade running projects with the target release's own coordinator. It accepts
3044
3139
  a running source on control protocol 9 (0.6.x), 10 (0.7.0 through 0.12.0),
3045
- 11 (0.12.1 and 0.12.2), 12 (0.12.3), 13 (0.12.4 through 0.12.15), 14 (0.12.16), 15 (0.12.17 through 0.12.19) or 16 (0.12.20 through 0.21.0), and only
3046
- a target on its own protocol, so the target's coordinator fits every supported
3140
+ 11 (0.12.1 and 0.12.2), 12 (0.12.3), 13 (0.12.4 through 0.12.15), 14 (0.12.16), 15 (0.12.17 through 0.12.19) or 16 (0.12.20 through 0.25.0), plus the current 17/16 window, and only
3141
+ a target with a common protocol and the required recovery capabilities, so the target's coordinator fits every supported
3047
3142
  source and carries every recovery fix released up to it. Protocol 8 and older
3048
3143
  (0.5.x and earlier) are refused as `manual-bootstrap-required`. Run from the
3049
3144
  project directory, without replacing the global CLI first:
@@ -3056,7 +3151,7 @@ bunx --package @staix/agent-hub@0.12.43 ahub upgrade --to 0.12.43 --yes
3056
3151
  | Running now | Coordinator to use |
3057
3152
  | --- | --- |
3058
3153
  | 0.6.x (protocol 9) | the target's, through `bunx` as above |
3059
- | 0.7.0 through 0.12.0 (protocol 10), 0.12.1 and 0.12.2 (protocol 11), 0.12.3 (protocol 12), 0.12.4 through 0.12.15 (protocol 13), 0.12.16 (protocol 14), 0.12.17 through 0.12.19 (protocol 15), 0.12.20 through 0.21.0 (protocol 16) | the target's, through `bunx` as above |
3154
+ | 0.7.0 through 0.12.0 (protocol 10), 0.12.1 and 0.12.2 (protocol 11), 0.12.3 (protocol 12), 0.12.4 through 0.12.15 (protocol 13), 0.12.16 (protocol 14), 0.12.17 through 0.12.19 (protocol 15), 0.12.20 through 0.25.0 (protocol 16) | the target's, through `bunx` as above |
3060
3155
  | any supported source, with the installed CLI already at the target | `ahub upgrade` below, which is the same coordinator |
3061
3156
  | 0.5.x or earlier (protocol 8 and older) | not supported: bootstrap by hand with the matching CLI |
3062
3157
 
@@ -3080,11 +3175,10 @@ one queued); and one with the 0.11.0 coordinator from a 0.10.0 hub with
3080
3175
  `local.bash_network` on.
3081
3176
 
3082
3177
  The coordinator verifies and retains the exact target package, preserves its
3083
- own source, and promotes the global CLI only after restored projects pass
3084
- readback.
3178
+ own source, and verifies the selected project before releasing its hold. Legacy
3179
+ unscoped receipts additionally promote the global CLI after project readback.
3085
3180
 
3086
- Once the installed CLI matches the target release, review the current project or all registered
3087
- projects first:
3181
+ Once the installed CLI matches the target release, review the selected project first:
3088
3182
 
3089
3183
  ```bash
3090
3184
  ahub restart --dry-run
@@ -3107,8 +3201,8 @@ ahub recovery dispose [<operation-id>] --stop-and-archive --reason <text>
3107
3201
  `--yes` needs `--to`; a dry run without `--to` plans the latest release. A target
3108
3202
  newer than the CLI is handed to that release's coordinator with the same flags,
3109
3203
  and the command is printed on stderr first. The `recovery` commands take the
3110
- operation that holds the machine's lock when the id is left out, and say `no
3111
- recovery operation holds the lock` when none does.
3204
+ operation that holds the selected project's lock (or a legacy machine hold) when
3205
+ the id is left out, and report no operation when neither exists.
3112
3206
 
3113
3207
  The coordinator commits only once the source is quiet: no turn running, no
3114
3208
  approval pending, no completion check queued or running. It waits up to 10
@@ -5467,3 +5561,26 @@ What comes back: each change is recorded in `ahub board --handoffs`, and an agen
5467
5561
  Refusals, each named with the action: an unregistered target, this very project, a hub that is not running, a request over 8 KiB or with an unknown field, a caller that is not a planner, the conductor or the person, a private (PII) turn, a `[pii]` task named with `--task`, text that matches the target's PII patterns or that the target's screen cannot clear, a target hub in recovery or shutting down (hand it off again after that ends), and an older target hub (it lacks the `task_handoff` feature: run `ahub upgrade` in that project).
5468
5562
 
5469
5563
  Recovery: `task_handoff` is its own recovery label. A project that holds a handoff row, in either direction, refuses a recovery target that lacks it.
5564
+
5565
+ ## The agents `ahub up` starts (#608, agent-hub#1040)
5566
+
5567
+ `ahub up` starts the hub, then asks it to start this project's default agents: `claude` and `codex` unless you chose others. Each goes through the same hub start as the dashboard's Start button and `ahub <peer>` from the console, so the peer's own start mode applies (`peers.<id>.start_mode`), the terminal comes from `terminal.open` or Orca, and a peer that is already attached answers `already attached`. There is no headless fallback: a peer whose TUI cannot be opened is refused with the command to run by hand.
5568
+
5569
+ ```
5570
+ ahub up # the hub, then one line per agent
5571
+ ahub up --no-agents # the hub alone, for this run
5572
+ ahub setup --agents kimi,codex # this machine's choice, once
5573
+ ahub settings set start.agents claude,codex,kimi # this project's own list; none for no agent; inherit removes it
5574
+ ```
5575
+
5576
+ ```
5577
+ claude: started tui (terminal.open), it attaches when its TUI is ready
5578
+ codex: already attached
5579
+ kimi: not started: kimi starts in its TUI and the hub could not open a terminal for it (...). Run ahub kimi in a terminal
5580
+ ```
5581
+
5582
+ - **Where the list comes from, in order:** `start.agents` in the project's `.agenthub/config.local.json` (an empty list starts none), else the machine default `start.agents` in `~/.agenthub/defaults.json`, else `["claude", "codex"]`. `ahub setup` asks once ("Agents to start with ahub up"); `--agents <a,b>` answers it, `--yes` keeps the current choice or the default, and a run with no terminal and neither flag keeps the default and says so. `ahub init` of a new project copies the machine default into its `config.local.json`; a project that has its own value keeps it, and nothing is written into a file git tracks.
5583
+ - **Machine-local and trust tier:** `start.agents` is in `ALL_MACHINE_LOCAL`: a committed `.agenthub/config.json` that sets it is ignored with a notice, because a clone must not open terminals on someone else's machine. It is a settings row in the Start group that raises what the hub does on its own, so it needs a settings session (`ahub ui --settings`) or a person's terminal. `defaults.json` stays owner-only (mode 0600, outside every project root); a `start` entry that is not an object with an `agents` list of allowed peers makes the whole file ignored with the existing notice.
5584
+ - **Who and when:** only a person's own `ahub up` and the dashboard manager's "start" (which counts as one). An agent's `ahub up` is refused by the CLI before this runs. A daemon restart, a crash recovery, a config reload (`start` applies at the next `ahub up`) and the `ahub up` that an upgrade or recovery relaunches (it carries `AGENTHUB_RECOVERY_OPERATION`) start nothing from the list, and the running hub never reads it. `--live-test` starts none either.
5585
+ - **What can be listed:** `claude`, `codex`, `kimi`, `pi`, `local` and `jcode`. `qwen` is refused when saved ("the hub never opens its TUI"), as are `agy`, a declared instance, a Claude channel extra, an unknown id and a duplicate, each with a named reason; the loader and the settings writer say the same words. Pi in the list is the user-facing switch; `pi.auto_start` keeps working for configs that have it.
5586
+ - **Failure is local to one agent:** a refusal, a terminal provider that fails or a request that times out prints its own line and never stops the hub or the other agents. The lines come from the hub's own reply, so a provider's error text stays in `hub.log`.