@staix/agent-hub 0.25.0 → 0.27.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 (87) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +12 -6
  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 +7 -5
  6. package/docs/agent-notes/tasks.md +7 -6
  7. package/docs/agent-notes/tests.md +1 -0
  8. package/docs/dogfood.md +7 -1
  9. package/docs/operations.md +160 -17
  10. package/docs/profiles.md +1 -0
  11. package/docs/quickstart.md +18 -4
  12. package/docs/sdlc.md +3 -3
  13. package/docs/security.md +52 -9
  14. package/docs/specs/2026-09-19-agent-hub-design.md +1 -1
  15. package/docs/specs/2026-11-01-research-campaigns-design.md +21 -7
  16. package/docs/verification/2026-11-01-0.26.0.md +54 -0
  17. package/docs/verification/2026-11-01-0.27.0.md +49 -0
  18. package/docs/verified.json +28 -16
  19. package/package.json +1 -1
  20. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  21. package/plugins/agent-hub/server.js +190 -101
  22. package/src/adapters/channel-peer.ts +4 -2
  23. package/src/adapters/definitions/builtin-metadata.ts +6 -6
  24. package/src/adapters/definitions/qwen.ts +2 -0
  25. package/src/adapters/registry-types.ts +2 -0
  26. package/src/adapters/registry.ts +1 -0
  27. package/src/cli/conductor-recovery.ts +3 -2
  28. package/src/cli/help.ts +9 -8
  29. package/src/cli/identity.ts +5 -5
  30. package/src/cli/main.ts +68 -24
  31. package/src/cli/recovery-package.ts +94 -4
  32. package/src/cli/reset.ts +14 -4
  33. package/src/cli/runtime-version-warning.ts +28 -0
  34. package/src/cli/start-agents-run.ts +125 -0
  35. package/src/cli/study-output.ts +56 -0
  36. package/src/cli/study.ts +26 -3
  37. package/src/cli/task-output.ts +5 -0
  38. package/src/cli/upgrade-interactive.ts +4 -4
  39. package/src/cli/upgrade-runtime.ts +105 -57
  40. package/src/cli/upgrade.ts +90 -28
  41. package/src/hub/action-policy.ts +5 -3
  42. package/src/hub/autonomy-defaults.ts +32 -7
  43. package/src/hub/board.ts +129 -13
  44. package/src/hub/budget.ts +3 -4
  45. package/src/hub/child-process.ts +4 -3
  46. package/src/hub/conductor.ts +3 -4
  47. package/src/hub/config-apply.ts +13 -2
  48. package/src/hub/config-edit-host.ts +1 -0
  49. package/src/hub/config-tier.ts +3 -0
  50. package/src/hub/config-trust.ts +10 -2
  51. package/src/hub/control-client.ts +30 -10
  52. package/src/hub/daemon.ts +103 -26
  53. package/src/hub/db-schema.ts +143 -0
  54. package/src/hub/delivery-journal.ts +3 -4
  55. package/src/hub/events.ts +2 -1
  56. package/src/hub/execution-budget.ts +3 -4
  57. package/src/hub/folder-trigger.ts +3 -4
  58. package/src/hub/hub-tools.ts +7 -5
  59. package/src/hub/invocations.ts +3 -4
  60. package/src/hub/lifecycle.ts +87 -29
  61. package/src/hub/manager.ts +49 -33
  62. package/src/hub/no-writes.ts +1 -1
  63. package/src/hub/orca-env.ts +19 -0
  64. package/src/hub/project-runtime-state.ts +164 -0
  65. package/src/hub/project-runtime.ts +62 -0
  66. package/src/hub/protocol-window.ts +19 -0
  67. package/src/hub/recovery-store.ts +102 -12
  68. package/src/hub/registry.ts +5 -0
  69. package/src/hub/research-experiments.ts +33 -4
  70. package/src/hub/research-runner.ts +172 -0
  71. package/src/hub/restart.ts +9 -0
  72. package/src/hub/runtime-capture-prune.ts +415 -0
  73. package/src/hub/runtime-prune-health.ts +112 -0
  74. package/src/hub/schedules.ts +3 -4
  75. package/src/hub/sdlc-merge.ts +7 -0
  76. package/src/hub/sdlc-queue.ts +13 -1
  77. package/src/hub/sdlc-release-authorization.ts +11 -5
  78. package/src/hub/session-identity.ts +1 -1
  79. package/src/hub/settings.ts +4 -0
  80. package/src/hub/snapshots.ts +3 -4
  81. package/src/hub/start-agents.ts +44 -0
  82. package/src/hub/task-approval.ts +23 -0
  83. package/src/hub/tasks.ts +133 -34
  84. package/src/hub/usage-rollup.ts +3 -4
  85. package/src/pi/tool-receipts.ts +3 -4
  86. package/templates/presets/confirmation-matrix.json +5 -5
  87. package/templates/release-guard.block.md +2 -1
@@ -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.27.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
@@ -1484,6 +1488,7 @@ ahub study approve rc-0123456789ab 2 # person only: create the unit task
1484
1488
  ahub study show [rc-0123456789ab] # list campaigns (50 per page, --after continues) or one projection
1485
1489
  ahub study close rc-0123456789ab 2 --reason "answered"
1486
1490
  ahub study approve-protocol <64-hex hash> # person only: approve one exact experiment protocol
1491
+ ahub study experiments --campaign rc-0123456789ab [--after 50] # read the recorded attempts (also --attempt ra-... or --plan <hash> --baseline ra-... --variant ra-...)
1487
1492
  ```
1488
1493
 
1489
1494
  - **Contract.** The JSON names the question, scope, exclusions, registered source ids, 1 to 32 units
@@ -1514,13 +1519,39 @@ ahub study approve-protocol <64-hex hash> # person only: approve one exact ex
1514
1519
  - **Experiments.** Protocols, comparison plans and attempts are stored with their provenance (`declared`,
1515
1520
  `host-observed` from a check receipt or a hub-hashed deliverable), and `ahub study approve-protocol` binds an
1516
1521
  approval to one exact protocol hash and its evaluator; a changed protocol needs its own approval. The hub
1517
- never ranks results. This release has no hub tool or CLI that records experiments; they are host storage
1518
- for the slices that follow.
1522
+ never ranks results. The current in-progress owner of a bound unit task records inert data through
1523
+ `hub_experiment_record`: `kind` is `protocol`, `plan`, `attempt`, `event` or `receipt`, using the existing
1524
+ protocol/attempt/event/plan envelope or `{ kind: "receipt", receipt, signature }` for a signed runner receipt.
1525
+ The person may relay a signed receipt through the authenticated console connection; this does not grant
1526
+ agents person authority or allow a person to bypass the current task/source/campaign checks. Every PII turn
1527
+ refuses the tool. It returns only record metadata (kind, ids/hashes, sequence and receipt id as applicable),
1528
+ and neither starts an experiment nor signs or verifies a receipt in the relay. Existing host evidence and
1529
+ signature verification remain inside `Tasks`. Read records with `ahub study experiments` or
1530
+ `hub_experiment_show` (see Experiment readback below).
1531
+ - **Experiment readback (#602, #932 T3).** `ahub study experiments --campaign <id>` lists a campaign's attempts
1532
+ (50 rows a page, `--after <cursor>` continues), `--attempt <ra-id>` shows one, and `--plan <hash> --baseline
1533
+ <ra-id> --variant <ra-id>` shows one comparison under a recorded plan. `hub_experiment_show` takes the same three
1534
+ forms and is a read for every local peer. It runs no command and reads no file; it renders what
1535
+ `Tasks.experimentShow` already returns. Every value says where it came from: `declared` (the agent's own word),
1536
+ `host-observed` (a check receipt or a hub-hashed deliverable), `runner-attested` (a signature-verified runner
1537
+ receipt) or `person-attested`. A declared value is never shown as observed, a metric or output the hub could not
1538
+ read is `unknown`, sealing is `declared` and `isolation unverified` unless a runner receipt supplies the
1539
+ evidence, and failed, cancelled and unknown attempts stay in the counts at the top. A repeat is never an
1540
+ independent reproduction, and a comparison whose fixed inputs, evaluator, split or metric units differ, or that
1541
+ changes an undeclared factor, reads `no: <field> differs` and is shown, never ranked. A private campaign, task or
1542
+ source shows ids, counts and states only. **Optional rerun checklist** (written for a person, never run by the
1543
+ hub): a person who wants stronger evidence reruns the protocol themselves, then attaches it through the existing
1544
+ paths and no new one: a check on the task (`hub_check_start`, #513 and the remote runner #793) makes the end
1545
+ event's exit status `host-observed`; an output file named in an `output` event is hashed by the hub (#509) and
1546
+ stays `unknown` when it is missing or denied; a registered runner's signed receipt (#1050) is the only source of
1547
+ `runner-attested` sealing and isolation. Without one of these the experiment is reported as declared and
1548
+ unverified, which is not the same as wrong.
1519
1549
  - **Privacy.** A campaign whose parent task is PII, or that names a private source, is private: every view keeps
1520
1550
  ids, counts, states and provenance and stubs its text as `[pii]`. Study tool arguments and results are kept out
1521
1551
  of hub-managed memory capture.
1522
1552
  - **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
1553
+ `research_experiments`, `research_comparisons` and `research_runners`. While the matching rows exist (for
1554
+ `research_runners`, stored runner receipts or any `research.runners` block), a restart, upgrade or
1524
1555
  recovery onto a release without the feature is refused.
1525
1556
 
1526
1557
  ## Hub memory
@@ -2146,6 +2177,7 @@ A guarded file edit that the daemon keeps at its accepted value remains visible
2146
2177
  | Permissions | `permission_modes.<peer>` (the default mode) | `config.local.json` | next peer start |
2147
2178
  | 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
2179
  | Start | `pi.auto_start` | `config.local.json` | next startup/recovery auto-start decision; never starts a peer merely on reload |
2180
+ | Start | `start.agents` (peer list; `none` for an empty list) | `config.local.json` | next use; the next person's `ahub up` reads it |
2149
2181
  | Routing | `routing.stay_switch`, `routing.classes.<class>.peers`, `.escalate_to`, `.route`, `.pi_backend` | `routing.local.toml` | at once |
2150
2182
  | Switches | `research.enabled`, `approvals.notify`, `snapshots.enabled` | `config.local.json` | live, without disturbing an active turn |
2151
2183
  | Switches | `coordination` | `config.local.json` | newly created tasks only |
@@ -3025,6 +3057,95 @@ Agent CLIs update themselves often, and a hub release cannot follow every one. O
3025
3057
 
3026
3058
  ## Upgrade and crash recovery
3027
3059
 
3060
+ ### Side by side project versions (#1039)
3061
+
3062
+ `ahub --project <path|id> upgrade --to <version>` reviews and changes only that
3063
+ project. With no selector, the current project is selected. Its recovery hold is
3064
+ per project, so another project can keep working, start, or perform its own
3065
+ upgrade. The registry and manager still serialize their short ownership writes.
3066
+ Legacy unscoped recovery receipts retain their machine-wide hold and original
3067
+ shared installation steps; finish or dispose those before a new project upgrade.
3068
+
3069
+ From 0.26.0, `ahub up`, manager Start and dashboard Start honor the selected
3070
+ project's `<state dir>/runtime.json`. It binds the project identity to a verified
3071
+ runtime version, root and capture digest. A caller at that version or later uses
3072
+ the retained runtime; an older caller refuses with `this project runs X; use X or
3073
+ later / ahub upgrade`. A scoped upgrade changes only that project's selection and
3074
+ does not refresh the shared manager. The daemon records its claimed instance,
3075
+ and readiness confirms the same selection before returning.
3076
+
3077
+ With no runtime record, the first managed startup captures the invoking package
3078
+ and its exact first-party memory core under
3079
+ `~/.agenthub/releases/<VERSION>/source-<digest>/`. Existing records with invalid
3080
+ bindings or missing/changed captures refuse rather than selecting another
3081
+ package. Runtime selection is machine-local state: a Git-tracked runtime record
3082
+ or indeterminate Git tracking check refuses before launch. Each startup checks
3083
+ only the runtime file's containing paths in Git once, with a bounded deadline;
3084
+ unrelated index entries are not enumerated. Plain directories without a
3085
+ repository remain supported. Capture reuse does not depend on a mutable global installation. Scoped
3086
+ upgrade targets use the same version directory, with the verified registry
3087
+ install kept separately in `registry/`. Recovery uses its authority receipt's
3088
+ exact target path and records that selection through the updated startup path.
3089
+
3090
+ This selector/refusal guarantee starts with 0.26.0. Published CLIs older than
3091
+ 0.26.0 ignore the runtime record. After a scoped upgrade, invoke `ahub up` with a
3092
+ 0.26.0-or-later CLI that is at least the selected project's version; keeping an
3093
+ older global CLI does not retrofit this protection. No database schema or
3094
+ bootstrap floor is raised for runtime selection. Tests of the updated resolver
3095
+ are not receipts from those older published binaries.
3096
+
3097
+ After a confirmed new startup, a separate, rate-limited maintenance worker keeps
3098
+ the newest three source captures per version plus every registered runtime, live
3099
+ manager runtime and retained recovery reference. Metadata snapshots and atomic
3100
+ quarantine use short registry writer transactions. Only removal candidates are
3101
+ hashed; package verification and recursive deletion run outside that lock and
3102
+ outside the startup result. Active recovery, uncertain owners, exhausted
3103
+ inventory budgets or an unreadable/incomplete inventory defer deletion.
3104
+ Only verified versioned `source-<digest>` directories are eligible;
3105
+ registry installs, unversioned coordinator sources, unknown layouts and symlinks
3106
+ remain. Reserved `.runtime-prune-*` quarantine paths cannot launch a managed
3107
+ runtime. Changed or unknown quarantined contents remain undeleted for inspection.
3108
+ Admission is home-wide, at most once per hour with at most three removals per
3109
+ run. `~/.agenthub/runtime-prune.db` records admission and the bounded outcome.
3110
+ Admission stores the worker's PID and process signature. The next admission
3111
+ atomically replaces a worker proven gone; live or uncertain owners remain
3112
+ fenced. Worker completion uses the normal database wait bound, while startup
3113
+ admission keeps its 1 ms contention bound. `ahub doctor` names an uncertain or
3114
+ stuck ledger and leftover quarantine paths without changing either. Leftover
3115
+ containers remain available for inspection even after a dead worker is replaced.
3116
+ Startup still succeeds when maintenance defers. A damaged runtime
3117
+ record/capture requires inspection or restoration.
3118
+ Archiving the record deliberately resets first-use selection; archived copies
3119
+ are not active pruning references.
3120
+
3121
+ A scoped upgrade leaves the global CLI and shared Claude plugin unchanged.
3122
+ Update the CLI separately with `bun add --global @staix/agent-hub@<version>`, then
3123
+ run `ahub setup` for the plugin. `ahub doctor` reports either tool when its known
3124
+ version is older than the selected running daemon; it installs nothing.
3125
+
3126
+ | Runtime | Control protocol window | Legacy status field |
3127
+ | --- | --- | --- |
3128
+ | 0.12.20 through 0.25.0 | 16 only | `protocol: 16` |
3129
+ | 0.26.0 through 0.27.0 | 16 through 17 | `protocol: 16`, `protocolMin: 16`, `protocolMax: 17` |
3130
+
3131
+ New clients choose the highest common protocol. The runtime supports N-1, so the
3132
+ published protocol-16 client and channel plugin remain usable; unrelated feature
3133
+ capabilities still require their own labels. A disjoint window refuses with 4426.
3134
+ The compatibility-floor `protocol` field remains readable by old clients.
3135
+
3136
+ Guard-aware runtimes read `hub.db` format metadata before startup effects and
3137
+ before writable database opens. A newer incompatible format refuses with
3138
+ `this project was opened by agent-hub X; use X or later`. The format counter
3139
+ advances for incompatible storage changes, not every package release. Controlled
3140
+ recovery refuses a target without `schema_guard` before source replacement. A
3141
+ fully committed legacy receipt continues from retained target identity evidence;
3142
+ new staging gates are not applied retroactively to a target already started.
3143
+ Published binaries from before this guard cannot enforce it; use their matching CLI for bootstrap and never open
3144
+ a newer database directly with an unguarded old binary. New scoped receipts use
3145
+ schema 3 so an older recovery coordinator refuses them rather than applying its
3146
+ machine-wide upgrade steps.
3147
+
3148
+
3028
3149
  On a terminal, one command reviews, applies and follows an upgrade:
3029
3150
 
3030
3151
  ```bash
@@ -3042,8 +3163,8 @@ section is what they run.
3042
3163
 
3043
3164
  Upgrade running projects with the target release's own coordinator. It accepts
3044
3165
  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
3166
+ 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
3167
+ a target with a common protocol and the required recovery capabilities, so the target's coordinator fits every supported
3047
3168
  source and carries every recovery fix released up to it. Protocol 8 and older
3048
3169
  (0.5.x and earlier) are refused as `manual-bootstrap-required`. Run from the
3049
3170
  project directory, without replacing the global CLI first:
@@ -3056,7 +3177,7 @@ bunx --package @staix/agent-hub@0.12.43 ahub upgrade --to 0.12.43 --yes
3056
3177
  | Running now | Coordinator to use |
3057
3178
  | --- | --- |
3058
3179
  | 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 |
3180
+ | 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
3181
  | any supported source, with the installed CLI already at the target | `ahub upgrade` below, which is the same coordinator |
3061
3182
  | 0.5.x or earlier (protocol 8 and older) | not supported: bootstrap by hand with the matching CLI |
3062
3183
 
@@ -3080,11 +3201,10 @@ one queued); and one with the 0.11.0 coordinator from a 0.10.0 hub with
3080
3201
  `local.bash_network` on.
3081
3202
 
3082
3203
  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.
3204
+ own source, and verifies the selected project before releasing its hold. Legacy
3205
+ unscoped receipts additionally promote the global CLI after project readback.
3085
3206
 
3086
- Once the installed CLI matches the target release, review the current project or all registered
3087
- projects first:
3207
+ Once the installed CLI matches the target release, review the selected project first:
3088
3208
 
3089
3209
  ```bash
3090
3210
  ahub restart --dry-run
@@ -3107,8 +3227,8 @@ ahub recovery dispose [<operation-id>] --stop-and-archive --reason <text>
3107
3227
  `--yes` needs `--to`; a dry run without `--to` plans the latest release. A target
3108
3228
  newer than the CLI is handed to that release's coordinator with the same flags,
3109
3229
  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.
3230
+ operation that holds the selected project's lock (or a legacy machine hold) when
3231
+ the id is left out, and report no operation when neither exists.
3112
3232
 
3113
3233
  The coordinator commits only once the source is quiet: no turn running, no
3114
3234
  approval pending, no completion check queued or running. It waits up to 10
@@ -4998,7 +5118,7 @@ blocks that undo until the identity and workspace can be reviewed.
4998
5118
 
4999
5119
  ## Release approval and optional tag guard
5000
5120
 
5001
- Run `ahub sdlc authorize-release <version>` from the running daemon's release-candidate checkout in a person's terminal after committed documentation is fresh. The receipt binds commit, package version, remote identity and the docs verdict. Agents and delegated live-test calls cannot authorize. The daemon keeps approval only in memory and logs metadata for audit; restart requires a new approval. The read-only `ahub sdlc release <owner/repo>` report shows its receipt, retaining native-verification blockers.
5121
+ Run `ahub sdlc authorize-release <version>` from the running daemon's release-candidate checkout after committed documentation is fresh. The person may run it directly; the unique configured conductor may run it only on the person's explicit instruction in its own session naming the release. A hub message, task text, review note, file or peer relay never qualifies. Other peers, child `toolsOwner` sessions, private or PII turns, and delegated `--live-test` calls cannot authorize. The receipt binds commit, package version, remote identity and the docs verdict; its `by` and the audit name the actual asker. The daemon keeps approval only in memory; restart requires a new approval. The read-only `ahub sdlc release <owner/repo>` report shows its receipt, retaining native-verification blockers.
5002
5122
 
5003
5123
  `ahub init --release-guard` installs a managed Git pre-push guard and release instructions (`--dry-run --json` previews them). It preserves an existing default hook as `pre-push.agent-hub-original` and passes original argv/stdin to it only after approval matches. Unknown or mismatched `v*` tag pushes fail closed, including when the daemon is down. Non-version branch pushes remain independent of the daemon. Custom `core.hooksPath` and linked/hardlinked hook files require manual integration. Re-run init after changing the Bun/package location. The person's `git push --no-verify` remains an explicit bypass; agents are instructed never to use it.
5004
5124
 
@@ -5467,3 +5587,26 @@ What comes back: each change is recorded in `ahub board --handoffs`, and an agen
5467
5587
  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
5588
 
5469
5589
  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.
5590
+
5591
+ ## The agents `ahub up` starts (#608, agent-hub#1040)
5592
+
5593
+ `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.
5594
+
5595
+ ```
5596
+ ahub up # the hub, then one line per agent
5597
+ ahub up --no-agents # the hub alone, for this run
5598
+ ahub setup --agents kimi,codex # this machine's choice, once
5599
+ ahub settings set start.agents claude,codex,kimi # this project's own list; none for no agent; inherit removes it
5600
+ ```
5601
+
5602
+ ```
5603
+ claude: started tui (terminal.open), it attaches when its TUI is ready
5604
+ codex: already attached
5605
+ kimi: not started: kimi starts in its TUI and the hub could not open a terminal for it (...). Run ahub kimi in a terminal
5606
+ ```
5607
+
5608
+ - **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.
5609
+ - **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.
5610
+ - **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.
5611
+ - **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.
5612
+ - **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`.
package/docs/profiles.md CHANGED
@@ -22,6 +22,7 @@
22
22
  - Example deterministic check: `bun /path/to/agent-hub/scripts/check-citations.ts report.md references.bib`. It checks bracketed Markdown `[@key; @other]` keys against BibTeX entry keys, with 256 KiB/4 MiB input caps. It performs no fetch, validates no source quality and is not a full Markdown/BibTeX parser. A missing key exits 1. A resolved bibliography is one check, not proof that claims are supported.
23
23
  - Keep a person's sign-off as the final review. The live three-source/two-agent example remains unverified and was not run during solo implementation.
24
24
  - A multi-unit study (#930, #601) goes through `ahub study`: the person (or a configured planner with `hub_study_draft` / `hub_study_revise`) stores a contract as a draft, only the person approves it with `ahub study approve <id> <revision>`, which creates one task per unit, and anyone reads coverage with `ahub study show [<id>]` or `hub_study_show`. A unit owner records `hub_study_handoff` with evidence for every source the unit names, or `insufficientEvidence`; a done task is not a handoff, and an incomplete handoff does not close a discovery round. A private campaign, task or source shows ids, counts and states only.
25
+ - Experiment attempts recorded on a study unit are read with `ahub study experiments --campaign <id>` (or `--attempt`, or `--plan` with `--baseline` and `--variant`) and `hub_experiment_show`. Each value shows its source: `declared` is the agent's own word, and only `host-observed`, `runner-attested` or `person-attested` rows are evidence, none of which proves the science. A rerun by a person attaches through an existing check, a hashed output file or a registered runner's signed receipt, as the checklist in `docs/operations.md` ("Experiment readback") says; without one the experiment stays declared and its sealing unverified. The synthetic CPU-only fixture is `test/research-experiments-fixture.test.ts`; no account, GPU or native model has run an experiment here.
25
26
  - The deterministic three-source, two-peer example (`test/study-daemon.test.ts`, "three-source, two-peer example"): one contract names sources S0, S1 and S2 and two discovery units, `alpha` (S0 and S1, owned by codex) and `beta` (S2, an independent check, owned by kimi). After `ahub study approve` both units read `none` and `0/2 covered`, `0/1 covered`. A handoff for S0 only moves `alpha` to `incomplete` with `1/2 covered`. A handoff for both S0 and S1, and one for S2, make both `complete` and the discovery round complete. Another peer's handoff for `alpha` is refused. This runs with fake peers: it shows the hub's rules, not that a native model follows the guidance.
26
27
 
27
28
  # General project example
@@ -17,7 +17,7 @@ ahub setup # installs the channel plugin and a pr
17
17
  Or use the matching GitHub release:
18
18
 
19
19
  ```bash
20
- bun add -g github:STAIxBWLB/agent-hub#v0.25.0
20
+ bun add -g github:STAIxBWLB/agent-hub#v0.27.0
21
21
  ahub setup
22
22
  ```
23
23
 
@@ -27,8 +27,17 @@ From a clone instead: `git clone`, `bun install`, `bun link`.
27
27
 
28
28
  For managed launchers, two-agent task/review work, approvals, graceful shutdown,
29
29
  and upgrade or crash recovery, see the [operations guide](operations.md). To
30
- move running projects to a newer release later, run `ahub upgrade` in a
31
- terminal: it reviews the plan with you, applies it and follows it.
30
+ move a running project to a newer release later, run `ahub upgrade` in a
31
+ terminal in that project: it reviews the plan with you, applies it and follows
32
+ it; other projects keep their version.
33
+
34
+ ## Upgrading to 0.27.0
35
+
36
+ Nothing changes until you use something new. Release consent can now come from your explicit instruction to the conductor: say in its own session which release to ship (for example "release 0.27.0"), and the configured conductor runs `ahub sdlc authorize-release <version>` for you, under the same admission as `sdlc merge`; you can still run it yourself. The approval keeps every binding it had (exact commit, version, origin and fresh docs; a restart revokes it), its record names who ran it, and a hub message, task text, review note, file or another peer's relay never counts as the instruction (#1063, #1066). Research experiments gain a read and a write: `ahub study experiments` and `hub_experiment_show` read a campaign's attempts, one attempt or one comparison (#602), and an owner records protocols, plans, attempts, events and receipts with `hub_experiment_record`, refused during a private turn (#615). Concurrent starts of one project no longer fail on a `runtime.json` replaced while it was being read (#616). As for 0.26.0, update the global CLI and the plugin yourself afterwards: `bun add -g @staix/agent-hub@0.27.0`, then `ahub setup`.
37
+
38
+ ## Upgrading to 0.26.0
39
+
40
+ `ahub upgrade` now upgrades one project: the one in the current directory, or the one `--project <path|id>` names; other projects keep their running version (#1039, #1045). From a 0.25.0 CLI, run it in the project directory, since 0.25.0 refuses `--project` for upgrade; it hands over to the 0.26.0 coordinator as before. It no longer updates the global CLI or the shared Claude plugin: run `bun add -g @staix/agent-hub@0.26.0` and then `ahub setup` yourself, and `ahub doctor` warns while either is older than a running daemon. The daemon speaks control protocol 17 and still accepts 16, so a 0.25.0 plugin keeps connecting. Each project keeps its verified runtime across stop and start; start it with a 0.26.0 or later CLI, because an older CLI ignores that record (#1052). A daemon refuses a `hub.db` in a newer format and names the version it needs. `ahub up` now also starts `claude` and `codex` (see [First session](#first-session)): `ahub setup` asks once which agents (`--agents <a,b>`, `none` for no agent), `ahub settings set start.agents <a,b>` changes one project, and `ahub up --no-agents` skips them for one run; a restart, recovery or upgrade relaunch starts none (#1046). An owner can re-file `hub_task_done` on an approved, unmerged task with a new head, which sends it back to review, and a reviewer can withdraw an approval with `changes_requested` (#1049). Signed external runner receipts for research attempts are opt-in: the person registers a runner under `research.runners` in untracked `.agenthub/config.local.json` (#1050). Restart Claude sessions so they load the 0.26.0 plugin.
32
41
 
33
42
  ## Upgrading to 0.25.0
34
43
 
@@ -167,9 +176,14 @@ In your project directory, one terminal each:
167
176
 
168
177
  ```bash
169
178
  ahub init # .agenthub/config.json, .agenthub/routing.toml, the marker block in AGENTS.md
170
- ahub up # starts the daemon and opens the operator console in a terminal
179
+ ahub up # starts the daemon, opens claude and codex in terminals (see below), then the operator console
171
180
  ahub console # use in another terminal if up used --no-console; Tab opens panels
172
181
  ahub kimi # Kimi in its own pull-only TUI; --mode headless for the hub-driven peer
182
+ ```
183
+
184
+ `ahub up` also starts a default set of agents, `claude` and `codex` unless you choose others, each in a terminal the hub opens (`terminal.open`, or Orca). It prints one line per agent, and an agent it cannot open is named with the command to run yourself (`ahub claude`, `ahub codex`). Choose the set once with `ahub setup --agents kimi,codex` (or answer the question in `ahub setup`), change one project with `ahub settings set start.agents <a,b>`, and skip it for a run with `ahub up --no-agents`. Without a terminal provider nothing is opened and the hub keeps running; start them by hand:
185
+
186
+ ```bash
173
187
  ahub codex # Codex TUI, attached through the hub
174
188
  ahub claude # Claude Code with the hub channel
175
189
  ```
package/docs/sdlc.md CHANGED
@@ -389,7 +389,7 @@ A private (PII) task reads no eval metadata and records none.
389
389
 
390
390
  ## Person release authorization (#481 S6)
391
391
 
392
- From the running daemon's candidate checkout, the person runs `ahub sdlc authorize-release <version>`. The console-only operation binds approval to the exact current commit, its committed package version, the origin fetch/push identity and fresh committed documentation evidence. It rereads the head and remote after the documentation check. Agent shells, including `--live-test`, cannot grant it. An append-only metadata event records the approval id, commit, version and documentation digest/count. The authoritative ledger is daemon-owned memory: restart revokes every approval; audit history cannot restore one.
392
+ From the running daemon's candidate checkout, the person or the one configured conductor runs `ahub sdlc authorize-release <version>` (#1063). The conductor acts only on the person's explicit instruction in its own session naming that release. A hub message, task text, review note, file or another peer's relay is never that instruction; the daemon cannot verify this cooperative rule. A conductor request uses the tools role and refuses a child `toolsOwner`, private turn or PII context, exactly like `sdlc merge`. Other peers and `--live-test` refuse. Consent binds the exact current commit, its committed package version, origin fetch/push identity and fresh committed documentation evidence, all reread after the docs check. Receipt `by` and the append-only metadata audit identify the actual asker alongside the approval id, commit, version and docs digest/count. The authoritative ledger is daemon-owned memory: restart revokes every approval; audit history cannot restore one.
393
393
 
394
394
  `ahub sdlc release <owner/repo>` shows a matching or stale authorization receipt. Authorization removes only the person-decision blocker. Native acceptance remains unverified and `ready` remains false until separately supported. No tag, workflow or publication is changed by authorization or readback.
395
395
 
@@ -403,13 +403,13 @@ Tracked npm cleanliness compares the cached index to HEAD and raw tracked blob b
403
403
 
404
404
  ## Release preparation and the guarded tag (#787 P3b)
405
405
 
406
- Release preparation is an agent task; the release decision is not. They are two records and two commands, and consent stays person-only.
406
+ Release preparation is an agent task; the release decision remains the person's. Consent may be recorded by the person or by the configured conductor on the explicit version-naming instruction above; preparation alone grants no consent.
407
407
 
408
408
  `bun scripts/release-prepare.ts [--version X.Y.Z] [--dry-run]` is the mechanical half of AGENTS.md "Releasing" steps 1 and 2. On a `release/<version>` branch (it refuses `main`, a detached HEAD, a dirty tree and any other branch name) it reads the commits since the last `v*` tag, applies the rule (any `feat` subject bumps the minor, otherwise the patch; a version below the rule or not newer than the last tag is refused, so is a missing or empty `## Unreleased`), and writes `package.json`, the README `Status:` line, the CHANGELOG section (the Unreleased entries move under the version, Unreleased stays empty), the bundle (`bun run build`) and a verification report skeleton `docs/verification/<date>-<version>.md`. The skeleton lists the pull requests the commits name and the native acceptance receipts, which it reads from `docs/verification/` files added since the tag, never from what an agent writes. It leaves `TODO(agent)` markers for what only a review can state: each PR's review and CI, the document review, the checks and the known gaps.
409
409
 
410
410
  It restamps nothing by itself. A stamp in `docs/verified.json` records that someone read what changed under a document, so `bun scripts/release-prepare.ts restamp <document>... --reviewed` sets `verifiedAgainst` to HEAD for the named documents only, refuses without `--reviewed`, refuses an unnamed or unknown document and refuses with staged changes. The agent runs it per document after reading the source commits, commits `docs/verified.json`, opens the release PR and merges it with a merge commit (never squash).
411
411
 
412
- `bun scripts/release-tag.ts <version> [--execute]` is the one guarded tag run. Every run repeats the whole preflight and reports every refusal at once: a valid release version; `package.json`, the README Status line and a CHANGELOG section at HEAD all naming it; HEAD on `main`, clean, and the merge commit of a `release/<version>` PR (a squash is refused); HEAD an ancestor of `origin/main` (fetched, not assumed); no `v<version>` tag locally or on the remote; `scripts/check-docs.mjs --require-fresh --committed` clean; and exactly one `docs/verification/<date>-<version>.md` with no `TODO(agent)` left. Without `--execute` it only reports, and whether the person's consent matches (exit 2 when it does not). With `--execute` it reads the consent through `ahub sdlc release-guard -- git push <remote> <commit>:refs/tags/v<version>` (read only; the ledger `ahub sdlc authorize-release` fills, person only, revoked by a hub restart), repeats the preflight, checks the commit did not move, reads the consent once more, and then makes the single push of that exact commit. A rejected push is reported and never retried, and the script passes neither `--force` nor `--no-verify`; the pre-push hook from `ahub init --release-guard` still runs on that push. The push creates a lightweight tag (`<commit>:refs/tags/v<version>`), the exact form `ahub sdlc release-guard` parses and the consent receipt binds; the repository's tags are mixed (v0.19.0 and v0.18.0 are annotated, v0.17.0 is not) and `.github/workflows/release.yml` accepts either (`v*`, then `--verify-tag`), so this is a convention difference, not a gap. If the project wants annotated release tags, create the tag object first and push the tag ref, which the guard also accepts (its source object is peeled to the commit); that is a later change to this script. `AHUB_RELEASE_ROOT` points both scripts at a checkout other than the one they live in (the tests use it so nothing outside a scratch repository is touched). Nothing here calls `authorize-release`, and no agent shell can: the report only names the command. Publication (npm, the GitHub release) stays with the tag-triggered workflow, which has its own gate (`.github/workflows/release.yml`).
412
+ `bun scripts/release-tag.ts <version> [--execute]` is the one guarded tag run. Every run repeats the whole preflight and reports every refusal at once: a valid release version; `package.json`, the README Status line and a CHANGELOG section at HEAD all naming it; HEAD on `main`, clean, and the merge commit of a `release/<version>` PR (a squash is refused); HEAD an ancestor of `origin/main` (fetched, not assumed); no `v<version>` tag locally or on the remote; `scripts/check-docs.mjs --require-fresh --committed` clean; and exactly one `docs/verification/<date>-<version>.md` with no `TODO(agent)` left. Without `--execute` it only reports, and whether the person's consent matches (exit 2 when it does not). With `--execute` it reads the consent through `ahub sdlc release-guard -- git push <remote> <commit>:refs/tags/v<version>` (read only; the ledger `ahub sdlc authorize-release` fills from the person or the explicitly instructed configured conductor, revoked by a hub restart), repeats the preflight, checks the commit did not move, reads the consent once more, and then makes the single push of that exact commit. A rejected push is reported and never retried, and the script passes neither `--force` nor `--no-verify`; the pre-push hook from `ahub init --release-guard` still runs on that push. The push creates a lightweight tag (`<commit>:refs/tags/v<version>`), the exact form `ahub sdlc release-guard` parses and the consent receipt binds; the repository's tags are mixed (v0.19.0 and v0.18.0 are annotated, v0.17.0 is not) and `.github/workflows/release.yml` accepts either (`v*`, then `--verify-tag`), so this is a convention difference, not a gap. If the project wants annotated release tags, create the tag object first and push the tag ref, which the guard also accepts (its source object is peeled to the commit); that is a later change to this script. `AHUB_RELEASE_ROOT` points both scripts at a checkout other than the one they live in (the tests use it so nothing outside a scratch repository is touched). Neither script calls `authorize-release`: the report only names the command; the person or the explicitly instructed configured conductor records consent separately. Publication (npm, the GitHub release) stays with the tag-triggered workflow, which has its own gate (`.github/workflows/release.yml`).
413
413
 
414
414
  The scripts and their pure helpers live in `scripts/release-prepare.ts`, `scripts/release-tag.ts` and `scripts/lib/release-plan.ts`; they are maintainer tooling and are not part of the published package. The deterministic recovery barriers of #787 AC3 are not needed by either leg and are not part of this change.
415
415
 
package/docs/security.md CHANGED
@@ -7,7 +7,7 @@ agent-hub connects agents that can each run commands. This page says what the hu
7
7
  - **Other agents' text is untrusted.** Every message that crosses from one peer to another is framed as untrusted input: a channel tag with `meta.source` for Claude started with `ahub claude`; otherwise a fixed header line (`frame()`, `[agent-hub message from ...]`) with an instruction that says it is untrusted, the plugin's server instructions for a tools-only Claude reading `hub_inbox` and a standing instruction for the others. A message body cannot forge the hub's own headers: such lines are quoted (`sanitize`). Replies inherit a hop count capped at 3, so agents cannot ping-pong forever; neither a digest nor a steer can reset it.
8
8
  - **The control link is loopback plus a secret.** The daemon and the Codex proxy bind 127.0.0.1 only. The control WebSocket requires a per-run token (`.agenthub/state/control-token`, mode 600), and both servers refuse any request that carries an `Origin` header: any web page can open a WebSocket to localhost, and browsers always send `Origin`. The bearer header a Codex TUI sends to the proxy carries only the terminal launch id (#586): it binds terminal ownership and is not a credential. External clients cannot claim the console user's id or a hub-managed peer's id.
9
9
  - **Permission prompts stay on unless an operator selects a permission mode.** `ask` leaves native decisions unchanged. `ask-when-needed` grants scoped Claude file tools or Pi/local writes and edits, and a headless Claude implementer's proven shell commands through the host wrapper (#845, below); Kimi and Codex use their own native policies. `never-ask` requires explicit runtime confirmation, a person's console `y` for a config default at hub start, or retained autonomy consent bound to that peer's exact never-ask value in the untracked `config.local.json` (#999, below). Without one of these the peer runs in `ask`. Kimi's and `local`'s permission requests are relayed to the console and cancelled after `approvals.timeout_s` (default 120 s) of silence; the macOS notification for a waiting request carries the peer and the tool name only. The one exception is the hub's own tools (`hub_send` and the task tools, matched by exact name): Kimi's requests for them are approved once without a prompt and logged by name, the same trust Codex gets through `approval_mode` in the hub's config. They invoke hub-owned operations rather than arbitrary file or shell tools, and every call passes the hub's own checks. A call the always-ask policy classes (below) is never approved this way, whatever its title. Identity comes from the exact permission title, or from the earlier tool-call title bound to the same call id and resolved against the configured MCP servers when the permission title contains argument JSON (Qwen). Payload text and unrelated display titles never establish identity. Configured ACP kinds (#451) never get this exception: a request whose title spells a hub tool exactly still goes to the person approval path, and only a mapped native mode changes what the agent itself asks. For ACP agents, a payload longer than the console shows is marked as cut and never offers a session-wide grant. Pi's write, edit, shell and git write requests are relayed the same way and can be answered `Always allow <tool> until Pi restarts`: later calls of that tool by the same Pi start skip the prompt but stay inside the path guard and sandbox. The grant covers every later call of that tool whatever its payload, is kept in memory only, cannot be given from the dashboard, and each call it allows is logged by tool name. A Pi or `local` request whose call was cancelled or whose turn ended is withdrawn at once: a later answer is refused and grants nothing, an `Always allow` included. One shell command runs without a hub approval: the `!command` a person types in the managed Pi TUI. It stays inside the same sandbox and denylist, and the hub takes it only from the verified owner process of a Pi in TUI mode, while Pi is idle, on a reservation that is used once; a model's own bash call never takes that path (issue #253). `--unattended` turns prompts off, says so loudly, and is never the default; a classed call from a hub-owned ACP peer, the local worker or Pi still waits for a person under it.
10
- - **A committed config cannot choose launch commands, credential files, data endpoints, permission modes or a wider sandbox.** The machine-local fields (`kimi_cmd`, `permission_modes`, `codex_bin`, `pi.cmd`, `checks`, `mlx.bin`, `mlx.runtimeDir`, `mlx.modelPath`, `omniroute.urls`, `omniroute.access_hosts`, the `omniroute` key files, `memory.worker_url` (still accepted, ignored since 0.16.0), `local.read_allow`, `local.bash_network`, `local.network_allow`, `local.doc_read`, `local.web_fetch`, `terminal.open`, `tasks.auto_pull`, `tasks.queue_backpressure`, `context.max_assignment_used`, `schedules` (the `every`, `cron` and `tz` forms alike), `folder_triggers`, `corpus.paths`, `invocation`, `instances`, `action_policy`, `rooms`) apply only from a config file git confirms nobody committed (`instances`, `action_policy` and `rooms` are read only from an untracked `.agenthub/config.local.json`; in `config.json` they are ignored even when untracked): `.agenthub/config.json` or `.agenthub/config.local.json`, matched by file identity so no other spelling the file system accepts slips past, and `.agenthub` itself not a committed symlink or submodule. Without a repository, or when git fails, they keep their defaults; an empty value always means the default. "Untracked" is answered by the repository that contains the project: a checkout copied or extracted into an unrelated repository, or into an ignored directory of one, is trusted like your own files. So a cloned repository cannot choose a launch command, a completion check, a gateway to send a key file to, a memory endpoint, a permission mode, the command that opens a terminal, a folder the hub reads, work proposed on a timer, a host a model may fetch from, which of your agents' skills other peers may call, or a wider sandbox. A command in `checks` runs as you, outside the local worker's sandbox, like a git hook. So do the `setup` and `verify` commands of a benchmark suite (issue #251): `ahub bench run <suite.json>` is a person's command, refused from an agent session, and it resets the work tree for each attempt, so it runs only with `bench.enabled`, at the root of a clean repository that tracks nothing under `.agenthub/`. Nothing in `routing.toml` or in task text is ever run. `routing.toml` and the other shared fields still come from the checkout, and they matter: `routing.toml` picks the models the local worker and the hub's inference use at your gateway and can turn the PII constraint off, and roles and budget shape who does what. Review them in a repository you do not trust. The hub's own git calls outside the sandbox (snapshots, facts, config trust, project discovery, memory recall, the bench runner) all use the single hardened implementation in the exact-pinned first-party `@staix/agent-mem/platform` package, re-exported by `src/hub/git.ts`, which disables the programs a repository's configuration can name — the file-system monitor, hooks, external diff, the pager, and the credential and askpass helpers, with prompts failing closed — so a config that ever does get written cannot run code with the hub's rights. Of those, the monitor, hooks and external diff are reachable by commands the hub issues (the planted-program test exercises all three; a `diff --no-index` reads the repo's config, so that call also passes `--no-ext-diff` and `--no-textconv`); the pager is not (every call pipes stdout). Credential and askpass helpers are disabled for project repositories; memory sync alone may use credentials for its own memory repository, without prompts. The cost: `core.fsmonitor=false` also turns off a person's enabled built-in monitor for the hub's own calls, so the per-turn snapshot's `add -A` scans the whole tree (the snapshot Git calls of one burst share a 250 ms budget, each call at most 10 s; a boundary that runs out is recorded as unknown, never as a stale baseline) (#576). git has no global switch for filter drivers, and of the hub's commands only the snapshot's `add`/`restore` would run one (its driver command lives in the protected `.git/config`).
10
+ - **A committed config cannot choose launch commands, credential files, data endpoints, permission modes or a wider sandbox.** The machine-local fields (`kimi_cmd`, `permission_modes`, `codex_bin`, `pi.cmd`, `checks`, `mlx.bin`, `mlx.runtimeDir`, `mlx.modelPath`, `omniroute.urls`, `omniroute.access_hosts`, the `omniroute` key files, `memory.worker_url` (still accepted, ignored since 0.16.0), `local.read_allow`, `local.bash_network`, `local.network_allow`, `local.doc_read`, `local.web_fetch`, `terminal.open`, `tasks.auto_pull`, `tasks.queue_backpressure`, `context.max_assignment_used`, `schedules` (the `every`, `cron` and `tz` forms alike), `folder_triggers`, `corpus.paths`, `invocation`, `start.agents`, `instances`, `action_policy`, `rooms`) apply only from a config file git confirms nobody committed (`instances`, `action_policy` and `rooms` are read only from an untracked `.agenthub/config.local.json`; in `config.json` they are ignored even when untracked): `.agenthub/config.json` or `.agenthub/config.local.json`, matched by file identity so no other spelling the file system accepts slips past, and `.agenthub` itself not a committed symlink or submodule. Without a repository, or when git fails, they keep their defaults; an empty value always means the default. "Untracked" is answered by the repository that contains the project: a checkout copied or extracted into an unrelated repository, or into an ignored directory of one, is trusted like your own files. So a cloned repository cannot choose a launch command, a completion check, a gateway to send a key file to, a memory endpoint, a permission mode, the command that opens a terminal, which agents `ahub up` opens terminals for, a folder the hub reads, work proposed on a timer, a host a model may fetch from, which of your agents' skills other peers may call, or a wider sandbox. A command in `checks` runs as you, outside the local worker's sandbox, like a git hook. So do the `setup` and `verify` commands of a benchmark suite (issue #251): `ahub bench run <suite.json>` is a person's command, refused from an agent session, and it resets the work tree for each attempt, so it runs only with `bench.enabled`, at the root of a clean repository that tracks nothing under `.agenthub/`. Nothing in `routing.toml` or in task text is ever run. `routing.toml` and the other shared fields still come from the checkout, and they matter: `routing.toml` picks the models the local worker and the hub's inference use at your gateway and can turn the PII constraint off, and roles and budget shape who does what. Review them in a repository you do not trust. The hub's own git calls outside the sandbox (snapshots, facts, config trust, project discovery, memory recall, the bench runner) all use the single hardened implementation in the exact-pinned first-party `@staix/agent-mem/platform` package, re-exported by `src/hub/git.ts`, which disables the programs a repository's configuration can name — the file-system monitor, hooks, external diff, the pager, and the credential and askpass helpers, with prompts failing closed — so a config that ever does get written cannot run code with the hub's rights. Of those, the monitor, hooks and external diff are reachable by commands the hub issues (the planted-program test exercises all three; a `diff --no-index` reads the repo's config, so that call also passes `--no-ext-diff` and `--no-textconv`); the pager is not (every call pipes stdout). Credential and askpass helpers are disabled for project repositories; memory sync alone may use credentials for its own memory repository, without prompts. The cost: `core.fsmonitor=false` also turns off a person's enabled built-in monitor for the hub's own calls, so the per-turn snapshot's `add -A` scans the whole tree (the snapshot Git calls of one burst share a 250 ms budget, each call at most 10 s; a boundary that runs out is recorded as unknown, never as a stale baseline) (#576). git has no global switch for filter drivers, and of the hub's commands only the snapshot's `add`/`restore` would run one (its driver command lives in the protected `.git/config`).
11
11
  - **Telemetry holds no bodies.** `.agenthub/state/events.jsonl` (issue #40) records envelope ids, routing and sizes, task ids and states, overlapping paths and token counts, and the task id (with a PII flag) that each usage and token record is attributed to. It never records a message body, a task title or detail, and marks private (PII) envelopes and tasks as such. Budget pauses, resumes and quota resets carry the peer, window, times and the hub's own reason text, and a person's release authorization its commit, version and docs digest. The usage roll-up (#683) folds the usage, token and limit records into `hub.db` and joins issue, PR and release from the board when read; `ahub usage` and the console's Usage panel answer only the console and a configured planner, and a planner not during a private turn. Kimi's token records (#285) are counted from its own session wire log under its home: only the four token integers, the model label and the row time are copied, and the record id is an opaque hash, never a path, session id or text. The log stays on the machine; `ahub export` only prints it. Two opt-in stores copy numbers from it into your home directory, shared by every project on the machine (directories mode 700, files 600): research records (issue #247, `research.enabled`) in `~/.agenthub/research/` hold per approved task the ids, states, counts, tokens, times and PII flag the events hold, a person's label from a closed list, and a hash in place of the project id; benchmark runs (issue #251) in `~/.agenthub/bench/` hold suite name and hash, arm label, the attached peers' ids, models and permission modes, task ids, outcomes, exit codes, counts, tokens and times, never suite text or a command's output.
12
12
  - **Snapshots stay in your repository.** Per-turn snapshots (issue #33) are git objects in the project's own object store, written through a temporary index; nothing is referenced, pushed or copied elsewhere, and `git gc` prunes them. They hold what the work tree held, including untracked files that are not ignored, so keep secrets in ignored files. They carry the repository's own permissions, and nothing caps their disk use but `git gc`. A turn of a peer holding an open PII task is not snapshotted; a PII file left in the project is snapshotted by later turns like any other file. `ahub undo` restores only files whose current content is exactly what the turn left.
13
13
  - **The edit hook reads, never decides.** `ahub check-path --hook` (issue #32) reads hub.db and returns context for Claude and a line for you; it sets no permission decision, so your permission rules stay in charge. It names other owners' task ids, titles and states, which then reach Claude's model; PII tasks are left out.
@@ -819,7 +819,7 @@ The review brief (#520) withholds private changes whole (both sides of a move) a
819
819
 
820
820
  Deliverable records (#509a). `hub_task_done` and `hub_sdlc_record` name files as `{path, format, version?}`; the hub hashes them itself and refuses a claimed `sha256`, `hash` or `bytes` and any field it does not own. Paths resolve under the owner's execution root with `realPath` and `guardPath` in read mode: outside the project, the secrets denylist, the hub's own state (`.git`, `.agenthub`), a dangling link, a non-regular file and more than 256 MiB each or 512 MiB together are refused, with the path as given and never a resolved one. The file is opened without following a final link and its device, inode, size and time are compared with what was checked, before and after the read, so a swapped or changing file is refused rather than recorded half-read; a bad call writes nothing. A declared worktree instance whose root is not proven refuses, and a later read of it reports `unknown`. No hash of hub state is ever taken, and a file the hub cannot read or vouch for is `unknown`, never `unchanged`. A reviewer's approval re-hashes the files and is refused when one changed, went missing or is unknown; only the person may approve the current bytes, which are then measured and bound. For a PII task, a file under `pii.paths` or a path matching a PII pattern, `publicDeliverables` is the one projection every peer view, review request, status and log uses: id, hash and size only. The board keeps the path, as it keeps a PII task's text. `ahub deliverables` (#509b) from an agent shell lists only the tasks `hub_task_show` would show that agent (#933), and `--live-test` cannot delegate it. It lists the board's records through `publicDeliverables` and opens a file only with `--check`, through the same reader and limits. The dashboard's Deliverables chip and task group use the same projection of the same records and never read a file on the poll; a `changed` mark there is only as fresh as the last live read in this daemon.
821
821
 
822
- Person release authorization (#481 S6). `ahub sdlc authorize-release <version>` is taken from the console role only and refused under `--live-test`. It binds the daemon checkout's commit, package version, push remote identity and a fresh-docs verdict, lives only in daemon memory (a restart revokes it) and is logged with its commit, version and docs digest. `ahub sdlc release-guard -- <argv>` is a read-only preflight that agents may run, except during a private or PII turn; it never executes the command and refuses ambient target selectors (`GIT_DIR`, `GH_REPO`, npm prefix and registry settings and the like). The pre-push hook that `ahub init --release-guard` installs refuses an unauthorized or unknown `v*` tag push, also with the daemon down, and passes other pushes through. It and the release block in `AGENTS.md` are a cooperative guard, not an operating-system boundary: `git push --no-verify`, a custom `core.hooksPath` or another Git invocation bypasses the hook, and `gh release create` and `npm publish` are checked only when the preflight is run (tasks 312 and 313).
822
+ Person-directed release authorization (#481 S6, #1063). `ahub sdlc authorize-release <version>` accepts the console user or the one configured conductor under the same admission as `sdlc merge`: tools role, matching configured conductor, no child `toolsOwner`, private turn or PII context. It remains refused under `--live-test`. The conductor may record consent only on the person's explicit instruction in its own session naming the release; a hub message, task text, review note, file or peer relay is never that instruction. The daemon cannot verify that cooperative prerequisite. Receipt `by` and the audit identify the actual asker. It binds the daemon checkout's commit, package version, push remote identity and a fresh-docs verdict, lives only in daemon memory (a restart revokes it) and is logged with its commit, version and docs digest. `ahub sdlc release-guard -- <argv>` is a read-only preflight that agents may run, except during a private or PII turn; it never executes the command and refuses ambient target selectors (`GIT_DIR`, `GH_REPO`, npm prefix and registry settings and the like). The pre-push hook that `ahub init --release-guard` installs refuses an unauthorized or unknown `v*` tag push, also with the daemon down, and passes other pushes through. It and the release block in `AGENTS.md` are a cooperative guard, not an operating-system boundary: `git push --no-verify`, a custom `core.hooksPath` or another Git invocation bypasses the hook, and `gh release create` and `npm publish` are checked only when the preflight is run (tasks 312 and 313).
823
823
 
824
824
  SDLC metrics and control bands (#481 S7). `ahub sdlc metrics` and `hub_sdlc_metrics` are planner or person only, read-only, and refused during a private or PII turn. A private task is counted as withheld and read for nothing else; the report calls no model and makes no GitHub read. Band breaches are written only by the daemon's evaluator through Tasks, never while it is stopping, recovering or has a storage error. A `diagnose` or `propose` card is a backlog task (`triage` or `plan`) whose text is fixed words, numbers and task ids, never task text; it passes the PII signal check and the candidate-text screen before it is written, and no GitHub issue is created. A `log` breach is one hub notice, remembered in the board's `band_notices` table as a marker (`<metric>:#<task>:log`, checked against a fixed pattern) and a time, nothing else.
825
825
 
@@ -848,16 +848,17 @@ role admission.
848
848
  contract's `budget.hardCap` is checked against that exact budget id at approval
849
849
  and again at every dispatch and owner change; another budget covering the task
850
850
  proves nothing. A hold never stops a running native turn.
851
- - **Provenance is never upgraded.** Experiment fields are `declared` unless the hub
852
- measured them itself from a finished check receipt for the same task and commit,
853
- or by hashing a deliverable through the #509 path policy; an agent-supplied hash
854
- is never host evidence. `ahub study approve-protocol` binds approval to one exact
851
+ - **Provenance stays separate.** Experiment declarations remain `declared`. Separate
852
+ `host-observed` values come from a finished check receipt for the same task and
853
+ commit, or from hashing a deliverable through the #509 path policy; an
854
+ agent-supplied hash is never host evidence. `ahub study approve-protocol` binds approval to one exact
855
855
  protocol hash and evaluator identity hash, and nothing copies it to a changed
856
856
  protocol. Environment descriptors are refused when they contain assignment
857
857
  forms, Bearer credentials or known secret shapes; that screen detects those forms
858
- only and does not prove a secret is absent. This release exposes no hub tool for
859
- recording experiments, and runner attestation (`research.runners`) is not
860
- implemented yet.
858
+ only and does not prove a secret is absent. Signed external receipts use the
859
+ separate `runner-attested` level described below; their fields never overwrite
860
+ declared or host-measured evidence. Public experiment recording tools and the
861
+ receipt CLI are the separate #602 slice.
861
862
  - **Privacy.** A campaign whose parent task is PII, or that names a private source,
862
863
  is private, and so is an attempt with a private task, input or output path.
863
864
  Public views keep ids, counts, states and provenance and stub every question,
@@ -869,6 +870,48 @@ role admission.
869
870
  `research_*` feature is refused while campaign, dispatch, re-offer, experiment or
870
871
  comparison rows exist, or while `research.runners` is configured.
871
872
 
873
+ ### Registered external runner receipts (#1018)
874
+
875
+ A runner registration is a trust root, not permission to execute a command. The
876
+ person registers an Ed25519 SPKI public key, allowed project ids and a revocation
877
+ flag under `research.runners` in untracked `.agenthub/config.local.json`. Shared,
878
+ tracked or untrusted sources refuse even an empty block. Whole-block structural
879
+ edits require the person's tier2 confirmation; the conductor cannot register a
880
+ key. The hub stores no private key, contacts no runner and schedules no compute.
881
+
882
+ Receipt admission verifies the domain-separated canonical body, signature,
883
+ current accepted registration and exact project/attempt/protocol binding. A
884
+ commit-based protocol also requires an explicitly supplied commit claim to match.
885
+ The current owner or the person may relay a receipt; signature authority never
886
+ bypasses current task, source, campaign, privacy or request authority. All checks
887
+ are repeated after asynchronous screening before any write. An unknown/revoked
888
+ key, foreign project, changed identity or tampered signature refuses before write.
889
+
890
+ Stored fields are `runner-attested`, including claimed timestamps, exit status,
891
+ image, network, resources and file hashes. Missing fields remain unknown. A signed
892
+ commit for a deliverable-based protocol stays an independent claim with unknown
893
+ code match. Sealing is attested only with distinct candidate/evaluator containers,
894
+ valid mount references and nonempty evaluator code/data mounts that are all
895
+ read-only and absent from candidates. Other receipts retain declared/unverified
896
+ sealing. This is a signed runner statement, not host observation or proof of
897
+ scientific correctness, isolation enforcement or evaluator secrecy.
898
+
899
+ An identical canonical receipt and signature with a known receipt id returns its
900
+ stored proof without re-verification or writes, including after revocation. A
901
+ changed body/signature at that id refuses; every new id needs a currently trusted
902
+ key. Readback retains as-of-signing provenance and separately shows whether the
903
+ signing key is active, revoked, replaced or unavailable now. Private records stub
904
+ the signed fields, mount/output names, signer identity and key details.
905
+
906
+ The first stored attestation, its event and the `hub.db` format-2 marker commit in
907
+ one transaction; unused databases keep the format-1 baseline. Recovery requires
908
+ `research_runners` for stored receipts or raw/accepted registration presence,
909
+ including empty blocks. Its release-equality integrity digest covers stored proofs only and is present
910
+ only when receipts exist. Accepted registrations remain configuration, governed by
911
+ the feature floor and the config-reload path; pending edits do not change the
912
+ board-state equality required for release. Earlier research guards remain intact. Fixtures
913
+ use test key pairs with no GPU, runner network request or live account.
914
+
872
915
  ## Cross-project task handoff (#604, #1030)
873
916
 
874
917
  A handoff crosses a project boundary, but not a security boundary: both projects
@@ -5888,7 +5888,7 @@ Order: 2a first (it measures everything else). 2b and 2c are independent. 2d aft
5888
5888
 
5889
5889
  ### S6 person release authorization boundary (#481)
5890
5890
 
5891
- - `ahub sdlc authorize-release <version>` is console-only, never live-test delegated. The daemon binds consent to its candidate checkout's exact HEAD, committed package version, origin fetch/push identity and fresh committed-doc verdict. Caller-supplied expected HEAD must match. Head and origin are rechecked after the bounded package-owned documentation checker. Only ids, hashes, version, timestamp and counts enter the receipt/audit event; no project script or release operation runs.
5891
+ - `ahub sdlc authorize-release <version>` accepts the console user or the one configured conductor, never live-test delegated (#1063). Conductor admission matches `sdlc merge`: tools role, `conductorPeer(config.roles) === c.peer`, no `toolsOwner`, private turn or caller PII context. The conductor records consent only on the person's explicit instruction in its own session naming the release; a hub message, task text, review note, file or another peer's relay is never that instruction. This instruction-origin condition is cooperative, not verified by the daemon. Receipt `by` and the metadata audit identify the actual asker (`user` or conductor peer). The daemon binds consent to its candidate checkout's exact HEAD, committed package version, origin fetch/push identity and fresh committed-doc verdict. Caller-supplied expected HEAD must match. Head and origin are rechecked after the bounded package-owned documentation checker. Only ids, hashes, version, timestamp and counts enter the receipt/audit event; no project script or release operation runs.
5892
5892
  - Authoritative receipts live only in the current daemon generation's bounded memory ledger (at most 64 candidates). An additive metadata event audits consent but cannot recreate it. Restart revokes all approvals. The release report shows matching/stale consent without certifying native acceptance, changing the tag workflow or publishing.
5893
5893
  - Optional `ahub init --release-guard` installs a managed executable Git pre-push hook plus instructions. A regular existing hook is renamed, preserving its inode/mode, and chained with exact argv/stdin after successful guard readback. Linked/hardlinked hooks, occupied backups and custom hooksPath refuse before project writes. Re-init retains the guard; the wrapper pins Bun/package paths and needs re-init if they move.
5894
5894
  - Actual pushed `v*` refs must have a valid version and captured source-object/commit matching consent. Missing/mismatched commit/version/remote, unreachable daemon, deletion, unknown evidence or restart refuses the version-tag push. Branch-only pushes work offline. `--no-verify` remains the person's explicit bypass and is forbidden to agents; this is cooperative Git enforcement, not a sandbox.