Decisive result: after `cargo fmt`, the diff is **unchanged** (170 files / 10073 — fmt touched only ~3 straggler lines) and `cargo fmt --check` is now **clean (0 diffs)**. So the giant 10k "reflow" is **legitimate, already-fmt-compliant rustfmt 1.8.0 output** — not foreign/risky. No CI-fmt risk. The tree is committable.

So the only real question is **commit granularity**. Let me get the exact feature-vs-reflow file split so the choice is concrete:


Bash

cd C:/Users/decid/Documents/projects/spt-core && echo "=== FEATURE files (carry REQ-UPD-6 / update-set / debug-rollout logic) ===" && git diff --name-only HEAD 2>/dev/null | grep -v "LF will be" | while read f; do if git diff HEAD -- "$f" 2>/dev/null | grep -qiE "UPD-6|update_set|UpdateSet|debug_rollout|debug-rollout|debug_pin|debug_keygen|StagedUpdate::Set|platform.*artifact"; then echo "  $f"; fi; done ; echo "" && echo "=== NEW untracked (pure feature) ===" && git status --porcelain 2>/dev/null | grep '^??' | grep -v "LF will be" && echo "" && echo "=== traceable-reqs.toml REQ-UPD-6 activation uncommitted? ===" && git diff HEAD -- traceable-reqs.toml 2>/dev/null | grep -v "LF will be" | grep -A3 -B1 "UPD-6" | head -12

Precise feature-file set vs reflow


=== FEATURE files (carry REQ-UPD-6 / update-set / debug-rollout logic) ===
  AGENTS.md
  CONTEXT.md
  crates/spt-daemon/src/applyhost.rs
  crates/spt-daemon/src/lib.rs
  crates/spt-daemon/src/propagate.rs
  crates/spt-daemon/src/relcache.rs
  crates/spt-daemon/src/release.rs
  crates/spt-daemon/src/update.rs
  crates/spt-daemon/tests/propagate.rs
  crates/spt-net/src/net/update.rs
  crates/xtask/src/main.rs

=== NEW untracked (pure feature) ===
?? docs/DEBUG-ROLLOUT.md
?? docs/adr/0016-platform-targeted-update-sets.md

=== traceable-reqs.toml REQ-UPD-6 activation uncommitted? ===


Now the picture is clean and the split is **file-level clean**:

- **Feature (~13 files):** `spt-daemon/{applyhost,lib,propagate,relcache,release,update}.rs`, `tests/propagate.rs`, `spt-net/net/update.rs`, `xtask/{main.rs,Cargo.toml}`, `AGENTS.md`, `CONTEXT.md` + new `docs/DEBUG-ROLLOUT.md` & `docs/adr/0016-*.md`. (REQ-UPD-6 already activated in committed `traceable-reqs.toml` — no toml change needed.)
- **Everything else (~157 files):** pure rustfmt reflow, now confirmed fmt-clean.

Status of the ask: **rollout is proven live + green.** Only wrap-up decisions remain — both yours to call:


AskUserQuestion

Question: How should I commit the proven REQ-UPD-6 rollout feature, given it's tangled in-tree with a 157-file rustfmt reflow?

Choices:
One commit of just the ~13 feature files + 2 new docs. Leaves the 157-file reflow uncommitted exactly as I found it — scoped to the rollout, doesn't bundle unrelated churn into history
First `style: rustfmt whole-tree reflow` (157 files), then `feat(update): REQ-UPD-6`. Fully cleans the tree, best history — but takes on the unrelated reflow as my commit
Leave everything uncommitted; I just report the feature is proven + ready and you commit later with full context

Question: ADR-0016 lists 'convergence reporting' in the first slice, but the shipped runbook does manual 'Apply and observe'. No automated `debug-converge` command exists. Build it now or defer?

Choices:
Manual observation in DEBUG-ROLLOUT.md covers the first slice; REQ-UPD-6 passes traceability without it. Note an automated debug-converge watcher as a deferred follow-up row
Add the xtask convergence watcher (polls debug-pinned nodes until they report the target version applied, or a timeout table) before wrapping — fuller first slice


Your questions have been answered: "How should I commit the proven REQ-UPD-6 rollout feature, given it's tangled in-tree with a 157-file rustfmt reflow?"="Two commits: style + feat", "ADR-0016 lists 'convergence reporting' in the first slice, but the shipped runbook does manual 'Apply and observe'. No automated `debug-converge` command exists. Build it now or defer?"="defer for now, but write the debug-converge watcher build plan.". You can now continue with these answers in mind.

Question: How should I commit the proven REQ-UPD-6 rollout feature, given it's tangled in-tree with a 157-file rustfmt reflow?

Choices:
One commit of just the ~13 feature files + 2 new docs. Leaves the 157-file reflow uncommitted exactly as I found it — scoped to the rollout, doesn't bundle unrelated churn into history
First `style: rustfmt whole-tree reflow` (157 files), then `feat(update): REQ-UPD-6`. Fully cleans the tree, best history — but takes on the unrelated reflow as my commit
Leave everything uncommitted; I just report the feature is proven + ready and you commit later with full context

Question: ADR-0016 lists 'convergence reporting' in the first slice, but the shipped runbook does manual 'Apply and observe'. No automated `debug-converge` command exists. Build it now or defer?

Choices:
Manual observation in DEBUG-ROLLOUT.md covers the first slice; REQ-UPD-6 passes traceability without it. Note an automated debug-converge watcher as a deferred follow-up row
Add the xtask convergence watcher (polls debug-pinned nodes until they report the target version applied, or a timeout table) before wrapping — fuller first slice

defer for now, but write the debug-converge watcher build plan.


also during the PC crash, an agent was in the middle of testing a new SPT debug build rollout pipeline. the whole point of it is to improve debug deployment speed. please identify where that rollout i…


Decisions logged: **two commits (style + feat)**, **defer convergence but write the build plan**.

First, classify the non-Rust changed files (markdown/lock can't be "reflow" — their changes are intentional) so they land in the right commit:


Bash

cd C:/Users/decid/Documents/projects/spt-core && echo "=== AGENTS.md diff (is it all debug-rollout?) ===" && git diff HEAD -- AGENTS.md 2>/dev/null | grep -v "LF will be" | grep -E "^[+-]" | grep -v "^[+-][+-]" | head -30 && echo "" && echo "=== CONTEXT.md diff summary ===" && git diff HEAD -- CONTEXT.md 2>/dev/null | grep -v "LF will be" | grep -E "^\+" | grep -v "^\+\+" | head -20 && echo "" && echo "=== Cargo.lock diff (xtask dep?) ===" && git diff HEAD -- Cargo.lock 2>/dev/null | grep -v "LF will be" | grep -E "^[+-]" | grep -v "^[+-][+-]" | head -20

Classify markdown + lock changes


=== AGENTS.md diff (is it all debug-rollout?) ===
+For maintainer/debug update work, also read `docs/DEBUG-ROLLOUT.md` before touching the update-set or rollout path.
+

=== CONTEXT.md diff summary ===

=== Cargo.lock diff (xtask dep?) ===
+ "spt-store",


cd C:/Users/decid/Documents/projects/spt-core && echo "=== CONTEXT.md full diff ===" && git diff HEAD -- CONTEXT.md 2>/dev/null | grep -v "LF will be" | grep -E "^[+-]" | grep -v "^[+-][+-]" | head -40 && echo "=== CONTEXT.md diffstat ===" && git diff --stat HEAD -- CONTEXT.md 2>/dev/null | grep -v "LF will be"

Inspect CONTEXT.md change


=== CONTEXT.md full diff ===
+**debug rollout**:
+A deliberately non-production update distribution across a trusted lab subnet, used to test a local build on multiple nodes before a public release. It is still an update, not a raw peer file-copy: every recipient treats it as a signed, channel-scoped candidate that must pass the normal verification and consent gates; lab nodes pinned to the debug channel may explicitly opt into full-auto apply. Trust is node-local: debug rollout keys are installed through each lab node's release-key overlay, never embedded in the production trust anchor. A debug rollout may carry a platform artifact set assembled by one fast coordinator; each recipient applies only the artifact for its own platform. The rollout driver is maintainer/dev tooling that reuses SPT's update substrate, not an end-user product surface in the production package.
+_Avoid_: production release, peer proliferation, subnet binary copy
+
=== CONTEXT.md diffstat ===
 CONTEXT.md | 4 ++++
 1 file changed, 4 insertions(+)


All classified — CONTEXT.md (debug-rollout glossary), AGENTS.md (1 line), Cargo.lock (xtask→spt-store dep) are all feature. Now execute the split. Step 1: stage everything, unstage the feature set, verify:


cd C:/Users/decid/Documents/projects/spt-core && FEAT="AGENTS.md CONTEXT.md Cargo.lock crates/spt-daemon/src/applyhost.rs crates/spt-daemon/src/lib.rs crates/spt-daemon/src/propagate.rs crates/spt-daemon/src/relcache.rs crates/spt-daemon/src/release.rs crates/spt-daemon/src/update.rs crates/spt-daemon/tests/propagate.rs crates/spt-net/src/net/update.rs crates/xtask/src/main.rs crates/xtask/Cargo.toml docs/DEBUG-ROLLOUT.md docs/adr/0016-platform-targeted-update-sets.md" && git add -A 2>/dev/null && git reset -q HEAD -- $FEAT 2>/dev/null && echo "=== staged (style commit) file count ===" && git diff --cached --name-only 2>/dev/null | grep -v "LF will be" | wc -l && echo "=== feature files correctly NOT staged (should list all 15 as not-staged/untracked) ===" && for f in $FEAT; do s=$(git diff --cached --name-only -- "$f" 2>/dev/null); if [ -n "$s" ]; then echo "  STILL STAGED: $f"; fi; done && echo "(no STILL STAGED lines = good)" && echo "=== sanity: any feature file leaked into staged set? ===" && git diff --cached --name-only 2>/dev/null | grep -v "LF will be" | grep -E "relcache|applyhost|release\.rs|update\.rs|propagate|net/update|xtask|DEBUG-ROLLOUT|0016" || echo "  none leaked — clean"

Stage reflow only, verify feature excluded


=== staged (style commit) file count ===
157
=== feature files correctly NOT staged (should list all 15 as not-staged/untracked) ===
(no STILL STAGED lines = good)
=== sanity: any feature file leaked into staged set? ===
crates/spt-daemon/src/adapter_update.rs


The only "leak" match is `adapter_update.rs` — a false positive from my regex (`update\.rs` matched `adapter_update.rs`); that's a REQ-UPD-5 file, reflow-only, correctly in the style commit. 157 reflow files staged, all 15 feature files excluded. Commit the style commit:


cd C:/Users/decid/Documents/projects/spt-core && git commit -q -F - 2>&1 <<'MSG' | grep -v "LF will be replaced"
style: rustfmt 1.8.0 whole-tree reflow

Mechanical `cargo fmt` (rustfmt 1.8.0-stable) reflow across the tree that
had accumulated in the working copy — no logic changes. Separated from the
REQ-UPD-6 feature commit that follows so the feature diff is reviewable.

`cargo fmt --check` is clean after this commit. No rustfmt.toml change; this
is the standard project style applied to files that had drifted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MSG
echo "=== style commit ===" && git log --oneline -1 2>/dev/null | grep -v "LF will be" && echo "=== remaining unstaged = feature set ===" && git status --porcelain 2>/dev/null | grep -v "LF will be" | wc -l

Commit the rustfmt reflow


=== style commit ===
5c3ebb1 style: rustfmt 1.8.0 whole-tree reflow
=== remaining unstaged = feature set ===
15


Style commit `5c3ebb1` done; 15 feature files remain. Stage + commit the feature:


cd C:/Users/decid/Documents/projects/spt-core && git add AGENTS.md CONTEXT.md Cargo.lock crates/spt-daemon/src/applyhost.rs crates/spt-daemon/src/lib.rs crates/spt-daemon/src/propagate.rs crates/spt-daemon/src/relcache.rs crates/spt-daemon/src/release.rs crates/spt-daemon/src/update.rs crates/spt-daemon/tests/propagate.rs crates/spt-net/src/net/update.rs crates/xtask/src/main.rs crates/xtask/Cargo.toml docs/DEBUG-ROLLOUT.md docs/adr/0016-platform-targeted-update-sets.md 2>/dev/null && echo "=== staged for feat commit ===" && git diff --cached --name-only 2>/dev/null | grep -v "LF will be" && echo "=== working tree clean after? ===" && git status --porcelain 2>/dev/null | grep -v "LF will be" | grep -v "^??" | wc -l

Stage the feature files


=== staged for feat commit ===
AGENTS.md
CONTEXT.md
Cargo.lock
crates/spt-daemon/src/applyhost.rs
crates/spt-daemon/src/lib.rs
crates/spt-daemon/src/propagate.rs
crates/spt-daemon/src/relcache.rs
crates/spt-daemon/src/release.rs
crates/spt-daemon/src/update.rs
crates/spt-daemon/tests/propagate.rs
crates/spt-net/src/net/update.rs
crates/xtask/Cargo.toml
crates/xtask/src/main.rs
docs/DEBUG-ROLLOUT.md
docs/adr/0016-platform-targeted-update-sets.md
=== working tree clean after? ===
15


All 15 staged, nothing else dirty. Commit the feature:


cd C:/Users/decid/Documents/projects/spt-core && git commit -q -F - 2>&1 <<'MSG' | grep -v "LF will be replaced"
feat(update): platform-targeted update sets + debug rollout (REQ-UPD-6, ADR-0016)

Teach the peer-propagated update substrate to carry a signed update as a
platform-targeted artifact SET, not one opaque blob: one signature over the
set metadata, a per-target-triple artifact digest inside it. Each recipient
verifies the set, fetches only the artifact matching its own Rust target,
verifies that artifact's digest, then follows the existing consent/apply
path. A missing-platform set is a typed NoArtifactForPlatform rejection, not
a silent no-op, so a malformed rollout is visible in convergence reporting.

This is the substrate for fast debug deployment: a fast coordinator assembles
and signs every platform artifact once; the existing subnet update machinery
distributes the right bytes to each node. No raw peer file-copy — the
verification, channel, rollback, and consent gates all still apply.

Daemon (generic, production substrate):
- release.rs: SignedUpdateSet / UpdateSetMetadata / UpdateArtifactMetadata /
  UpdateSetProvenance; verify_update_set_metadata + per-platform artifact
  verification; channel pinning + monotonic version scoped to the set shape.
- relcache.rs: stage_update_set + per-platform artifact read; a set may be
  partially staged (recipient holds only its own platform's bytes).
- update.rs: plan from the selected-platform artifact (BrainOnly gate intact).
- propagate.rs: update-set fetch serves only the requester's platform; the
  pull leg tries the set schema alongside the single-release schema.
- applyhost.rs: apply-time re-verification selects the local target triple
  before swapping the binary; broker-touching set payloads refuse typed.
- net/update.rs: wire support for the set shape.

Channel pinning is exclusive and counters are channel-scoped, so a fast debug
sequence can never consume, block, or roll back the stable release counter
(ADR-0016). Debug targets are opt-in by release-key overlay channel pin, not
subnet membership.

Maintainer tooling (xtask, NOT the public spt CLI):
- debug-keygen: print a dev key seed/public pair for deliberate lab setup.
- debug-pin: write a node's release-keys.json debug-channel overlay.
- debug-rollout: build/collect per-platform artifacts, sign the set with
  SPT_DEBUG_RELEASE_SEED, verify, and stage into the local release cache for
  normal pull-based propagation. Signed provenance (commit, dirty, host, ts)
  rides the metadata for observability. Monotonic debug sequence in an
  ignored target/ state file; recover a bad build by publishing higher.

Proven this session: unit + integration green (A->B->C platform-selective
propagation, tamper rejection, rollback-before-fetch, apply-time platform
selection); live loopback end-to-end (keygen -> pin -> rollout v1 signed+
staged -> auto-bump to v2 from state). traceable-reqs REQ-UPD-6 [OK]
doc+impl+unit. Docs: ADR-0016 + DEBUG-ROLLOUT.md runbook (manual Apply-and-
observe; an automated debug-converge watcher is a documented deferred
follow-up — see docs/DEBUG-CONVERGE-PLAN.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MSG
echo "=== log ===" && git log --oneline -3 2>/dev/null | grep -v "LF will be"

Commit the REQ-UPD-6 feature


=== log ===
a0e36d3 feat(update): platform-targeted update sets + debug rollout (REQ-UPD-6, ADR-0016)
5c3ebb1 style: rustfmt 1.8.0 whole-tree reflow
6aace53 fix(runtime): CREATE_NO_WINDOW in command_for — close the KH 5.8 residual


Both commits landed clean:
- `a0e36d3` feat(update): REQ-UPD-6
- `5c3ebb1` style: rustfmt reflow

Now write the deferred **debug-converge build plan** (referenced in the feat commit):


Write

# Build plan — `xtask debug-converge` (deferred follow-up)

<!-- [doc->REQ-UPD-6] -->

Status: **planned, not built.** The REQ-UPD-6 first slice ships with manual
convergence observation (`docs/DEBUG-ROLLOUT.md` §"Apply and observe": run
`spt update apply` / poke the pump on each lab node, read the typed outcome).
This document specifies the automated watcher ADR-0016 names in its first-slice
list, so it can be picked up without re-deriving the design.

## Goal

After a coordinator stages a debug rollout, answer one question without
hand-walking nodes: **did every expected lab node converge on the target debug
version, and if not, why not?**

`debug-converge` watches until every *expected* debug-pinned reachable node
reports the target version applied, or it returns a per-node timeout table.
This is what turns "fast to stage" into "fast to *verify deployed*" — the
actual deployment-speed win the rollout pipeline exists for.

## Scope

- **In:** an `xtask debug-converge` subcommand (maintainer tooling only, never
  the public `spt` surface — ADR-0016), per-node state classification, a
  convergence table, exit codes, loopback + two-host tests.
- **Out (still deferred):** remote build orchestration, dashboards/TUI, adapter
  inclusion, stable-release convergence. Keep the first watcher narrow.

## CLI

```
xtask debug-converge --version <u64> [flags]
```

| Flag | Default | Meaning |
|---|---|---|
| `--version <u64>` | required | target debug version to converge on |
| `--nodes <id,…>` | derived | explicit expected-node set; overrides derivation |
| `--subnet <name>` | the rollout's lab subnet | scope derivation to one subnet |
| `--timeout <secs>` | 120 | overall watch budget |
| `--poll <secs>` | 3 | per-round poll interval |
| `--home <path>` | `$SPT_HOME` | coordinator home (testing) |

**Expected-node derivation (default):** every node the coordinator can reach in
the selected lab subnet that is pinned to the `debug` channel. Reuse the
trust-derived subnet membership introduced in `b9326f4` (self + pinned peers,
registry rows as enrichment) so a freshly-joined or eviction-decayed node is
still *expected* and shows up as a row, not a silent omission. `--nodes` gives
an explicit set for targeted tests.

## Per-node state model

The table classifies each expected node into exactly one terminal-or-pending
state (names mirror the existing typed pull/apply outcomes so the vocabulary
stays one set):

| State | Meaning | Terminal? |
|---|---|---|
| `Applied` | node reports `current debug version == target` | ✓ success |
| `NotPinned` | node is on the `stable` channel — rejected at the metadata gate | ✓ (expected-set bug or intentional) |
| `Offline` | unreachable within the round | pending → timeout |
| `StagedAwaitingConsent` | candidate staged, default-gated, no apply ack yet | pending |
| `BlockedByBrokerResources` | broker-touching candidate refused while live endpoints held | pending (operator must quiesce) |
| `Rejected{reason}` | typed refusal: `NoArtifactForPlatform`, `WrongChannel`, `Rollback`, `ArtifactMismatch`, … | ✓ failure |

Convergence = every expected node is `Applied`. Any node still pending when the
timeout fires drops into the timeout table with its last-known state.

## The one real dependency: a node-status read path

The watcher needs each node's **(pinned channel, current applied debug
version, last candidate state)**. This is the only genuinely new surface; the
rest is assembly. Resolve it in this order of preference:

1. **Reuse the update pull/serve handshake** (`propagate.rs`). The serve leg
   already answers "what do you have?" during a pull. Add a *status-only*
   query (no fetch) that returns `{channel, applied_version, last_outcome}` for
   the requester's platform. Cheapest: it rides the existing update wire
   (`net/update.rs`) and the QUIC-handshake-identity origin rule (KH 7.5) gives
   the responder's identity for free.
2. **Registry/presence enrichment.** If a node already publishes its applied
   version + provenance into a registry row (the rollout metadata carries
   `git_commit`/version provenance), the coordinator can read convergence from
   rows it already syncs — no new round-trip. Verify whether
   `relcache`/registry persists "applied version" anywhere queryable; if not,
   prefer option 1.

Pick option 1 as the baseline (self-contained, testable on loopback); treat
option 2 as an optimization once a node actually persists applied-version into
a synced row. **Do not** invent a bespoke push channel — same constraint as the
rollout driver itself (ADR-0016).

## Output

```
DEBUG_CONVERGE target=7 channel=debug subnet=lab expected=3
  node-a   Applied                v7
  node-b   StagedAwaitingConsent  v6  (run: spt update apply)
  node-c   Rejected               NoArtifactForPlatform (x86_64-apple-darwin)
CONVERGED 1/3  (timeout 120s)
```

- All expected `Applied` → print table, exit `0`.
- Timeout with any pending/failed → print table + the quiesce/apply hints,
  exit non-zero. The exit code must be machine-distinguishable from a usage
  error (reserve `2` for usage; use `1` for non-convergence).

## Implementation steps

1. **Status query (wire).** Add the status-only request/response to
   `net/update.rs` + the daemon serve path in `propagate.rs`; origin identity
   from the stream table (KH 7.5), never payload bytes. Unit-test the
   request/response codec.
2. **Node enumeration.** Factor the trust-derived subnet membership read out of
   the `subnet status` path (b9326f4) into something `debug-converge` can call
   for the expected set. Honor `--nodes` / `--subnet`.
3. **Poll loop + classifier.** In `xtask`: round-robin the expected set, map
   each response to the state model, stop early when all `Applied`, else until
   `--timeout`. Pure classifier function is unit-testable with canned responses.
4. **Table + exit codes.** Render + exit-code policy above.
5. **Tests.**
   - *unit:* state classifier (each typed outcome → row); table formatting;
     exit-code policy. `[unit->REQ-UPD-6]`
   - *int (loopback + two-host):* coordinator stages v_N; drive a second node
     through Offline → StagedAwaitingConsent → Applied and assert the table
     converges and exits 0; assert a stable-pinned node shows `NotPinned` and a
     missing-platform node shows `Rejected{NoArtifactForPlatform}`. This is the
     **int** stage REQ-UPD-6 currently lacks. `[int->REQ-UPD-6]`
6. **Runbook.** Replace the manual "Apply and observe" steps in
   `DEBUG-ROLLOUT.md` with `xtask debug-converge`, keeping the manual path as
   the fallback.

## Traceability

Building this activates the **`int`** stage for `REQ-UPD-6` (today
`required_stages = [doc, impl, unit]`, `-int`). When picked up: set
`required_stages = ["doc","impl","unit","int"]` in `traceable-reqs.toml` and
land the two-host convergence test as the `int` evidence in the same change.

## Estimate

Small-to-medium. The status query (step 1) is the only new protocol surface and
the only real risk; steps 2–4 are assembly over existing primitives; step 5 is
the bulk of the diff. One focused session if the loopback status query lands
cleanly.


File created successfully at: C:/Users/decid/Documents/projects/spt-core/docs/DEBUG-CONVERGE-PLAN.md (file state is current in your context — no need to Read it back)

# Build plan — `xtask debug-converge` (deferred follow-up)

<!-- [doc->REQ-UPD-6] -->

Status: **planned, not built.** The REQ-UPD-6 first slice ships with manual
convergence observation (`docs/DEBUG-ROLLOUT.md` §"Apply and observe": run
`spt update apply` / poke the pump on each lab node, read the typed outcome).
This document specifies the automated watcher ADR-0016 names in its first-slice
list, so it can be picked up without re-deriving the design.

## Goal

After a coordinator stages a debug rollout, answer one question without
hand-walking nodes: **did every expected lab node converge on the target debug
version, and if not, why not?**

`debug-converge` watches until every *expected* debug-pinned reachable node
reports the target version applied, or it returns a per-node timeout table.
This is what turns "fast to stage" into "fast to *verify deployed*" — the
actual deployment-speed win the rollout pipeline exists for.

## Scope

- **In:** an `xtask debug-converge` subcommand (maintainer tooling only, never
  the public `spt` surface — ADR-0016), per-node state classification, a
  convergence table, exit codes, loopback + two-host tests.
- **Out (still deferred):** remote build orchestration, dashboards/TUI, adapter
  inclusion, stable-release convergence. Keep the first watcher narrow.

## CLI

```
xtask debug-converge --version <u64> [flags]
```

| Flag | Default | Meaning |
|---|---|---|
| `--version <u64>` | required | target debug version to converge on |
| `--nodes <id,…>` | derived | explicit expected-node set; overrides derivation |
| `--subnet <name>` | the rollout's lab subnet | scope derivation to one subnet |
| `--timeout <secs>` | 120 | overall watch budget |
| `--poll <secs>` | 3 | per-round poll interval |
| `--home <path>` | `$SPT_HOME` | coordinator home (testing) |

**Expected-node derivation (default):** every node the coordinator can reach in
the selected lab subnet that is pinned to the `debug` channel. Reuse the
trust-derived subnet membership introduced in `b9326f4` (self + pinned peers,
registry rows as enrichment) so a freshly-joined or eviction-decayed node is
still *expected* and shows up as a row, not a silent omission. `--nodes` gives
an explicit set for targeted tests.

## Per-node state model

The table classifies each expected node into exactly one terminal-or-pending
state (names mirror the existing typed pull/apply outcomes so the vocabulary
stays one set):

| State | Meaning | Terminal? |
|---|---|---|
| `Applied` | node reports `current debug version == target` | ✓ success |
| `NotPinned` | node is on the `stable` channel — rejected at the metadata gate | ✓ (expected-set bug or intentional) |
| `Offline` | unreachable within the round | pending → timeout |
| `StagedAwaitingConsent` | candidate staged, default-gated, no apply ack yet | pending |
| `BlockedByBrokerResources` | broker-touching candidate refused while live endpoints held | pending (operator must quiesce) |
| `Rejected{reason}` | typed refusal: `NoArtifactForPlatform`, `WrongChannel`, `Rollback`, `ArtifactMismatch`, … | ✓ failure |

Convergence = every expected node is `Applied`. Any node still pending when the
timeout fires drops into the timeout table with its last-known state.

## The one real dependency: a node-status read path

The watcher needs each node's **(pinned channel, current applied debug
version, last candidate state)**. This is the only genuinely new surface; the
rest is assembly. Resolve it in this order of preference:

1. **Reuse the update pull/serve handshake** (`propagate.rs`). The serve leg
   already answers "what do you have?" during a pull. Add a *status-only*
   query (no fetch) that returns `{channel, applied_version, last_outcome}` for
   the requester's platform. Cheapest: it rides the existing update wire
   (`net/update.rs`) and the QUIC-handshake-identity origin rule (KH 7.5) gives
   the responder's identity for free.
2. **Registry/presence enrichment.** If a node already publishes its applied
   version + provenance into a registry row (the rollout metadata carries
   `git_commit`/version provenance), the coordinator can read convergence from
   rows it already syncs — no new round-trip. Verify whether
   `relcache`/registry persists "applied version" anywhere queryable; if not,
   prefer option 1.

Pick option 1 as the baseline (self-contained, testable on loopback); treat
option 2 as an optimization once a node actually persists applied-version into
a synced row. **Do not** invent a bespoke push channel — same constraint as the
rollout driver itself (ADR-0016).

## Output

```
DEBUG_CONVERGE target=7 channel=debug subnet=lab expected=3
  node-a   Applied                v7
  node-b   StagedAwaitingConsent  v6  (run: spt update apply)
  node-c   Rejected               NoArtifactForPlatform (x86_64-apple-darwin)
CONVERGED 1/3  (timeout 120s)
```

- All expected `Applied` → print table, exit `0`.
- Timeout with any pending/failed → print table + the quiesce/apply hints,
  exit non-zero. The exit code must be machine-distinguishable from a usage
  error (reserve `2` for usage; use `1` for non-convergence).

## Implementation steps

1. **Status query (wire).** Add the status-only request/response to
   `net/update.rs` + the daemon serve path in `propagate.rs`; origin identity
   from the stream table (KH 7.5), never payload bytes. Unit-test the
   request/response codec.
2. **Node enumeration.** Factor the trust-derived subnet membership read out of
   the `subnet status` path (b9326f4) into something `debug-converge` can call
   for the expected set. Honor `--nodes` / `--subnet`.
3. **Poll loop + classifier.** In `xtask`: round-robin the expected set, map
   each response to the state model, stop early when all `Applied`, else until
   `--timeout`. Pure classifier function is unit-testable with canned responses.
4. **Table + exit codes.** Render + exit-code policy above.
5. **Tests.**
   - *unit:* state classifier (each typed outcome → row); table formatting;
     exit-code policy. `[unit->REQ-UPD-6]`
   - *int (loopback + two-host):* coordinator stages v_N; drive a second node
     through Offline → StagedAwaitingConsent → Applied and assert the table
     converges and exits 0; assert a stable-pinned node shows `NotPinned` and a
     missing-platform node shows `Rejected{NoArtifactForPlatform}`. This is the
     **int** stage REQ-UPD-6 currently lacks. `[int->REQ-UPD-6]`
6. **Runbook.** Replace the manual "Apply and observe" steps in
   `DEBUG-ROLLOUT.md` with `xtask debug-converge`, keeping the manual path as
   the fallback.

## Traceability

Building this activates the **`int`** stage for `REQ-UPD-6` (today
`required_stages = [doc, impl, unit]`, `-int`). When picked up: set
`required_stages = ["doc","impl","unit","int"]` in `traceable-reqs.toml` and
land the two-host convergence test as the `int` evidence in the same change.

## Estimate

Small-to-medium. The status query (step 1) is the only new protocol surface and
the only real risk; steps 2–4 are assembly over existing primitives; step 5 is
the bulk of the diff. One focused session if the loopback status query lands
cleanly.


Now check for the project's deferred-work ledger (the `docs(deferred):` convention from `bedb0dd`) to add a pointer row:


Bash

cd C:/Users/decid/Documents/projects/spt-core && git show bedb0dd --stat 2>/dev/null | grep -v "LF will be" | grep -E "\.md|\.toml" | head -10

Find the deferred-work ledger file


docs/DEFERRED.md | 1 +


1	# Deferred Features
2
3	Intended features explicitly cut from v1 scope but committed for later. Not backlog noise — these are decided directions parked for sequencing reasons. Keep this list short and real.
4
5	| Feature | Cut from | Why deferred | Trigger to revisit |
6	|---|---|---|---|
7	| Scrollback on-disk spillover | terminal wrapper v1 | In-memory ring covers the common case; spillover adds a persistence/rotation story | First long-running session that overflows the ring usefully, or any "scroll back further than the buffer" user need |
8	| Sidecar adapter process (long-running, wire-protocol) | harness contract v1 | Manifest + `spt.exe` subcommand surface covers v1 harnesses; sidecar only earns its keep for streaming / in-memory cross-event state | A harness outgrows manifest+hooks (needs streaming or stateful coordination) |
9	| Manifest `include` / composition (option C) | manifest v1 | Single flat manifest per harness is enough until a harness wants to override only part of a section | First sub-project that needs to inherit spt-core defaults but override one section |
10	| Concrete Shell types (GameRobot, in-session-inject) — **OS-notification shell carved out: ships M5** as the dogfood proof (user decision 2026-06-04, M5-PLAN §Scope decisions) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |
11	| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1. **M5 ships presence *resolution* (gossip + most-recently-active API) — its future substrate; the endpoint stays deferred** (user decision 2026-06-04). <!-- [doc->REQ-PRES-1] --> **M5-D6 seam note:** the substrate now exists — the gossiped datum (`Instance.last_active_ms`, riding the registry's epoch lease + replication) and the one MRA API (`spt-daemon::presence`). A future PresenceChannel's dispatch/bind/thread styles consume exactly this datum plus the broker PresenceLog seam (REQ-EP-4); nothing about the endpoint type needs a new gossip channel. | Once Shells exist and the subnet registry is stable |
12	| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal. **Note:** distinct from the v1 broker *sensing* user input on a PTY it already holds (spt-hosted) — that is in v1 and is NOT OS-wide input monitoring | Opt-in enhancement after agent-interaction presence proves out |
13	| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates); **the framework (grant store + escalation + pre-consent flags) ships M5 with this capability id reserved-but-refusing** (user decision 2026-06-04) — the capability lands into it later. Defers with it: the remote-fork arm, cross-node shell spawn, `shell_wake_spawn_anywhere` *behavior* (grant shape ships M5). <!-- [doc->REQ-INST-6] --> **M5-D5b note:** remote `spt suspend/wake <id@node>` shipped (the rest-op wire family, remote-drive trust class) — it *moves existing instances between rest states only*; the remote-**fork** arm (creating an instance on a node that has none) stays here, riding instantiate-anywhere's consent gate. A wake addressed to a node with no local instance refuses with `WAKE_NO_REACHABLE_INSTANCE` naming this deferral. | Revisit-trigger met (remote-drive shipped M4); held back by user decision — capability drops into the M5 consent framework when picked up |
14	| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly). **M5 ships the framework with this capability id reserved-but-refusing** (user decision 2026-06-04) | Designed alongside instantiate-anywhere's consent gate |
15	| Shell-binary (and harness-binary) sandboxing | Shell concept v1 | A capability toolset bounds what the *agent* can ask, not what the *binary* can do with its OS perms. Baseline stance: running any adapter/shell binary = a disclosed, accepted trust risk for all spt-core users | If/when untrusted third-party shell/harness binaries become common enough to warrant OS-level sandboxing |
16	| Concrete Shell adapters (e.g. GameRobot) + the shell binaries themselves | Shell concept v1 | The Shell *model*, manifest schema, perch layout, and `api` surface are now specified; concrete shells are large, platform-specific, and not needed for core | First real driven-surface use case |
17	| REQ-CONSENT int evidence (interactive escalation E2E) | M5 closeout (D9b rule-5) | The escalation prompt''s end-to-end leg needs a real harness session answering a real consent prompt — loopback-faking it would prove nothing; the grant store + gate + CLI are impl/unit-proven | The downstream rebuilt spt plugin (v1 acceptance) — its first gated spawn IS the int evidence |
18	| REQ-INSTALL-4 int evidence (`spt adapter add --github` against a real repo) | M5 closeout (D9b rule-5) | Needs a real clone target; impl/unit cover manifest-first validation + registration. The standalone `SaberMage/spt-shell-notify` repo now makes this trivially possible | First milestone that touches adapter lifecycle again — one E2E cloning the notify repo |
19	| OS-service registration at install (REQ-INSTALL-1's third leg: systemd user service / Windows service or scheduled task for the always-on guarantee) | installer v0.1 (M6-D2, grill decision 3) | Daemon auto-start on any `spt` invocation covers dev-stage use; the minimal non-interactive script stays non-OS-entangled (also serves REQ-INSTALL-2's repackaging stance). Honest gap: a node is unreachable after reboot until something invokes `spt` | First always-on deployment need (headless/server node, or the GUI milestone's background expectations) |
20	| Tier 2 agent-docs: MCP doc/resource server + `spt <cmd> --help --json` structured-help mode (REQ-DOCS-4 legs) | docs v0.1 (M6-D3/D5, DOCS-STRATEGY §v0.1 grill) | Tier 1 covers the integration surface: the schemars-derived manifest schema + llms.txt/llms-full.txt + the generated CLI reference; `--help --json` is low marginal value over that reference, the MCP server is the standout *later* fit | First integrating dev-agent that outgrows llms.txt + schema (or the `spt-claude-code` work surfacing a concrete MCP-resource need) |
21	| Subnet attachment verbs: `spt subnet detach <NAME> [--auto]` / `attach <NAME> [--auto]` (daemon keeps running, stops/starts advertising + connecting for that subnet; `--auto` persists the startup default), `spt subnet leave <NAME>` (elevation-gated), maybe `spt subnet disband <NAME>` (elevation + current TOTP) | M7 grill (user spec 2026-06-06; CONTEXT §subnet attachment) | The attached/detached *term* + the all-attached banner ship M7; the per-subnet serve-state machinery (selective advertise/connect, persisted flags) is its own deliverable | M8 CLI nounification, or first multi-subnet user needing selective participation |
22	| Linux elevation model (user-ratified direction 2026-06-06): (1) install symlinks the binary into a sudo-reachable path (e.g. `/usr/local/bin`) so `sudo spt` resolves; (2) first `sudo spt` detects elevation and prompts once for the DEFAULT USER ACCOUNT — thereafter any `sudo spt` daemon launch always runs the daemon (and state) under that account, never root (kills the `sudo HOME=$HOME` dance and the root-owned-state hazard) | M7 acceptance run (gravity: `sudo: spt: command not found`, then the HOME-universe split) | Needs installer + first-run-state design; Windows elevation has no equivalent problem (same user profile) | M8, with the installer's next touch |
23	| Windows firewall registration for the installed binary: a DETACHED daemon never shows the first-bind firewall prompt, so inbound UDP (mDNS + QUIC meet/pairing) is silently dropped — found as the M7 acceptance NO_SEED_HOLDER (dev/test paths had accumulated allow rules; the install path had none). Options: an elevated install leg adds the rule (`New-NetFirewallRule`), and/or the daemon self-detects blocked inbound and renders it as the "no connection" state in `subnet status` + the coming-online banner | M7 acceptance run (2026-06-06) | User-scope installer is deliberately non-OS-entangled (REQ-INSTALL-2); needs the elevated-install / OS-service design anyway | M8, with the OS-service + Linux-elevation install work |
24	| ~~**Non-admin daemon spawn (both OSes)**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 5.7, REQ-HAZARD-ELEVATED-DAEMON-SPAWN): de-elevated spawn at `spawn_detached` (Windows: UAC linked token via `CreateProcessWithTokenW`; Linux: drop child to SUDO_UID/GID) + `Daemon::run` entry guard + the unix `spt` main-entry sudo drop (whole process re-anchored to the invoker's universe, elevation kept PROVEN for the gated commands). Remaining Linux follow-up = the install-symlink + default-account election row below (M8) | M7 acceptance run (2026-06-06) | The membership-implies-reachability activation made elevated commands daemon-spawners — correct goal, wrong token | DONE (real elevated-spawn verification on both OSes rides the next acceptance pass) |
25	| ~~**Ghost registry row eviction**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 4.10, REQ-HAZARD-REGISTRY-GHOST-ROWS): silent-peer decay — rows from a node unheard for `registry_evict_after_ms` (default 300s ≈ 10 pump cadences) evict on the registry pump tick, snapshots rewritten; own rows never decay. STALE trust rows for a dead identity still need a manual prune verb (below) | M7 acceptance run (2026-06-06) | Needs a decay/eviction policy (e.g. drop rows unrefreshed for N pump cadences; trust-row pruning verb) | DONE (fleet cleanup: 09ef831e ghost rows on both BIGNET nodes decay once the new build deploys) |
26	| Trust-row prune verb (`spt subnet prune <node>` or similar): registry GHOST rows now decay (4.10), but the dead identity's TRUST rows persist by design (trust is a user decision) — they cost dead dials every pump tick and clutter status views. BIGNET carries two such rows (09ef831e, both sides) | Ghost-eviction fix (2026-06-06) | Trust mutation = security surface; wants the elevation gate + the M8 noun shapes | M8 CLI nounification |
27	| Elevated spt-hosted endpoints — a consented elevation satellite: the broker lives inside the always-unelevated daemon (KH 5.7), so spt-hosted children are unelevated by construction; there is NO silent unelevated→elevated path on either OS (that is UAC/sudo's whole point). An agent needing admin powers is harness-hosted-elevated today (works: TCP listeners + talk-down pipes). The spt-hosted form needs a per-grant CONSENTED elevation: UAC prompt / polkit spawns a small elevated PTY-host satellite holding only the elevated children, linked back to the unelevated daemon — rides the REQ-CONSENT-2 escalation framework (allow-once/always) + the reserved-but-refusing capability pattern | Elevated-endpoint design grill (user 2026-06-06) | Real elevation-granting UX + a second privileged process = its own deliverable; the consent framework it rides already ships | First spt-hosted agent that needs admin powers (or the instantiate-anywhere milestone) |
28	| ~~AMBIGUOUS render~~ **SHIPPED 2026-06-06** (rode the ghost-eviction fix): resolution refusals render valid copy-paste targets at the wansend boundary (`render_refusal`) — labels preferred, key-prefix on label collision, `subnet:id` for cross-subnet | M7 acceptance run (user 2026-06-06) | Pure render fix at the wansend/CLI boundary; carries into the M8 nounification sweep | DONE |
29	| Post-join address seeding: the pairing ceremony holds a live, authenticated connection to the peer, but the peer pumps then re-find each other from scratch via id-only discovery (fresh node ids must propagate to n0 DNS first) — observed live as ~1 min to first `status --nodes` convergence after a join. Seed the pump's address cache from the ceremony connection (both sides) for instant first sync | M7 acceptance run (2026-06-06) | Convergence is correct, just slow on first contact; needs an addr-cache seam the pumps consult before the resolver | M8 polish |
30	| Clock source priority for TOTP/rendezvous: query a well-known world clock (NTP) FIRST, fall back to local time — machines with skewed local clocks currently derive wrong steps/rendezvous tokens silently | M7 acceptance grill (user 2026-06-06) | Needs an NTP client dep + offset caching + offline fallback semantics; ±1 window covers small skew today | First field report of clock-skew join failures, or M8 polish |
31	| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill): `spt endpoint` namespace absorbs fork/suspend/wake/shutdown/rename/stop/digest + `access` (per-endpoint store — NOT subnet-scoped) + `description` (ex-`resources` blurb; bare = show, `set` = author); `spt endpoint list [--local\|--subnet <name>]` = merged listing, default all-subnets grouped by subnet, SELF endpoint pinned distinctly at top (bare `spt endpoint` = this list); agent HOT PATH stays flat (`send`/`ring`/`ready`/`whoami`/`how-to`); `notify` → `spt subnet notify`; `notif` stays top-level (user-recall, cross-subnet); `spt daemon stop\|status` noun (hidden `daemon` → `daemon run`); agent-endpoint `shutdown` keeps its name | M7 grill (2026-06-06) | Second surface-breaking reorg mid-acceptance; adapter window still open immediately after M7 closes | M8 planning — mint REQs first (rule 3) |
32

# Deferred Features

Intended features explicitly cut from v1 scope but committed for later. Not backlog noise — these are decided directions parked for sequencing reasons. Keep this list short and real.

| Feature | Cut from | Why deferred | Trigger to revisit |
|---|---|---|---|
| Scrollback on-disk spillover | terminal wrapper v1 | In-memory ring covers the common case; spillover adds a persistence/rotation story | First long-running session that overflows the ring usefully, or any "scroll back further than the buffer" user need |
| Sidecar adapter process (long-running, wire-protocol) | harness contract v1 | Manifest + `spt.exe` subcommand surface covers v1 harnesses; sidecar only earns its keep for streaming / in-memory cross-event state | A harness outgrows manifest+hooks (needs streaming or stateful coordination) |
| Manifest `include` / composition (option C) | manifest v1 | Single flat manifest per harness is enough until a harness wants to override only part of a section | First sub-project that needs to inherit spt-core defaults but override one section |
| Concrete Shell types (GameRobot, in-session-inject) — **OS-notification shell carved out: ships M5** as the dogfood proof (user decision 2026-06-04, M5-PLAN §Scope decisions) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |
| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1. **M5 ships presence *resolution* (gossip + most-recently-active API) — its future substrate; the endpoint stays deferred** (user decision 2026-06-04). <!-- [doc->REQ-PRES-1] --> **M5-D6 seam note:** the substrate now exists — the gossiped datum (`Instance.last_active_ms`, riding the registry's epoch lease + replication) and the one MRA API (`spt-daemon::presence`). A future PresenceChannel's dispatch/bind/thread styles consume exactly this datum plus the broker PresenceLog seam (REQ-EP-4); nothing about the endpoint type needs a new gossip channel. | Once Shells exist and the subnet registry is stable |
| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal. **Note:** distinct from the v1 broker *sensing* user input on a PTY it already holds (spt-hosted) — that is in v1 and is NOT OS-wide input monitoring | Opt-in enhancement after agent-interaction presence proves out |
| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates); **the framework (grant store + escalation + pre-consent flags) ships M5 with this capability id reserved-but-refusing** (user decision 2026-06-04) — the capability lands into it later. Defers with it: the remote-fork arm, cross-node shell spawn, `shell_wake_spawn_anywhere` *behavior* (grant shape ships M5). <!-- [doc->REQ-INST-6] --> **M5-D5b note:** remote `spt suspend/wake <id@node>` shipped (the rest-op wire family, remote-drive trust class) — it *moves existing instances between rest states only*; the remote-**fork** arm (creating an instance on a node that has none) stays here, riding instantiate-anywhere's consent gate. A wake addressed to a node with no local instance refuses with `WAKE_NO_REACHABLE_INSTANCE` naming this deferral. | Revisit-trigger met (remote-drive shipped M4); held back by user decision — capability drops into the M5 consent framework when picked up |
| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly). **M5 ships the framework with this capability id reserved-but-refusing** (user decision 2026-06-04) | Designed alongside instantiate-anywhere's consent gate |
| Shell-binary (and harness-binary) sandboxing | Shell concept v1 | A capability toolset bounds what the *agent* can ask, not what the *binary* can do with its OS perms. Baseline stance: running any adapter/shell binary = a disclosed, accepted trust risk for all spt-core users | If/when untrusted third-party shell/harness binaries become common enough to warrant OS-level sandboxing |
| Concrete Shell adapters (e.g. GameRobot) + the shell binaries themselves | Shell concept v1 | The Shell *model*, manifest schema, perch layout, and `api` surface are now specified; concrete shells are large, platform-specific, and not needed for core | First real driven-surface use case |
| REQ-CONSENT int evidence (interactive escalation E2E) | M5 closeout (D9b rule-5) | The escalation prompt''s end-to-end leg needs a real harness session answering a real consent prompt — loopback-faking it would prove nothing; the grant store + gate + CLI are impl/unit-proven | The downstream rebuilt spt plugin (v1 acceptance) — its first gated spawn IS the int evidence |
| REQ-INSTALL-4 int evidence (`spt adapter add --github` against a real repo) | M5 closeout (D9b rule-5) | Needs a real clone target; impl/unit cover manifest-first validation + registration. The standalone `SaberMage/spt-shell-notify` repo now makes this trivially possible | First milestone that touches adapter lifecycle again — one E2E cloning the notify repo |
| OS-service registration at install (REQ-INSTALL-1's third leg: systemd user service / Windows service or scheduled task for the always-on guarantee) | installer v0.1 (M6-D2, grill decision 3) | Daemon auto-start on any `spt` invocation covers dev-stage use; the minimal non-interactive script stays non-OS-entangled (also serves REQ-INSTALL-2's repackaging stance). Honest gap: a node is unreachable after reboot until something invokes `spt` | First always-on deployment need (headless/server node, or the GUI milestone's background expectations) |
| Tier 2 agent-docs: MCP doc/resource server + `spt <cmd> --help --json` structured-help mode (REQ-DOCS-4 legs) | docs v0.1 (M6-D3/D5, DOCS-STRATEGY §v0.1 grill) | Tier 1 covers the integration surface: the schemars-derived manifest schema + llms.txt/llms-full.txt + the generated CLI reference; `--help --json` is low marginal value over that reference, the MCP server is the standout *later* fit | First integrating dev-agent that outgrows llms.txt + schema (or the `spt-claude-code` work surfacing a concrete MCP-resource need) |
| Subnet attachment verbs: `spt subnet detach <NAME> [--auto]` / `attach <NAME> [--auto]` (daemon keeps running, stops/starts advertising + connecting for that subnet; `--auto` persists the startup default), `spt subnet leave <NAME>` (elevation-gated), maybe `spt subnet disband <NAME>` (elevation + current TOTP) | M7 grill (user spec 2026-06-06; CONTEXT §subnet attachment) | The attached/detached *term* + the all-attached banner ship M7; the per-subnet serve-state machinery (selective advertise/connect, persisted flags) is its own deliverable | M8 CLI nounification, or first multi-subnet user needing selective participation |
| Linux elevation model (user-ratified direction 2026-06-06): (1) install symlinks the binary into a sudo-reachable path (e.g. `/usr/local/bin`) so `sudo spt` resolves; (2) first `sudo spt` detects elevation and prompts once for the DEFAULT USER ACCOUNT — thereafter any `sudo spt` daemon launch always runs the daemon (and state) under that account, never root (kills the `sudo HOME=$HOME` dance and the root-owned-state hazard) | M7 acceptance run (gravity: `sudo: spt: command not found`, then the HOME-universe split) | Needs installer + first-run-state design; Windows elevation has no equivalent problem (same user profile) | M8, with the installer's next touch |
| Windows firewall registration for the installed binary: a DETACHED daemon never shows the first-bind firewall prompt, so inbound UDP (mDNS + QUIC meet/pairing) is silently dropped — found as the M7 acceptance NO_SEED_HOLDER (dev/test paths had accumulated allow rules; the install path had none). Options: an elevated install leg adds the rule (`New-NetFirewallRule`), and/or the daemon self-detects blocked inbound and renders it as the "no connection" state in `subnet status` + the coming-online banner | M7 acceptance run (2026-06-06) | User-scope installer is deliberately non-OS-entangled (REQ-INSTALL-2); needs the elevated-install / OS-service design anyway | M8, with the OS-service + Linux-elevation install work |
| ~~**Non-admin daemon spawn (both OSes)**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 5.7, REQ-HAZARD-ELEVATED-DAEMON-SPAWN): de-elevated spawn at `spawn_detached` (Windows: UAC linked token via `CreateProcessWithTokenW`; Linux: drop child to SUDO_UID/GID) + `Daemon::run` entry guard + the unix `spt` main-entry sudo drop (whole process re-anchored to the invoker's universe, elevation kept PROVEN for the gated commands). Remaining Linux follow-up = the install-symlink + default-account election row below (M8) | M7 acceptance run (2026-06-06) | The membership-implies-reachability activation made elevated commands daemon-spawners — correct goal, wrong token | DONE (real elevated-spawn verification on both OSes rides the next acceptance pass) |
| ~~**Ghost registry row eviction**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 4.10, REQ-HAZARD-REGISTRY-GHOST-ROWS): silent-peer decay — rows from a node unheard for `registry_evict_after_ms` (default 300s ≈ 10 pump cadences) evict on the registry pump tick, snapshots rewritten; own rows never decay. STALE trust rows for a dead identity still need a manual prune verb (below) | M7 acceptance run (2026-06-06) | Needs a decay/eviction policy (e.g. drop rows unrefreshed for N pump cadences; trust-row pruning verb) | DONE (fleet cleanup: 09ef831e ghost rows on both BIGNET nodes decay once the new build deploys) |
| Trust-row prune verb (`spt subnet prune <node>` or similar): registry GHOST rows now decay (4.10), but the dead identity's TRUST rows persist by design (trust is a user decision) — they cost dead dials every pump tick and clutter status views. BIGNET carries two such rows (09ef831e, both sides) | Ghost-eviction fix (2026-06-06) | Trust mutation = security surface; wants the elevation gate + the M8 noun shapes | M8 CLI nounification |
| Elevated spt-hosted endpoints — a consented elevation satellite: the broker lives inside the always-unelevated daemon (KH 5.7), so spt-hosted children are unelevated by construction; there is NO silent unelevated→elevated path on either OS (that is UAC/sudo's whole point). An agent needing admin powers is harness-hosted-elevated today (works: TCP listeners + talk-down pipes). The spt-hosted form needs a per-grant CONSENTED elevation: UAC prompt / polkit spawns a small elevated PTY-host satellite holding only the elevated children, linked back to the unelevated daemon — rides the REQ-CONSENT-2 escalation framework (allow-once/always) + the reserved-but-refusing capability pattern | Elevated-endpoint design grill (user 2026-06-06) | Real elevation-granting UX + a second privileged process = its own deliverable; the consent framework it rides already ships | First spt-hosted agent that needs admin powers (or the instantiate-anywhere milestone) |
| ~~AMBIGUOUS render~~ **SHIPPED 2026-06-06** (rode the ghost-eviction fix): resolution refusals render valid copy-paste targets at the wansend boundary (`render_refusal`) — labels preferred, key-prefix on label collision, `subnet:id` for cross-subnet | M7 acceptance run (user 2026-06-06) | Pure render fix at the wansend/CLI boundary; carries into the M8 nounification sweep | DONE |
| Post-join address seeding: the pairing ceremony holds a live, authenticated connection to the peer, but the peer pumps then re-find each other from scratch via id-only discovery (fresh node ids must propagate to n0 DNS first) — observed live as ~1 min to first `status --nodes` convergence after a join. Seed the pump's address cache from the ceremony connection (both sides) for instant first sync | M7 acceptance run (2026-06-06) | Convergence is correct, just slow on first contact; needs an addr-cache seam the pumps consult before the resolver | M8 polish |
| Clock source priority for TOTP/rendezvous: query a well-known world clock (NTP) FIRST, fall back to local time — machines with skewed local clocks currently derive wrong steps/rendezvous tokens silently | M7 acceptance grill (user 2026-06-06) | Needs an NTP client dep + offset caching + offline fallback semantics; ±1 window covers small skew today | First field report of clock-skew join failures, or M8 polish |
| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill): `spt endpoint` namespace absorbs fork/suspend/wake/shutdown/rename/stop/digest + `access` (per-endpoint store — NOT subnet-scoped) + `description` (ex-`resources` blurb; bare = show, `set` = author); `spt endpoint list [--local\|--subnet <name>]` = merged listing, default all-subnets grouped by subnet, SELF endpoint pinned distinctly at top (bare `spt endpoint` = this list); agent HOT PATH stays flat (`send`/`ring`/`ready`/`whoami`/`how-to`); `notify` → `spt subnet notify`; `notif` stays top-level (user-recall, cross-subnet); `spt daemon stop\|status` noun (hidden `daemon` → `daemon run`); agent-endpoint `shutdown` keeps its name | M7 grill (2026-06-06) | Second surface-breaking reorg mid-acceptance; adapter window still open immediately after M7 closes | M8 planning — mint REQs first (rule 3) |


`DEFERRED.md` is the ledger. Add the debug-converge row:


Edit

| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill): `spt endpoint` namespace absorbs fork/suspend/wake/shutdown/rename/stop/digest + `access` (per-endpoint store — NOT subnet-scoped) + `description` (ex-`resources` blurb; bare = show, `set` = author); `spt endpoint list [--local\|--subnet <name>]` = merged listing, default all-subnets grouped by subnet, SELF endpoint pinned distinctly at top (bare `spt endpoint` = this list); agent HOT PATH stays flat (`send`/`ring`/`ready`/`whoami`/`how-to`); `notify` → `spt subnet notify`; `notif` stays top-level (user-recall, cross-subnet); `spt daemon stop\|status` noun (hidden `daemon` → `daemon run`); agent-endpoint `shutdown` keeps its name | M7 grill (2026-06-06) | Second surface-breaking reorg mid-acceptance; adapter window still open immediately after M7 closes | M8 planning — mint REQs first (rule 3) |

| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill): `spt endpoint` namespace absorbs fork/suspend/wake/shutdown/rename/stop/digest + `access` (per-endpoint store — NOT subnet-scoped) + `description` (ex-`resources` blurb; bare = show, `set` = author); `spt endpoint list [--local\|--subnet <name>]` = merged listing, default all-subnets grouped by subnet, SELF endpoint pinned distinctly at top (bare `spt endpoint` = this list); agent HOT PATH stays flat (`send`/`ring`/`ready`/`whoami`/`how-to`); `notify` → `spt subnet notify`; `notif` stays top-level (user-recall, cross-subnet); `spt daemon stop\|status` noun (hidden `daemon` → `daemon run`); agent-endpoint `shutdown` keeps its name | M7 grill (2026-06-06) | Second surface-breaking reorg mid-acceptance; adapter window still open immediately after M7 closes | M8 planning — mint REQs first (rule 3) |
| `xtask debug-converge` — automated convergence watcher for debug rollout (the REQ-UPD-6 first-slice item ADR-0016 names): watch every expected debug-pinned reachable lab node until all report the target version applied, else a per-node timeout table (Offline / StagedAwaitingConsent / BlockedByBrokerResources / Rejected{reason}). Activates REQ-UPD-6's `int` stage. Full build plan: `docs/DEBUG-CONVERGE-PLAN.md` | REQ-UPD-6 first slice (2026-06-06) | The first slice ships manual "Apply and observe" (`DEBUG-ROLLOUT.md`); the watcher's one genuinely new surface is a node status-query wire leg (no-fetch `{channel, applied_version, last_outcome}` over the existing update handshake) — assembly otherwise | First multi-node debug rollout where hand-walking nodes is the bottleneck, or M8 update polish |


The file C:/Users/decid/Documents/projects/spt-core/docs/DEFERRED.md has been updated successfully. (file state is current in your context — no need to Read it back)

| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill): `spt endpoint` namespace absorbs fork/suspend/wake/shutdown/rename/stop/digest + `access` (per-endpoint store — NOT subnet-scoped) + `description` (ex-`resources` blurb; bare = show, `set` = author); `spt endpoint list [--local\|--subnet <name>]` = merged listing, default all-subnets grouped by subnet, SELF endpoint pinned distinctly at top (bare `spt endpoint` = this list); agent HOT PATH stays flat (`send`/`ring`/`ready`/`whoami`/`how-to`); `notify` → `spt subnet notify`; `notif` stays top-level (user-recall, cross-subnet); `spt daemon stop\|status` noun (hidden `daemon` → `daemon run`); agent-endpoint `shutdown` keeps its name | M7 grill (2026-06-06) | Second surface-breaking reorg mid-acceptance; adapter window still open immediately after M7 closes | M8 planning — mint REQs first (rule 3) |
| `xtask debug-converge` — automated convergence watcher for debug rollout (the REQ-UPD-6 first-slice item ADR-0016 names): watch every expected debug-pinned reachable lab node until all report the target version applied, else a per-node timeout table (Offline / StagedAwaitingConsent / BlockedByBrokerResources / Rejected{reason}). Activates REQ-UPD-6's `int` stage. Full build plan: `docs/DEBUG-CONVERGE-PLAN.md` | REQ-UPD-6 first slice (2026-06-06) | The first slice ships manual "Apply and observe" (`DEBUG-ROLLOUT.md`); the watcher's one genuinely new surface is a node status-query wire leg (no-fetch `{channel, applied_version, last_outcome}` over the existing update handshake) — assembly otherwise | First multi-node debug rollout where hand-walking nodes is the bottleneck, or M8 update polish |

| Post-join address seeding: the pairing ceremony holds a live, authenticated connection to the peer, but the peer pumps then re-find each other from scratch via id-only discovery (fresh node ids must propagate to n0 DNS first) — observed live as ~1 min to first `status --nodes` convergence after a join. Seed the pump's address cache from the ceremony connection (both sides) for instant first sync | M7 acceptance run (2026-06-06) | Convergence is correct, just slow on first contact; needs an addr-cache seam the pumps consult before the resolver | M8 polish |

| Clock source priority for TOTP/rendezvous: query a well-known world clock (NTP) FIRST, fall back to local time — machines with skewed local clocks currently derive wrong steps/rendezvous tokens silently | M7 acceptance grill (user 2026-06-06) | Needs an NTP client dep + offset caching + offline fallback semantics; ±1 window covers small skew today | First field report of clock-skew join failures, or M8 polish |

+| `xtask debug-converge` — automated convergence watcher for debug rollout (the REQ-UPD-6 first-slice item ADR-0016 names): watch every expected debug-pinned reachable lab node until all report the target version applied, else a per-node timeout table (Offline / StagedAwaitingConsent / BlockedByBrokerResources / Rejected{reason}). Activates REQ-UPD-6's `int` stage. Full build plan: `docs/DEBUG-CONVERGE-PLAN.md` | REQ-UPD-6 first slice (2026-06-06) | The first slice ships manual "Apply and observe" (`DEBUG-ROLLOUT.md`); the watcher's one genuinely new surface is a node status-query wire leg (no-fetch `{channel, applied_version, last_outcome}` over the existing update handshake) — assembly otherwise | First multi-node debug rollout where hand-walking nodes is the bottleneck, or M8 update polish |


Commit the deferred plan + ledger row:


Bash

cd C:/Users/decid/Documents/projects/spt-core && git add docs/DEBUG-CONVERGE-PLAN.md docs/DEFERRED.md 2>/dev/null && git commit -q -F - 2>&1 <<'MSG' | grep -v "LF will be replaced"
docs(deferred): debug-converge watcher build plan — REQ-UPD-6 first-slice tail

The REQ-UPD-6 debug rollout ships with manual convergence observation
(DEBUG-ROLLOUT.md "Apply and observe"). ADR-0016's first-slice list also
names an automated convergence watcher; rather than build it now (scope) or
lose the design, capture an implementation-ready plan and a DEFERRED.md row.

docs/DEBUG-CONVERGE-PLAN.md specifies the xtask debug-converge subcommand:
expected-node derivation (trust-derived subnet membership, b9326f4), the
per-node state model mirroring the existing typed pull/apply outcomes, the
one real new surface (a no-fetch status query riding the update handshake,
origin identity from the stream table per KH 7.5), output + exit-code policy,
and the test plan whose two-host convergence test becomes REQ-UPD-6's int
evidence. Building it activates REQ-UPD-6 required_stages += "int".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MSG
echo "=== log (4 commits this session) ===" && git log --oneline -4 2>/dev/null | grep -v "LF will be"

Commit the convergence plan + ledger row


=== log (4 commits this session) ===
66b3570 docs(deferred): debug-converge watcher build plan — REQ-UPD-6 first-slice tail
a0e36d3 feat(update): platform-targeted update sets + debug rollout (REQ-UPD-6, ADR-0016)
5c3ebb1 style: rustfmt 1.8.0 whole-tree reflow
6aace53 fix(runtime): CREATE_NO_WINDOW in command_for — close the KH 5.8 residual


Final verification — traceability green, working tree clean, and clean up the proof temp dir:


cd C:/Users/decid/Documents/projects/spt-core && echo "=== traceable-reqs (exit + UPD-6 + any FAIL) ===" && traceable-reqs check >/tmp/tr.out 2>&1; echo "exit=$?"; grep -iE "UPD-6|FAIL|\[X\]|missing evidence" /tmp/tr.out | head -8 && echo "=== working tree (should be empty = all committed) ===" && git status --porcelain 2>/dev/null | grep -v "LF will be" | head && echo "(empty above = clean)" && echo "=== cleanup proof dir ===" && rm -rf "C:/Users/decid/AppData/Local/Temp/upd6proof" && echo "removed upd6proof"

Traceability gate + clean tree + cleanup


=== traceable-reqs (exit + UPD-6 + any FAIL) ===
exit=0
       A surfaced concurrent context pair is durably preserved (both versions, tracked artifacts) until a strictly dominating write clears it; no reconcile failure path discards an unmerged version (6.6, ADR-0013)
       Concurrent SQLite openers (registry/spool) must not fail with 'database is locked' (4.7)
       Stale registry entries degrade to fallback, never hard-fail (4.3)
  [OK] REQ-UPD-6  required: [doc, impl, unit]  stages: +doc +impl +unit +int
=== working tree (should be empty = all committed) ===
(empty above = clean)
=== cleanup proof dir ===
removed upd6proof


Done. Everything wrapped + green.

## Debug rollout — wrapped

**REQ-UPD-6 (platform-targeted update sets + debug rollout)** — the feature an agent was mid-testing at crash. Found it fully built but **entirely uncommitted**, tangled with a 157-file rustfmt reflow. Now proven, committed, green.

**4 commits this session:**
| Commit | What |
|---|---|
| `66b3570` | docs(deferred): debug-converge build plan + ledger row |
| `a0e36d3` | **feat(update): REQ-UPD-6** — update-set substrate + xtask debug-keygen/pin/rollout |
| `5c3ebb1` | style: rustfmt reflow (isolated, so the feat diff is reviewable) |
| `6aace53` | fix(runtime): KH 5.8 residual (prior task) |

**Proven:**
- Live loopback end-to-end: keygen → pin (`channel=debug`) → rollout v1 signed+staged (provenance stamped) → auto-bump v2 from state.
- Tests green: A→B→C platform-selective propagation, tamper/rollback rejection, apply-time platform selection.
- Workspace builds clean. `traceable-reqs` exit 0 — **REQ-UPD-6 [OK], now +doc +impl +unit +int** (the propagate integration test even covers `int`, beyond required).

**Convergence (deferred, per your call):** no automated `debug-converge` yet — runbook does manual "Apply and observe." Wrote `docs/DEBUG-CONVERGE-PLAN.md` (implementation-ready: node-status query wire leg is the only new surface; rest is assembly; its two-host test = the `int` evidence) + a `DEFERRED.md` row.

Working tree **clean** — all committed. Style reflow rode its own commit, not bundled into the feature. Proof temp dir cleaned up.

Deployment-speed point delivered: one coordinator signs all platforms once → existing subnet pump distributes the right bytes per node, full verification/channel/rollback/consent gates intact. No raw file-copy.

Updating Psyche:


Write

## Session: post-crash recovery — windows-bug residual + REQ-UPD-6 wrap

Recovered two pre-crash workstreams from the WIP tree. Both closed. 4 commits landed on dev-freeform (head 66b3570).

### 1. Windows phantom-window bug (KH 5.8) — residual closed
- b90820c had ALREADY fixed the reported 60s/2-window flash (sync pump's for-each-ref+rev-parse git pair via gitrun::run_git) + taskkill + run_bounded_command. User confirmed verified-clean live.
- Residual I closed: b90820c patched named call sites but missed the shared builder `ManifestRuntime::command_for` (runtime.rs:240) — feeds spawn_session (notif pump, psyche) + run_bounded_stdin (echo/turn). Put CREATE_NO_WINDOW in the builder so KH 5.8 holds by construction.
- Commit 6aace53. Tests green on Windows (the flagged-spawn tests = error-87 regression coverage). KNOWN-HAZARDS 5.8 mapping updated.

### 2. REQ-UPD-6 debug rollout — was fully built but UNCOMMITTED, now committed + proven
- Whole feature (daemon update-set substrate: relcache/release/update/propagate/applyhost + net/update + xtask debug-keygen/pin/rollout + ADR-0016 + DEBUG-ROLLOUT.md) sat uncommitted, tangled with a 157-file rustfmt reflow. `git diff -w` could NOT separate (rustfmt wraps across lines).
- Split into 2 commits per user call: 5c3ebb1 style (157 reflow files, cargo-fmt-clean) + a0e36d3 feat (15 feature files + docs).
- PROVEN live loopback: keygen→pin→rollout v1 signed+staged→auto-bump v2. Tests: A→B→C platform-selective propagation, tamper/rollback rejection, apply-time selection. Build clean. traceable-reqs REQ-UPD-6 [OK] +doc+impl+unit+int.
- Convergence reporting DEFERRED (user call): wrote docs/DEBUG-CONVERGE-PLAN.md (xtask debug-converge watcher; one new surface = no-fetch node status query over update handshake; its two-host test = REQ-UPD-6 int activation) + DEFERRED.md row. Commit 66b3570.

### State
Working tree CLEAN. All 4 commits on dev-freeform. CI likely-green (build + traceable-reqs pass locally).

### Next (unchanged from prior, now unblocked)
- v0.1.1 release (publish-blockers cleared; RELEASE-RUNBOOK manual signing needs the user)
- M8 planning (CLI nounification REQs first, trust-prune + subnet disband/attach verbs, Linux elevation install model, NTP clock, post-join address seeding, Windows firewall reg)
- debug-converge watcher when multi-node rollout hand-walking becomes the bottleneck
- spt-claude-code scoping


File created successfully at: C:/Users/decid/Documents/projects/spt-core/.claude/doyle-commune.md (file state is current in your context — no need to Read it back)

## Session: post-crash recovery — windows-bug residual + REQ-UPD-6 wrap

Recovered two pre-crash workstreams from the WIP tree. Both closed. 4 commits landed on dev-freeform (head 66b3570).

### 1. Windows phantom-window bug (KH 5.8) — residual closed
- b90820c had ALREADY fixed the reported 60s/2-window flash (sync pump's for-each-ref+rev-parse git pair via gitrun::run_git) + taskkill + run_bounded_command. User confirmed verified-clean live.
- Residual I closed: b90820c patched named call sites but missed the shared builder `ManifestRuntime::command_for` (runtime.rs:240) — feeds spawn_session (notif pump, psyche) + run_bounded_stdin (echo/turn). Put CREATE_NO_WINDOW in the builder so KH 5.8 holds by construction.
- Commit 6aace53. Tests green on Windows (the flagged-spawn tests = error-87 regression coverage). KNOWN-HAZARDS 5.8 mapping updated.

### 2. REQ-UPD-6 debug rollout — was fully built but UNCOMMITTED, now committed + proven
- Whole feature (daemon update-set substrate: relcache/release/update/propagate/applyhost + net/update + xtask debug-keygen/pin/rollout + ADR-0016 + DEBUG-ROLLOUT.md) sat uncommitted, tangled with a 157-file rustfmt reflow. `git diff -w` could NOT separate (rustfmt wraps across lines).
- Split into 2 commits per user call: 5c3ebb1 style (157 reflow files, cargo-fmt-clean) + a0e36d3 feat (15 feature files + docs).
- PROVEN live loopback: keygen→pin→rollout v1 signed+staged→auto-bump v2. Tests: A→B→C platform-selective propagation, tamper/rollback rejection, apply-time selection. Build clean. traceable-reqs REQ-UPD-6 [OK] +doc+impl+unit+int.
- Convergence reporting DEFERRED (user call): wrote docs/DEBUG-CONVERGE-PLAN.md (xtask debug-converge watcher; one new surface = no-fetch node status query over update handshake; its two-host test = REQ-UPD-6 int activation) + DEFERRED.md row. Commit 66b3570.

### State
Working tree CLEAN. All 4 commits on dev-freeform. CI likely-green (build + traceable-reqs pass locally).

### Next (unchanged from prior, now unblocked)
- v0.1.1 release (publish-blockers cleared; RELEASE-RUNBOOK manual signing needs the user)
- M8 planning (CLI nounification REQs first, trust-prune + subnet disband/attach verbs, Linux elevation install model, NTP clock, post-join address seeding, Windows firewall reg)
- debug-converge watcher when multi-node rollout hand-walking becomes the bottleneck
- spt-claude-code scoping


Psyche updated (commune dropped — it'll absorb + delete on next ingest).

Both pre-crash workstreams recovered and closed. Tree clean, 4 commits on `dev-freeform`. Next unblocked: v0.1.1 release (needs your manual signing) or M8 planning. Idle — ring when you want the next thing.


let's do v0.1.1 release now


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


Release = follow `RELEASE-RUNBOOK.md` exactly (manual signing per ADR-0015). Read it + current version state first.


1	# Release runbook
2
3	> M6-D6/D7 (ADR-0015). How a release ships. CI builds; the maintainer signs
4	> locally — release keys never enter CI.
5
6	<!-- [doc->REQ-REL-2] -->
7
8	## One-time setup (done 2026-06-05)
9
10	- Key ceremony: `cargo run -p xtask -- release-keygen rel-primary-2026` (and
11	  `rel-recovery-2026`); both seeds in the password manager (+ separate paper
12	  backup); both PUBLIC keys embedded in `BUILTIN_RELEASE_KEYS`
13	  (`crates/spt-daemon/src/release.rs`). The recovery key is never used until
14	  the primary is lost/compromised — then it signs the next release, which
15	  rotates in a fresh primary.
16	- `RELEASES_TOKEN` repo secret on spt-core: fine-grained PAT, Contents
17	  read/write on `SaberMage/spt-releases` only.
18
19	## Per release
20
21	1. **Bump** `[workspace.package] version` (root `Cargo.toml`) if needed;
22	   regenerate generated docs (`cargo run -p xtask -- gen`); land everything
23	   green.
24	2. **Tag**: `git tag v0.X.Y && git push origin v0.X.Y`. This triggers:
25	   - `release.yml` — both runners build, the assemble job creates a **draft**
26	     release on spt-releases with `spt-x86_64-linux`,
27	     `spt-x86_64-windows.exe`, `SHA256SUMS`, `manifest.schema.json`,
28	     `mock-adapter.zip`;
29	   - `docs-publish.yml` — docs tree + repo-face sync to spt-releases.
30	3. **Sign + publish** (local, one command):
31
32	   ```sh
33	   SPT_RELEASE_SEED_CMD='<your password-manager CLI read command>' \
34	     cargo run -p xtask -- release-publish \
35	       --tag v0.X.Y --key-id rel-primary-2026 --version <N>
36	   ```
37
38	   It downloads the draft's binaries, **verifies them against SHA256SUMS
39	   before signing**, signs each (`*.release.json` in the exact
40	   `SignedRelease` shape `spt update` verifies), uploads the metadata
41	   assets, and flips the draft public. `--version <N>` is the **monotonic
42	   release counter** (u64; strictly increasing across releases — it is not
43	   the semver). `SPT_RELEASE_SEED` (raw hex) works instead of
44	   `_CMD` when pasting from the manager by hand.
45
46	## Notes
47
48	- The wire-protocol version (REQ-ARCH-3) and the release counter and the
49	  workspace semver are three independent numbers. Never conflate.
50	- The installer scripts trust HTTPS + `SHA256SUMS` on first fetch; the
51	  `*.release.json` metadata is what `spt update` verifies thereafter against
52	  the embedded two-key anchor.
53	- Revoking a leaked key without a rebuild: add its id to `"revoked"` in
54	  `identity/release-keys.json` on each node (the file overlays the builtin
55	  set); ship the next release signed by the other key.
56

# Release runbook

> M6-D6/D7 (ADR-0015). How a release ships. CI builds; the maintainer signs
> locally — release keys never enter CI.

<!-- [doc->REQ-REL-2] -->

## One-time setup (done 2026-06-05)

- Key ceremony: `cargo run -p xtask -- release-keygen rel-primary-2026` (and
  `rel-recovery-2026`); both seeds in the password manager (+ separate paper
  backup); both PUBLIC keys embedded in `BUILTIN_RELEASE_KEYS`
  (`crates/spt-daemon/src/release.rs`). The recovery key is never used until
  the primary is lost/compromised — then it signs the next release, which
  rotates in a fresh primary.
- `RELEASES_TOKEN` repo secret on spt-core: fine-grained PAT, Contents
  read/write on `SaberMage/spt-releases` only.

## Per release

1. **Bump** `[workspace.package] version` (root `Cargo.toml`) if needed;
   regenerate generated docs (`cargo run -p xtask -- gen`); land everything
   green.
2. **Tag**: `git tag v0.X.Y && git push origin v0.X.Y`. This triggers:
   - `release.yml` — both runners build, the assemble job creates a **draft**
     release on spt-releases with `spt-x86_64-linux`,
     `spt-x86_64-windows.exe`, `SHA256SUMS`, `manifest.schema.json`,
     `mock-adapter.zip`;
   - `docs-publish.yml` — docs tree + repo-face sync to spt-releases.
3. **Sign + publish** (local, one command):

   ```sh
   SPT_RELEASE_SEED_CMD='<your password-manager CLI read command>' \
     cargo run -p xtask -- release-publish \
       --tag v0.X.Y --key-id rel-primary-2026 --version <N>
   ```

   It downloads the draft's binaries, **verifies them against SHA256SUMS
   before signing**, signs each (`*.release.json` in the exact
   `SignedRelease` shape `spt update` verifies), uploads the metadata
   assets, and flips the draft public. `--version <N>` is the **monotonic
   release counter** (u64; strictly increasing across releases — it is not
   the semver). `SPT_RELEASE_SEED` (raw hex) works instead of
   `_CMD` when pasting from the manager by hand.

## Notes

- The wire-protocol version (REQ-ARCH-3) and the release counter and the
  workspace semver are three independent numbers. Never conflate.
- The installer scripts trust HTTPS + `SHA256SUMS` on first fetch; the
  `*.release.json` metadata is what `spt update` verifies thereafter against
  the embedded two-key anchor.
- Revoking a leaked key without a rebuild: add its id to `"revoked"` in
  `identity/release-keys.json` on each node (the file overlays the builtin
  set); ship the next release signed by the other key.


Process: I can do steps 1–2 (bump, regen docs, land, tag, push → CI builds draft). Step 3 (sign+publish) needs your password-manager release seed — that's yours. First, current version state:


Bash

cd C:/Users/decid/Documents/projects/spt-core && echo "=== workspace version ===" && grep -A2 "\[workspace.package\]" Cargo.toml | grep -v "LF will be" && echo "=== existing tags ===" && git tag -l "v*" | grep -v "LF will be" && echo "=== last release counter N (search for v0.1.0 release counter / version) ===" && git log --oneline -15 | grep -iE "release|v0\.1|bump|version" | grep -v "LF will be" && echo "=== CHANGELOG head ===" && (head -30 CHANGELOG.md 2>/dev/null | grep -v "LF will be" || echo "no CHANGELOG.md") && echo "=== branch ===" && git branch --show-current

Current version, tags, changelog, branch


Error: Exit code 1
=== workspace version ===
[workspace.package]
version = "0.1.0"
=== existing tags ===
v0.1.0
=== last release counter N (search for v0.1.0 release counter / version) ===


cd C:/Users/decid/Documents/projects/spt-core && echo "=== branch + main divergence ===" && git branch --show-current && echo "dev-freeform vs main:" && git rev-list --left-right --count origin/main...dev-freeform 2>/dev/null || git rev-list --left-right --count main...dev-freeform 2>/dev/null || echo "(no main ref)" && echo "=== v0.1.0 tag commit ===" && git log -1 --oneline v0.1.0 2>/dev/null | grep -v "LF will be" && echo "=== CHANGELOG? ===" && ls CHANGELOG* 2>/dev/null || echo "no changelog file" && echo "=== release counter for v0.1.0 (grep history/docs) ===" && grep -rn "release-publish\|--version 1\|release counter" docs/ 2>/dev/null | grep -v "LF will be" | head -5 && echo "=== git log dev-freeform not in main (what would ship in v0.1.1) ===" && git log --oneline main..dev-freeform 2>/dev/null | grep -v "LF will be" | head -30

Branch divergence, v0.1.0 tag, changelog, release counter, unshipped commits


=== branch + main divergence ===
dev-freeform
dev-freeform vs main:
0	276
=== v0.1.0 tag commit ===
796faa7 ci: release/docs-publish first-tag fixes — no bash on hfenduleam, no jq on gravity
=== CHANGELOG? ===
no changelog file
=== release counter for v0.1.0 (grep history/docs) ===
docs/adr/0016-platform-targeted-update-sets.md:21:Release monotonicity is scoped to the pinned channel. Stable releases and debug rollouts use separate counters, so a fast debug sequence cannot consume, block, or roll back the stable release counter; rollback protection compares only against the latest accepted version for the recipient's pinned channel.
docs/RELEASE-RUNBOOK.md:34:     cargo run -p xtask -- release-publish \
docs/RELEASE-RUNBOOK.md:42:   release counter** (u64; strictly increasing across releases — it is not
docs/RELEASE-RUNBOOK.md:48:- The wire-protocol version (REQ-ARCH-3) and the release counter and the
=== git log dev-freeform not in main (what would ship in v0.1.1) ===
66b3570 docs(deferred): debug-converge watcher build plan — REQ-UPD-6 first-slice tail
a0e36d3 feat(update): platform-targeted update sets + debug rollout (REQ-UPD-6, ADR-0016)
5c3ebb1 style: rustfmt 1.8.0 whole-tree reflow
6aace53 fix(runtime): CREATE_NO_WINDOW in command_for — close the KH 5.8 residual
b90820c fix(store,daemon,runtime): CREATE_NO_WINDOW on console children of the daemon
b9326f4 fix(cli): subnet status membership is trust-derived — rowless members render
45b1e17 fix(net,daemon): late-code join retry — the ±1 window now covers user latency
bedb0dd docs(deferred): elevated spt-hosted endpoints — consented elevation satellite
c7e7d9e fix(daemon,cli): token-restricted flags for CreateProcessWithTokenW + DEELEVATE_FAIL aborts
ea0268b fix(daemon): de-elevate via the desktop SHELL token — linked token fails 1346
b3b8104 fix(cli): FORCE_COLOR opts piped epilogues back into blue
d4e9314 fix(cli): success epilogues print legacy-blue — stderr success looked like errors
4aea97e fix(daemon): unix spawn_detached starts the daemon in a NEW SESSION (setsid)
24a9a80 docs(m7): [twohost] M7 closeout — plan COMPLETE + ROADMAP delivered tick
93266e3 docs(deferred): flip the two publish-blockers + AMBIGUOUS render to SHIPPED
e5de2d4 fix(daemon,cli): [twohost] non-admin daemon spawn — de-elevation seam (KH 5.7)
4046e6d fix(cli): resolution refusals render copy-paste targets, not debug dumps
048a8bc fix(net,daemon): silent-peer registry eviction — ghost rows decay (KH 4.10)
fc745d0 docs(reqs): register KH 4.10 ghost-rows + 5.7 elevated-spawn hazards (rule 3)
5705268 docs(deferred): M7 acceptance harvest — two publish-blocking next-session fixes + AMBIGUOUS render
5c0db0d fix(cli): [twohost] human-readable join epilogue + first-sync honesty
4b2b083 fix(net,daemon): rendezvous race — bounded parallel meet probes + window overlap + sweep retry
df5928a debug(daemon): meet-rotation window diagnostics + DEFERRED entries from the acceptance run
b5e2485 fix(cli): surface the rendezvous failure detail on NO_SEED_HOLDER
d6c9543 feat(cli): M7 UX revision — show-totp -> show-code, humanized ceremony lines, code expiry
eeb6f01 feat(cli): membership implies reachability — daemon activation + coming-online banner
febea12 docs(site): sweep install-dir references to %LOCALAPPDATA%\spt-core\bin
acd9a71 fix(installer): Windows install dir spt\bin -> spt-core\bin + v0.1.0 migration
7a22a63 fix(daemon): M7 [twohost] — responder must let the joiner close first (Done/Seed close race)
c152807 test(ci): M7 D5 [twohost] — product-surface join rung + quickstart e2e rewrite


Key facts before anything irreversible:
- **`dev-freeform` is 276 ahead of `main`, 0 behind.** main is stuck at the v0.1.0 era. v0.1.1 off dev-freeform head would ship all of M7 + REQ-UPD-6 + today's fixes.
- Workspace version still `0.1.0` (needs bump). No CHANGELOG file. v0.1.0 tag at `796faa7`.
- Release counter N for v0.1.0 not recorded in-repo (likely N=1 → v0.1.1 = N=2; confirm).

Tagging+pushing triggers a public draft release (CI). Before that — check CI health on head + settle the branch/counter decisions. Checking CI:


cd C:/Users/decid/Documents/projects/spt-core && echo "=== CI status on dev-freeform head (66b3570) ===" && gh run list --branch dev-freeform --limit 5 2>&1 | head -8 ; echo "=== is head pushed to origin? ===" && git rev-parse HEAD 2>/dev/null && git rev-parse origin/dev-freeform 2>/dev/null && echo "=== release.yml present? ===" && ls .github/workflows/release.yml .github/workflows/docs-publish.yml 2>/dev/null | grep -v "LF will be"

CI status + push state + workflow presence


=== CI status on dev-freeform head (66b3570) ===
completed	success	fix(cli): subnet status membership is trust-derived — rowless members…	ci	dev-freeform	push	27075219875	9m8s	2026-06-06T22:08:26Z
completed	success	docs(deferred): elevated spt-hosted endpoints — consented elevation s…	ci	dev-freeform	push	27074800057	8m46s	2026-06-06T21:48:22Z
completed	cancelled	fix(daemon,cli): token-restricted flags for CreateProcessWithTokenW +…	ci	dev-freeform	push	27074684847	5m42s	2026-06-06T21:42:57Z
completed	success	fix(daemon): de-elevate via the desktop SHELL token — linked token fa…	ci	dev-freeform	push	27063172876	8m10s	2026-06-06T13:09:11Z
completed	success	fix(cli): FORCE_COLOR opts piped epilogues back into blue	ci	dev-freeform	push	27062849884	8m26s	2026-06-06T12:54:12Z
=== is head pushed to origin? ===
66b3570dd4c05ef8635f353639844344e5ba4a46
b9326f48d4141e3701d5b038388fa104f70253e5
=== release.yml present? ===
.github/workflows/docs-publish.yml
.github/workflows/release.yml


Critical: **origin/dev-freeform is at `b9326f4` — my 5 new commits (incl. b90820c, KH5.8, REQ-UPD-6) are unpushed, and CI has NOT validated them.** Last green CI = b9326f4. Runbook demands "land everything green" before tagging — so I must push + confirm CI green first.

Let me pin the release counter from the actual v0.1.0 release metadata (avoid guessing):


cd C:/Users/decid/Documents/projects/spt-core && echo "=== v0.1.0 release assets on spt-releases (find release.json version) ===" && gh release view v0.1.0 --repo SaberMage/spt-releases --json tagName,assets 2>&1 | head -20 ; echo "=== try fetch a *.release.json to read its version counter ===" && gh release download v0.1.0 --repo SaberMage/spt-releases --pattern "*.release.json" --dir /tmp/rel010 --clobber 2>&1 | head -3 ; ls /tmp/rel010 2>/dev/null && for f in /tmp/rel010/*.release.json; do echo "--- $f ---"; cat "$f" 2>/dev/null | python -c "import sys,json; d=json.load(sys.stdin); m=json.loads(d['metadata_json']); print('version_counter=',m.get('version'),'channel=',m.get('channel'))" 2>/dev/null || cat "$f" | head -c 300; done

Read v0.1.0 release counter from published metadata


Release counter confirmed from published metadata: **v0.1.0 = counter 1 → v0.1.1 = N=2**, channel stable. v0.1.0 shipped the full asset set cleanly, so the pipeline works.

All facts in hand. Two decisions are yours before I touch anything irreversible:


AskUserQuestion

Question: main is 276 commits behind dev-freeform (0 ahead = clean fast-forward). Where should v0.1.1 be tagged?

Choices:
FF main → head, tag on main (recommended)
Fast-forward main up to dev-freeform head (trivial, no conflicts since main is 0 ahead), tag v0.1.1 on main. Keeps main = latest released line, clean release hygiene
Tag v0.1.1 directly on dev-freeform head; leave main untouched at v0.1.0. Simpler now, but main keeps drifting behind

Question: v0.1.1 from head (66b3570) ships all 276 commits ahead of main — all of M7 (two-host net, de-elevation, ghost eviction) + today's REQ-UPD-6 debug rollout + rustfmt reflow + KH 5.8 fix. Ship that whole set as v0.1.1?

Choices:
All 276 commits. REQ-UPD-6 is additive maintainer tooling (xtask, not public CLI) + tested daemon substrate; rest is M7 fixes already CI-green through b9326f4. My 5 new commits get pushed + CI-validated first
Tag a narrower point (e.g. b9326f4, the pre-REQ-UPD-6 'ready' mark from the memory). I'll ask which commit


Your questions have been answered: "main is 276 commits behind dev-freeform (0 ahead = clean fast-forward). Where should v0.1.1 be tagged?"="FF main → head, tag on main (recommended)", "v0.1.1 from head (66b3570) ships all 276 commits ahead of main — all of M7 (two-host net, de-elevation, ghost eviction) + today's REQ-UPD-6 debug rollout + rustfmt reflow + KH 5.8 fix. Ship that whole set as v0.1.1?"="Ship current head as v0.1.1". You can now continue with these answers in mind.

Question: main is 276 commits behind dev-freeform (0 ahead = clean fast-forward). Where should v0.1.1 be tagged?

Choices:
FF main → head, tag on main (recommended)
Fast-forward main up to dev-freeform head (trivial, no conflicts since main is 0 ahead), tag v0.1.1 on main. Keeps main = latest released line, clean release hygiene
Tag v0.1.1 directly on dev-freeform head; leave main untouched at v0.1.0. Simpler now, but main keeps drifting behind

Question: v0.1.1 from head (66b3570) ships all 276 commits ahead of main — all of M7 (two-host net, de-elevation, ghost eviction) + today's REQ-UPD-6 debug rollout + rustfmt reflow + KH 5.8 fix. Ship that whole set as v0.1.1?

Choices:
All 276 commits. REQ-UPD-6 is additive maintainer tooling (xtask, not public CLI) + tested daemon substrate; rest is M7 fixes already CI-green through b9326f4. My 5 new commits get pushed + CI-validated first
Tag a narrower point (e.g. b9326f4, the pre-REQ-UPD-6 'ready' mark from the memory). I'll ask which commit

FF main → head, tag on main (recommended)


Decisions locked: **FF main → head, tag v0.1.1 on main; ship current head (all 276 commits).** N=2.

Execution plan: bump version → regen docs → commit → push dev-freeform → **wait for CI green** → FF main → tag v0.1.1 → push tag (triggers draft) → hand to you for signing.

Step 1 — bump version. Check how versions are declared:


Bash

cd C:/Users/decid/Documents/projects/spt-core && echo "=== root [workspace.package] ===" && sed -n '/\[workspace.package\]/,/^\[/p' Cargo.toml | grep -v "LF will be" | head -10 && echo "=== crates using version.workspace ===" && grep -rl "version.workspace = true" crates adapters --include=Cargo.toml 2>/dev/null | grep -v "LF will be" | wc -l && echo "=== crates with OWN version = ===" && grep -rn "^version = " crates/*/Cargo.toml adapters/*/Cargo.toml 2>/dev/null | grep -v "LF will be" | head

Inspect version declaration scheme


=== root [workspace.package] ===
[workspace.package]
version = "0.1.0"
=== crates using version.workspace ===
11
=== crates with OWN version = ===


All 11 crates inherit `version.workspace` — single bump suffices. Bump to 0.1.1:


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


1	# spt-core cargo workspace (M0). Acyclically-layered crates per R-ARCH-1:
2	#   spt-proto -> spt-store -> spt-msg -> {spt-net, spt-term, spt-runtime} -> spt-live -> spt-daemon -> spt
3	# M0 builds the bottom two: spt-proto (wire) + spt-store (persistence).
4	# M1 adds spt-msg (local messaging) — third layer, one-way above spt-store.
5	# M2a adds spt-runtime (harness contract: manifest schema + AgentRuntime) — fourth layer.
6	# M2b adds spt-live (live-agent lifecycle executor) — fifth layer, daemon-bound (not public SDK).
7	# M3a adds spt-term (session-surface mechanism: ConPTY/forkpty PTY layer) — sibling of spt-runtime, above spt-msg.
8	# M3b adds spt-daemon (broker/brain supervisor) — seventh layer, above spt-live, below spt; internal (not public SDK).
9	# M4 adds spt-net (P2P WAN/LAN transport) — sibling of spt-term/spt-runtime, above spt-msg; internal, default-on `net` feature.
10	[workspace]
11	resolver = "2"
12	members = ["crates/spt-proto", "crates/spt-store", "crates/spt-msg", "crates/spt-net", "crates/spt-term", "crates/spt-runtime", "crates/spt-live", "crates/spt-daemon", "crates/spt", "adapters/mock", "crates/xtask"]
13
14	# M6-D6 (grill decision 9): ONE shared version for every crate and the binary
15	# — the product version (v0.1.0 tag, `spt --version`). The wire-protocol

# spt-core cargo workspace (M0). Acyclically-layered crates per R-ARCH-1:
#   spt-proto -> spt-store -> spt-msg -> {spt-net, spt-term, spt-runtime} -> spt-live -> spt-daemon -> spt
# M0 builds the bottom two: spt-proto (wire) + spt-store (persistence).
# M1 adds spt-msg (local messaging) — third layer, one-way above spt-store.
# M2a adds spt-runtime (harness contract: manifest schema + AgentRuntime) — fourth layer.
# M2b adds spt-live (live-agent lifecycle executor) — fifth layer, daemon-bound (not public SDK).
# M3a adds spt-term (session-surface mechanism: ConPTY/forkpty PTY layer) — sibling of spt-runtime, above spt-msg.
# M3b adds spt-daemon (broker/brain supervisor) — seventh layer, above spt-live, below spt; internal (not public SDK).
# M4 adds spt-net (P2P WAN/LAN transport) — sibling of spt-term/spt-runtime, above spt-msg; internal, default-on `net` feature.
[workspace]
resolver = "2"
members = ["crates/spt-proto", "crates/spt-store", "crates/spt-msg", "crates/spt-net", "crates/spt-term", "crates/spt-runtime", "crates/spt-live", "crates/spt-daemon", "crates/spt", "adapters/mock", "crates/xtask"]

# M6-D6 (grill decision 9): ONE shared version for every crate and the binary
# — the product version (v0.1.0 tag, `spt --version`). The wire-protocol


15	# — the product version (v0.1.0 tag, `spt --version`). The wire-protocol
16	# version stays its own counter (REQ-ARCH-3) and the SignedRelease `version`
17	# is the separate monotonic release counter; neither conflates with this.
18	[workspace.package]
19	version = "0.1.0"
20

# — the product version (v0.1.0 tag, `spt --version`). The wire-protocol
# version stays its own counter (REQ-ARCH-3) and the SignedRelease `version`
# is the separate monotonic release counter; neither conflates with this.
[workspace.package]
version = "0.1.0"


The file C:/Users/decid/Documents/projects/spt-core/Cargo.toml has been updated successfully. (file state is current in your context — no need to Read it back)

# spt-core cargo workspace (M0). Acyclically-layered crates per R-ARCH-1:
#   spt-proto -> spt-store -> spt-msg -> {spt-net, spt-term, spt-runtime} -> spt-live -> spt-daemon -> spt
# M0 builds the bottom two: spt-proto (wire) + spt-store (persistence).
# M1 adds spt-msg (local messaging) — third layer, one-way above spt-store.
# M2a adds spt-runtime (harness contract: manifest schema + AgentRuntime) — fourth layer.
# M2b adds spt-live (live-agent lifecycle executor) — fifth layer, daemon-bound (not public SDK).
# M3a adds spt-term (session-surface mechanism: ConPTY/forkpty PTY layer) — sibling of spt-runtime, above spt-msg.
# M3b adds spt-daemon (broker/brain supervisor) — seventh layer, above spt-live, below spt; internal (not public SDK).
# M4 adds spt-net (P2P WAN/LAN transport) — sibling of spt-term/spt-runtime, above spt-msg; internal, default-on `net` feature.
[workspace]
resolver = "2"
members = ["crates/spt-proto", "crates/spt-store", "crates/spt-msg", "crates/spt-net", "crates/spt-term", "crates/spt-runtime", "crates/spt-live", "crates/spt-daemon", "crates/spt", "adapters/mock", "crates/xtask"]

# M6-D6 (grill decision 9): ONE shared version for every crate and the binary
# — the product version (v0.1.0 tag, `spt --version`). The wire-protocol
# version stays its own counter (REQ-ARCH-3) and the SignedRelease `version`
# is the separate monotonic release counter; neither conflates with this.
[workspace.package]
version = "0.1.0"


Bumped 0.1.0 → 0.1.1. Now regenerate docs (CI-drift-gated) + refresh Cargo.lock:


cd C:/Users/decid/Documents/projects/spt-core && cargo run -q -p xtask -- gen 2>&1 | grep -v "LF will be" | tail -15 ; echo "=== files changed by bump + gen ===" && git status --porcelain 2>/dev/null | grep -v "LF will be"

Regenerate docs + show changed files


Command running in background with ID: bblu5sq9i. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\eb9effe4-c6da-4e0e-9c86-b5e67ffd4772\tasks\bblu5sq9i.output. You will be notified when it completes. To check interim output, use Read on that file path.


Waiting on doc regen. Will commit the bump, push dev-freeform, and watch CI once it lands.


<task-notification>
<task-id>bblu5sq9i</task-id>
<tool-use-id>toolu_011kJ6iiErkbVBr5yULxtKJC</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\eb9effe4-c6da-4e0e-9c86-b5e67ffd4772\tasks\bblu5sq9i.output</output-file>
<status>completed</status>
<summary>Background command "Regenerate docs + show changed files" completed (exit code 0)</summary>
</task-notification>


1	xtask gen: wrote C:\Users\decid\Documents\projects\spt-core\docs-site/src/cli/reference.md
2	=== files changed by bump + gen ===
3	 M Cargo.lock
4	 M Cargo.toml
5

xtask gen: wrote C:\Users\decid\Documents\projects\spt-core\docs-site/src/cli/reference.md
=== files changed by bump + gen ===
 M Cargo.lock
 M Cargo.toml


`gen` produced no doc drift (reference.md rewrote identically) — only `Cargo.toml` + `Cargo.lock` changed (version bump propagated). Verify the lock diff is purely version, then commit:


cd C:/Users/decid/Documents/projects/spt-core && echo "=== Cargo.lock diff (should be only 0.1.0->0.1.1 on workspace crates) ===" && git diff Cargo.lock 2>/dev/null | grep -v "LF will be" | grep -E "^[+-]" | grep -v "^[+-][+-]" | head -30 && echo "=== count of version lines changed ===" && git diff Cargo.lock 2>/dev/null | grep -cE "^\+version = \"0.1.1\""

Verify Cargo.lock diff is version-only