1	- [v0.12.0 lifecycle milestone](v012-lifecycle-milestone.md) — SHIPPED 2026-06-18 (counter 25, MINOR). Operator-mandated BLOCKING: status=online latch reconciled vs liveness (controllable-gated, relay exempt) + stop/restart teardown/reap. 9 items all GATE-PASS; CI escape (Linux /bin/sh→dash basename in reap unit) caught+fixed @6de905b; post-publish changelog corrected (resume-picker = ready-only, +un-host-reap bullet). Roots verified-at-code-first; full seam-bar/wave
2	- [v0.13.0 forkpty ground truth](v013-forkpty-groundtruth.md) — 2026-06-21: combined forkpty CI (b4+skip-to-live+w5_a2+pump-carrier) is RED on Linux, 5 fails. Real core = LIVE attach forward-gap reject on ring-roll-behind ("output gap got 5968 want 4504"); b4+skip-to-live don't cover it (snap-above is eviction-only). a_journaled (b4's own gate) RED on Linux, Windows false-green (ConPTY slower). w5_a2 Cut-B refuted (conn-close clears, not output; loopback can't stage half-open). g2 acceptance fine (delivered=true). My code-gate PASSes refuted by real integration — green-units≠green-integration. Do NOT fold v0.13.0
3	- [v0.13.0 W1b effect-journal wedge](v013-w1b-effect-journal-wedge.md) — REAL operator wedge root (2026-06-19, new build STILL wedges, NOT W1): apply_once holds 1 global mutex across 2 fsync + the blocking PtyWrite (effect.rs:168-188); every keystroke=2 sync_all (measured 6.5ms med/198ms spike) → stutter, and a blocked write holds the lock forever → all attaches die 'brain IPC read deadline'. W1 was a GATE ESCAPE (only checked KIND_SESSIONS, never real rc PTY delivery). Minted REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE, dispatched todlando as W1b = W2 Layer C foundation. Fix=(1)no-lock-across-effect+(3)drop per-keystroke fsync (PtyWrite ephemeral)
4	- [v0.12.1 real-harness reopen](v0121-realharness-reopen.md) — v0.12.0 SHIPPED but lifecycle fixes DON'T work in the real claude-spt harness (gated on mocks/in-proc → green-but-broken). Reopened v0.12.1 (branch+JIT+5 REQs @a4a76c6). Roots: VIEWER-CLOSE-DETACH (missing CREATE_BREAKAWAY_FROM_JOB → terminal Job Object reaps daemon's harness on tab-close) + ATTACH-WEDGE (dead PTY child + undrained pump blocks 2-worker loopback runtime). BINDING: gate every lifecycle fix vs a REAL dummy-harness fixture + real daemon, NO mocks. Invariant "a view is independent from the endpoint" in CONTEXT.md. On legacy spt here = spt-core freely restartable
5	- [v0.12.1 L1 viewer-close-detach findings](v0121-l1-viewer-close-detach-findings.md) — breakaway impl+units LANDED @5ae68f8 (doc+impl+unit; CREATE_BREAKAWAY_FROM_JOB on both daemon spawn paths + ACCESS_DENIED fallback). LOAD-BEARING FINDING: cargo/CI runner's OWN job FORBIDS breakaway (proven, code 5) → int gate can't run faithfully in CI (nesting=false-green) AND a real job CAN deny breakaway, so IF WT/VSCode denies it breakaway alone won't fix it. int BLOCKED on doyle ruling (sent) + OPERATOR real-terminal test (endpoint run→close tab→rc must survive). harness-Job backstop→L4. NEXT independent=L2 ATTACH-WEDGE

- [v0.12.0 lifecycle milestone](v012-lifecycle-milestone.md) — SHIPPED 2026-06-18 (counter 25, MINOR). Operator-mandated BLOCKING: status=online latch reconciled vs liveness (controllable-gated, relay exempt) + stop/restart teardown/reap. 9 items all GATE-PASS; CI escape (Linux /bin/sh→dash basename in reap unit) caught+fixed @6de905b; post-publish changelog corrected (resume-picker = ready-only, +un-host-reap bullet). Roots verified-at-code-first; full seam-bar/wave
- [v0.13.0 forkpty ground truth](v013-forkpty-groundtruth.md) — 2026-06-21: combined forkpty CI (b4+skip-to-live+w5_a2+pump-carrier) is RED on Linux, 5 fails. Real core = LIVE attach forward-gap reject on ring-roll-behind ("output gap got 5968 want 4504"); b4+skip-to-live don't cover it (snap-above is eviction-only). a_journaled (b4's own gate) RED on Linux, Windows false-green (ConPTY slower). w5_a2 Cut-B refuted (conn-close clears, not output; loopback can't stage half-open). g2 acceptance fine (delivered=true). My code-gate PASSes refuted by real integration — green-units≠green-integration. Do NOT fold v0.13.0
- [v0.13.0 W1b effect-journal wedge](v013-w1b-effect-journal-wedge.md) — REAL operator wedge root (2026-06-19, new build STILL wedges, NOT W1): apply_once holds 1 global mutex across 2 fsync + the blocking PtyWrite (effect.rs:168-188); every keystroke=2 sync_all (measured 6.5ms med/198ms spike) → stutter, and a blocked write holds the lock forever → all attaches die 'brain IPC read deadline'. W1 was a GATE ESCAPE (only checked KIND_SESSIONS, never real rc PTY delivery). Minted REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE, dispatched todlando as W1b = W2 Layer C foundation. Fix=(1)no-lock-across-effect+(3)drop per-keystroke fsync (PtyWrite ephemeral)
- [v0.12.1 real-harness reopen](v0121-realharness-reopen.md) — v0.12.0 SHIPPED but lifecycle fixes DON'T work in the real claude-spt harness (gated on mocks/in-proc → green-but-broken). Reopened v0.12.1 (branch+JIT+5 REQs @a4a76c6). Roots: VIEWER-CLOSE-DETACH (missing CREATE_BREAKAWAY_FROM_JOB → terminal Job Object reaps daemon's harness on tab-close) + ATTACH-WEDGE (dead PTY child + undrained pump blocks 2-worker loopback runtime). BINDING: gate every lifecycle fix vs a REAL dummy-harness fixture + real daemon, NO mocks. Invariant "a view is independent from the endpoint" in CONTEXT.md. On legacy spt here = spt-core freely restartable
- [v0.12.1 L1 viewer-close-detach findings](v0121-l1-viewer-close-detach-findings.md) — breakaway impl+units LANDED @5ae68f8 (doc+impl+unit; CREATE_BREAKAWAY_FROM_JOB on both daemon spawn paths + ACCESS_DENIED fallback). LOAD-BEARING FINDING: cargo/CI runner's OWN job FORBIDS breakaway (proven, code 5) → int gate can't run faithfully in CI (nesting=false-green) AND a real job CAN deny breakaway, so IF WT/VSCode denies it breakaway alone won't fix it. int BLOCKED on doyle ruling (sent) + OPERATOR real-terminal test (endpoint run→close tab→rc must survive). harness-Job backstop→L4. NEXT independent=L2 ATTACH-WEDGE


Edit

- [v0.12.0 lifecycle milestone](v012-lifecycle-milestone.md)

- [v0.13.2 build drive](v0132-build-drive.md) — driving v0.13.2 adapter-packaging build→release (2026-06-22); branch reconciled @69c975f; dispatch live agents via `$OWL send`; W1→W2→W3-keystone→W4→W5; W3 design HOLE = adapter psyche binary locks shared install-dir (perri F-010×F-015), task#7 blocks W3
- [v0.12.0 lifecycle milestone](v012-lifecycle-milestone.md)


The file C:\Users\decid\.ccs\instances\bigscreen\projects\C--Users-decid-Documents-projects-spt-core\memory\MEMORY.md has been updated successfully. (file state is current in your context — no need to Read it back)

- [v0.13.2 build drive](v0132-build-drive.md) — driving v0.13.2 adapter-packaging build→release (2026-06-22); branch reconciled @69c975f; dispatch live agents via `$OWL send`; W1→W2→W3-keystone→W4→W5; W3 design HOLE = adapter psyche binary locks shared install-dir (perri F-010×F-015), task#7 blocks W3
- [v0.12.0 lifecycle milestone](v012-lifecycle-milestone.md)

+- [v0.13.2 build drive](v0132-build-drive.md) — driving v0.13.2 adapter-packaging build→release (2026-06-22); branch reconciled @69c975f; dispatch live agents via `$OWL send`; W1→W2→W3-keystone→W4→W5; W3 design HOLE = adapter psyche binary locks shared install-dir (perri F-010×F-015), task#7 blocks W3

- [v0.12.0 lifecycle milestone](v012-lifecycle-milestone.md) — SHIPPED 2026-06-18 (counter 25, MINOR). Operator-mandated BLOCKING: status=online latch reconciled vs liveness (controllable-gated, relay exempt) + stop/restart teardown/reap. 9 items all GATE-PASS; CI escape (Linux /bin/sh→dash basename in reap unit) caught+fixed @6de905b; post-publish changelog corrected (resume-picker = ready-only, +un-host-reap bullet). Roots verified-at-code-first; full seam-bar/wave

- [v0.13.0 forkpty ground truth](v013-forkpty-groundtruth.md) — 2026-06-21: combined forkpty CI (b4+skip-to-live+w5_a2+pump-carrier) is RED on Linux, 5 fails. Real core = LIVE attach forward-gap reject on ring-roll-behind ("output gap got 5968 want 4504"); b4+skip-to-live don't cover it (snap-above is eviction-only). a_journaled (b4's own gate) RED on Linux, Windows false-green (ConPTY slower). w5_a2 Cut-B refuted (conn-close clears, not output; loopback can't stage half-open). g2 acceptance fine (delivered=true). My code-gate PASSes refuted by real integration — green-units≠green-integration. Do NOT fold v0.13.0

- [v0.13.0 W1b effect-journal wedge](v013-w1b-effect-journal-wedge.md) — REAL operator wedge root (2026-06-19, new build STILL wedges, NOT W1): apply_once holds 1 global mutex across 2 fsync + the blocking PtyWrite (effect.rs:168-188); every keystroke=2 sync_all (measured 6.5ms med/198ms spike) → stutter, and a blocked write holds the lock forever → all attaches die 'brain IPC read deadline'. W1 was a GATE ESCAPE (only checked KIND_SESSIONS, never real rc PTY delivery). Minted REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE, dispatched todlando as W1b = W2 Layer C foundation. Fix=(1)no-lock-across-effect+(3)drop per-keystroke fsync (PtyWrite ephemeral)


perri triaged + captured. Status:

**perri's bug = live W3 motivating evidence.** Replied (classified F-010×F-015 lifecycle, real spt-core bug). Surfaced the design-critical bit: the locker is the adapter **psyche binary** running from the **shared install-dir** — ADR-0025 assumed psyche is in-daemon and excluded it from W3 cycling, so the keystone would ship with a hole. Disposition folded into W3: (a) orphan-psyche reap on stop/death, (c) psyche-from-own-copy, (b) extract-to-temp atomic swap. perri's repro becomes the W3 NO-mocks fixture + her ship-acceptance test.

**Captured:** task #7 (blocks W3), cross-session memory written.

**In flight:** todlando building W1. Waiting on its gate-packet report via owl. Then I gate → W2 → W3 (now with the psyche-lock requirement) → W4 → W5 → deployah cuts v0.13.2. Reachable on `doyle`.


<task-notification>
<task-id>b3ok5tzh4</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/8" id="3974baac" type="msg" from="perri">Great that it's load-bearing on W3/ADR-0025 — and yes, the crux is exactly that: claude-spt's psyche is a SEPARATE install-dir binary, not in-process (ADR-0004's assumption). The resident/ephemeral split has to treat the detached psyche as a lock vector on the shared target. (c) psyche-from-own-copy is the cleanest st</EVENT-PART>
<EVENT-PART seq="2/8" id="3974baac">ructural kill — it removes the vector independent of reap timing; (a) reap closes the F-010 root; (b) atomic-swap is the backstop. All three compose well.

The live orphan is GONE (I killed it during recovery), so this is reconstructed from my captures — but it's complete. Scanned the node: NO other stray claude-spt/cc-spt processes </EVENT-PART>
<EVENT-PART seq="3/8" id="3974baac">remain; wall-aa/b/c are dormant (alive=false, no psyche). Only wall-a had one.

W3 FIXTURE SPEC (real orphan + locked install-dir, no mocks):
- Orphan: claude-spt-psyche.exe pid 181272, CreationDate 6/21 23:51, lingered ~4h until manual kill.
- Parent: pid 152200 — DEAD at capture time (Get-CimInstance ProcessId=152200 → empty)</EVENT-PART>
<EVENT-PART seq="4/8" id="3974baac">. Detached child outlived it.
- Path (the LOCK TARGET): …\AppData\Local\spt-core\adapters\_github\SaberMage-spt-claude-code\claude-spt-psyche.exe (shared install dir).
- CmdLine: claude-spt-psyche.exe --id wall-a-psyche --session-id a8655f3e-6789-4ffe-9991-dbfd52cb17d5 --prompt &quot;PSYCHE REVIVAL\ntime: epoch-ms:1782111064102\nincomi</EVENT-PART>
<EVENT-PART seq="5/8" id="3974baac">ng event: (none)&quot;
- Detached per manifest [session.psyche_init] detach=true (Stdio::null, unsupervised). Hosted by the daemon livehost off wall-a's live_agent online status.
- Install dir (lock surface): manifest.toml + claude-spt-digest.exe + claude-spt-psyche.exe + cc-spt-idle-translate.exe + strings/.

REPRO RECIPE (determi</EVENT-PART>
<EVENT-PART seq="6/8" id="3974baac">nistic, no mocks):
1. Bring up a claude-spt live agent wall-X → daemon spawns claude-spt-psyche from the install dir, detached.
2. Kill the psyche's PARENT (livehost/agent), leaving the detached psyche running → parent-dead orphan.
3. `spt endpoint stop wall-X` → perch alive=false, BUT psyche survives (the reap gap).
4. `spt </EVENT-PART>
<EVENT-PART seq="7/8" id="3974baac">adapter update claude-spt` (or add --release) → tar overwrites locked claude-spt-psyche.exe → exit Some(1) → WHOLE update fails.
Pass criteria post-W3: step 3 reaps the orphan AND/OR the psyche never ran from the shared target AND/OR locked-file extract no longer fails the whole update.

Holding for the v0.13.2 build — I'll re</EVENT-PART>
<EVENT-PART seq="8/8" id="3974baac">-run this exact repro as the acceptance test the moment you ping. Shout if you want anything else for the fixture.</EVENT-PART></event>
</task-notification>


<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.


perri delivered the complete W3 fixture spec + deterministic repro. Gold for the keystone. Fold into task #7, short ack (she's holding).


TaskUpdate

perri live repro 2026-06-22 (spt 0.13.1, claude-spt 0.6.0, Win): claude-spt-psyche.exe (pid 181272, --id wall-a-psyche) ran DETACHED from shared install-dir, parent (152200) DEAD = orphan (~4h), held binary lock → adapter update/add --release failed (tar exit Some(1)). endpoint stop wall-a set alive=false but did NOT reap the detached psyche.

ROOT MECHANICS: psyche is detached per manifest [session.psyche_init] detach=true (Stdio::null, unsupervised), hosted by daemon livehost off the live_agent online status. It runs from the SHARED install dir (lock surface = manifest.toml + claude-spt-digest.exe + claude-spt-psyche.exe + cc-spt-idle-translate.exe + strings/).

LOAD-BEARING ON W3: ADR-0025 resident/ephemeral split assumed psyche=daemon-hosted in-process (ADR-0004), EXCLUDED from cycling. But it's a SEPARATE install-dir binary → unsupervised locker W3's resident-only stop/restart won't release. Fixes compose: (c) psyche-from-own-copy not shared dir = cleanest structural kill, removes vector independent of reap timing; (a) orphan reap on stop/agent-death = F-010 root close; (b) extract-to-temp+atomic-swap = F-015 backstop (one locked file ≠ whole-update fail).

W3 INT FIXTURE (perri spec, deterministic, NO mocks): 1. bring up claude-spt live agent wall-X → daemon spawns detached psyche from install dir; 2. kill psyche PARENT (livehost/agent) → parent-dead orphan; 3. spt endpoint stop wall-X → alive=false BUT psyche survives (reap gap); 4. spt adapter update claude-spt → tar overwrites locked psyche.exe → exit 1 → whole update fails. PASS post-W3: step 3 reaps orphan AND/OR psyche never ran from shared target AND/OR locked-file extract no longer fails whole update. perri re-runs this exact repro as the ship acceptance test on doyle ping.


Updated task #7 description


Bash

"$OWL" send perri doyle <<'EOF'
Got it — full fixture spec + the 4-step deterministic repro captured verbatim as the W3 build/gate target (detach=true livehost-hosted psyche, shared install-dir lock surface, parent-dead orphan, the reap-gap on endpoint stop). That's exactly the no-mocks int I'll hold W3 to. The three-fix composition (c structural + a F-010 reap + b atomic-swap backstop) matches my read; I'll settle the precise cut at W3 design once W1 lands.

Nothing more needed for the fixture. Holding you for the ship ping — you re-run this repro as the acceptance test. Thanks for the clean reconstruction.
EOF
echo "---sent exit $?---"

Ack perri fixture spec


➜ SENT:perri
---sent exit 0---


W3 fixture spec locked into task #7 (4-step deterministic repro, detach=true livehost psyche, shared install-dir lock surface, reap-gap). perri acked + holding for ship-acceptance.

Back to the critical path: todlando building W1. Waiting on its gate-packet. Reachable on `doyle`.


<task-notification>
<task-id>b3ok5tzh4</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/6" id="322758b8" type="msg" from="todlando">v0.13.2 W1 DONE @281176b (pushed v0.13.2-adapter-packaging) — multi-platform .spt, REQ-ADAPTER-MULTIPLATFORM-SPT / ADR-0024. Ready for your gate.

SCOPE delivered (exactly W1):
- KNOWN_TARGET_TRIPLES const in release.rs (ADR-0016 triple vocab, single source); detection keys on it -&gt; never misfires on a </EVENT-PART>
<EVENT-PART seq="2/6" id="322758b8">flat archive.
- extract_release_archive reworked: stage -&gt; classify_archive_layout by real top-level dirents -&gt; legacy-flat extracts as today (free back-compat) OR shared root + ONLY current_platform()'s triple subdir flattened into install_dir (REQ-INSTALL-11 flat resolution unchanged). Missing-my-triple in a multi-platform archive -</EVENT-PART>
<EVENT-PART seq="3/6" id="322758b8">&gt; typed ExtractError::NoArtifactForPlatform (mirrors ADR-0016), never silent no-op.
- --asset already defaults adapter.spt (cli.rs:5501/5789) — confirmed, left correct. verify_staged_archive (one whole-archive sig) untouched. min_spt_core_version forward gate untouched.

EVIDENCE:
- REQ activated required_stages=[doc,impl,unit</EVENT-PART>
<EVENT-PART seq="4/6" id="322758b8">,int]; tags on real evidence ([impl/unit/int-&gt;REQ-ADAPTER-MULTIPLATFORM-SPT]).
- Unit: classifier flat-never-misfires; only-known-triples captured (bogus darwin-foo excluded, dedup). Int: REAL tar-built .spt fixtures — right-triple flatten + shared root; missing-triple typed rejection (no silent no-op); legacy-flat stays flat. Non-vacu</EVENT-PART>
<EVENT-PART seq="5/6" id="322758b8">ity proven (removing the flatten step fails int test 3).
- cargo clippy --workspace --all-targets -D warnings: clean. traceable-reqs check: exit 0 (288/288). xtask check (docs-drift): OK. spt suite: 215/215 incl 5 new W1 tests. release:: module: 18/18.

CAVEAT for your gate: the spt-daemon lib lifecycle tests (seedmap/serveprobe/shell</EVENT-PART>
<EVENT-PART seq="6/6" id="322758b8">wake) DEADLOCK on this contended Windows host (live spt-core daemons present + load) — env, not the diff (const-only daemon change; all targets compile under clippy --all-targets; release:: 18/18). Lean on CI green both runners as the authority for that suite. No daemon paths touched.</EVENT-PART></event>
</task-notification>


todlando: W1 done @281176b, pushed. Now I gate — read the real diff, verify non-vacuity, re-run gates myself (green-units≠green-real). Fetch + inspect:


git fetch origin v0.13.2-adapter-packaging --quiet 2>&1 | tail -2; git checkout v0.13.2-adapter-packaging --quiet 2>&1 | tail -2; git pull --ff-only --quiet 2>&1 | tail -3; echo "===HEAD==="; git log --oneline -3 | cat; echo "===diff stat vs merge-base 69c975f==="; git diff --stat 69c975f..281176b | cat

Fetch W1 commit + diff stat


===HEAD===
281176b feat(adapter): multi-platform .spt packaging (W1, ADR-0024)
69c975f Merge branch 'main' into v0.13.2-adapter-packaging
7de5cfb Merge branch 'main' of https://github.com/SaberMage/spt-core
===diff stat vs merge-base 69c975f===
 crates/spt-daemon/src/release.rs |  10 +
 crates/spt/src/cli.rs            | 428 ++++++++++++++++++++++++++++++++++++++-
 traceable-reqs.toml              |   2 +-
 3 files changed, 433 insertions(+), 7 deletions(-)


git diff 69c975f..281176b -- crates/spt-daemon/src/release.rs traceable-reqs.toml | cat

Read release.rs + traceable diff


diff --git a/crates/spt-daemon/src/release.rs b/crates/spt-daemon/src/release.rs
index bc44ca7..c1f7d5a 100644
--- a/crates/spt-daemon/src/release.rs
+++ b/crates/spt-daemon/src/release.rs
@@ -180,6 +180,16 @@ pub struct SignedUpdateSet {
     pub signature_hex: String,
 }

+/// The Rust target triples spt-core recognizes as platform keys — the ADR-0016
+/// vocabulary, shared by platform-targeted update sets and the multi-platform
+/// `.spt` adapter archive (ADR-0024). Detection of a fat archive keys on THIS
+/// set, so a legacy flat archive (whose root entries are `manifest.toml` /
+/// `strings/` / role binaries — never a triple) can never misfire as
+/// multi-platform.
+// [impl->REQ-ADAPTER-MULTIPLATFORM-SPT]
+pub const KNOWN_TARGET_TRIPLES: &[&str] =
+    &["x86_64-pc-windows-msvc", "x86_64-unknown-linux-gnu"];
+
 /// Canonical platform identifier for this build, matching Rust target triples
 /// used by Cargo and ADR-0016.
 // [impl->REQ-UPD-6]
diff --git a/traceable-reqs.toml b/traceable-reqs.toml
index df9f8f7..88cdf3c 100644
--- a/traceable-reqs.toml
+++ b/traceable-reqs.toml
@@ -1337,7 +1337,7 @@ required_stages = []  # SEED ONLY (rule 5, activate-don't-pre-fail). The shipped
 [[requirements]]
 id = "REQ-ADAPTER-MULTIPLATFORM-SPT"
 title = "A `.spt` adapter archive may pack multiple platforms in one signed asset: shared `manifest.toml` + `strings/` at the root, role binaries under per-Rust-target-triple subdirectories (ADR-0016 triple vocabulary, e.g. `x86_64-pc-windows-msvc/`); install/update extracts the shared root plus ONLY `current_platform()`'s triple subdir, flattened into `install_dir` so flat `<install_dir>/<program>` resolution (REQ-INSTALL-11) is unchanged. Name stays `adapter.spt` (plain-tar or gzip, `--asset` optional default); one whole-archive Ed25519 signature over the fat archive (REQ-UPD-9 single-artifact verify). A legacy flat archive (no triple subdirs) extracts as today (free back-compat); a multi-platform archive sets `min_spt_core_version >= 0.13.2` (forward-compat gate, readable before extract); a multi-platform archive missing the recipient's triple -> typed `NoArtifactForPlatform`, never a silent no-op. Large adapters may still split per-platform (single-triple archives via `--asset`, or ADR-0016 update-set machinery). (ADR-0024, v0.13.2)"
-required_stages = ["doc"]  # doc = ADR-0024 + CONTEXT "adapter packaging & live update". impl/unit/int activate at the v0.13.2 build.
+required_stages = ["doc", "impl", "unit", "int"]  # doc = ADR-0024 + CONTEXT "adapter packaging & live update". impl/unit/int activated at the v0.13.2 W1 build.
 [[requirements]]
 id = "REQ-ADAPTER-LIVE-UPDATE"
 title = "An adapter update is live and daemon-coordinated (the adapter analog of brain self-update, ADR-0004): for an endpoint with a running RESIDENT adapter binary (today the `[message-idle-translation-binary]`), the CLI keeps fetch+verify and hands the APPLY to the daemon over IPC, which per affected endpoint (1) STOPS the resident binary -> releases the OS file lock (fixes the Windows 'Access denied (os error 5)' overwrite failure), (2) swaps on disk ONLY files whose CRC differs from the staged archive (unchanged files + their still-running binaries untouched), (3) RE-CLONES the new on-disk manifest into the running `BrainLifecycle` (the in-memory manifest is cached at bringup and otherwise goes stale -> binaries+manifest back on the same page), (4) RESTARTS the resident binary from the new files. An endpoint NOT running -> CLI swaps directly (no lock, no cache). Only the resident class is cycled; ephemeral adapter binaries (Psyche loop, `[digest]` extractor, `[session.*]` runners, hooks) self-heal on next spawn and are excluded. The daemon keeps a per-endpoint registry of resident adapter children. (ADR-0025, v0.13.2)"


release.rs clean — const tagged on real evidence, REQ activation correct. But `KNOWN_TARGET_TRIPLES` has only 2 triples; need to confirm `current_platform()` returns one of them and there's no broader existing platform list it should reuse. Read the cli.rs core logic + tests:


git diff 69c975f..281176b -- crates/spt/src/cli.rs | cat

Read full cli.rs W1 diff


diff --git a/crates/spt/src/cli.rs b/crates/spt/src/cli.rs
index 3561ea2..680d7ab 100644
--- a/crates/spt/src/cli.rs
+++ b/crates/spt/src/cli.rs
@@ -2552,12 +2552,70 @@ fn http_get_bytes(client: &reqwest::blocking::Client, url: &str) -> Result<Vec<u
     resp.bytes().map(|b| b.to_vec()).map_err(|e| e.to_string())
 }

-/// Extract a `.spt` adapter archive into `dest` via the system `tar`, bounded
+/// Why a `.spt` archive could not be extracted into the adapter install dir.
+/// `NoArtifactForPlatform` is the typed multi-platform rejection (ADR-0024,
+/// mirrors ADR-0016's `RejectReason`) — a fat archive that carries other
+/// platforms' triple subdirs but not this node's; never a silent no-op.
+#[derive(Debug)]
+enum ExtractError {
+    /// A multi-platform archive (one or more known triple subdirs) does not
+    /// carry the recipient's own triple.
+    NoArtifactForPlatform { platform: String },
+    /// The bounded `tar` extract itself failed (non-zero exit / spawn error).
+    Tar(String),
+    /// A filesystem step (staging, moving the selected tree) failed.
+    Io(String),
+}
+
+impl std::fmt::Display for ExtractError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            ExtractError::NoArtifactForPlatform { platform } => {
+                write!(f, "NoArtifactForPlatform({platform})")
+            }
+            ExtractError::Tar(e) => write!(f, "tar: {e}"),
+            ExtractError::Io(e) => write!(f, "io: {e}"),
+        }
+    }
+}
+
+/// How a `.spt` archive's root is laid out (ADR-0024).
+#[derive(Debug, PartialEq, Eq)]
+enum ArchiveLayout {
+    /// No known target-triple subdir at the root — a legacy single-platform
+    /// archive whose binaries sit at the root. Extract flat, exactly as today.
+    Flat,
+    /// One or more known target-triple subdirs at the root — a fat
+    /// multi-platform archive. `present` lists the triples found.
+    MultiPlatform { present: Vec<String> },
+}
+
+/// Classify a `.spt` by the names of its **top-level** entries. Keys strictly
+/// on [`KNOWN_TARGET_TRIPLES`] so a flat archive (root = `manifest.toml` /
+/// `strings/` / binaries) never misfires as multi-platform. Pure over the entry
+/// names — unit-testable without touching the filesystem or `tar`.
+// [impl->REQ-ADAPTER-MULTIPLATFORM-SPT]
+fn classify_archive_layout(top_level_names: &[String]) -> ArchiveLayout {
+    let mut present: Vec<String> = Vec::new();
+    for name in top_level_names {
+        if spt_daemon::release::KNOWN_TARGET_TRIPLES.contains(&name.as_str())
+            && !present.iter().any(|p| p == name)
+        {
+            present.push(name.clone());
+        }
+    }
+    if present.is_empty() {
+        ArchiveLayout::Flat
+    } else {
+        ArchiveLayout::MultiPlatform { present }
+    }
+}
+
+/// Extract the whole `.spt` into `dest` via the system `tar`, bounded
 /// (REQ-HAZARD-SUBPROCESS-TIMEOUT). `.spt` is a tar; `tar -xf` auto-detects an
-/// optional gzip, so a dev may ship either form. The archive's root holds the
-/// manifest + `strings/` + binaries, so they land directly under `dest`.
+/// optional gzip, so a dev may ship either form.
 // [impl->REQ-INSTALL-9]
-fn extract_release_archive(archive: &std::path::Path, dest: &std::path::Path) -> Result<(), String> {
+fn tar_extract_all(archive: &std::path::Path, dest: &std::path::Path) -> Result<(), ExtractError> {
     let keys = std::collections::BTreeMap::from([
         ("archive".to_string(), archive.to_string_lossy().into_owned()),
         ("dest".to_string(), dest.to_string_lossy().into_owned()),
@@ -2569,11 +2627,123 @@ fn extract_release_archive(archive: &std::path::Path, dest: &std::path::Path) ->
         None,
     ) {
         Ok(out) if out.success() => Ok(()),
-        Ok(out) => Err(format!("tar exit {:?}", out.status_code)),
-        Err(e) => Err(e.to_string()),
+        Ok(out) => Err(ExtractError::Tar(format!("exit {:?}", out.status_code))),
+        Err(e) => Err(ExtractError::Tar(e.to_string())),
     }
 }

+/// Top-level entry names of a directory (one path component each), or an IO
+/// error. Used to classify a freshly-staged archive's layout.
+fn top_level_names(dir: &std::path::Path) -> Result<Vec<String>, ExtractError> {
+    let mut names = Vec::new();
+    for entry in std::fs::read_dir(dir).map_err(|e| ExtractError::Io(e.to_string()))? {
+        let entry = entry.map_err(|e| ExtractError::Io(e.to_string()))?;
+        names.push(entry.file_name().to_string_lossy().into_owned());
+    }
+    Ok(names)
+}
+
+/// Move `src` to `dest`, falling back to copy+remove across filesystems / when a
+/// plain rename is refused (e.g. a pre-existing target on Windows).
+fn move_into(src: &std::path::Path, dest: &std::path::Path) -> Result<(), ExtractError> {
+    if std::fs::rename(src, dest).is_ok() {
+        return Ok(());
+    }
+    if src.is_dir() {
+        copy_dir_all(src, dest)?;
+        std::fs::remove_dir_all(src).map_err(|e| ExtractError::Io(e.to_string()))
+    } else {
+        std::fs::copy(src, dest).map_err(|e| ExtractError::Io(e.to_string()))?;
+        std::fs::remove_file(src).map_err(|e| ExtractError::Io(e.to_string()))
+    }
+}
+
+/// Recursively copy a directory tree (rename fallback).
+fn copy_dir_all(src: &std::path::Path, dest: &std::path::Path) -> Result<(), ExtractError> {
+    std::fs::create_dir_all(dest).map_err(|e| ExtractError::Io(e.to_string()))?;
+    for entry in std::fs::read_dir(src).map_err(|e| ExtractError::Io(e.to_string()))? {
+        let entry = entry.map_err(|e| ExtractError::Io(e.to_string()))?;
+        let from = entry.path();
+        let to = dest.join(entry.file_name());
+        if from.is_dir() {
+            copy_dir_all(&from, &to)?;
+        } else {
+            std::fs::copy(&from, &to).map_err(|e| ExtractError::Io(e.to_string()))?;
+        }
+    }
+    Ok(())
+}
+
+/// Extract a `.spt` adapter archive into `dest`, applying ADR-0024
+/// platform-selective placement. A **legacy flat** archive (no known triple
+/// subdir) extracts exactly as before — binaries land directly under `dest`. A
+/// **fat multi-platform** archive (shared `manifest.toml` + `strings/` at the
+/// root, role binaries under per-target-triple subdirs) places the shared root
+/// plus ONLY [`current_platform`]'s triple subdir, flattened into `dest`, so
+/// flat `<install_dir>/<program>` resolution (REQ-INSTALL-11) is unchanged. A
+/// fat archive lacking this node's triple is a typed
+/// [`ExtractError::NoArtifactForPlatform`], never a silent no-op.
+// [impl->REQ-INSTALL-9]
+// [impl->REQ-ADAPTER-MULTIPLATFORM-SPT]
+fn extract_release_archive(
+    archive: &std::path::Path,
+    dest: &std::path::Path,
+) -> Result<(), ExtractError> {
+    // Stage the whole archive to a sibling temp dir, then place only the
+    // shared root + this node's triple subdir into `dest`. Staging once keeps a
+    // single bounded `tar` call and lets the layout be read from real dirents
+    // (portable across GNU tar / Windows bsdtar) rather than parsed from a
+    // `tar -tf` listing.
+    let staging = match dest.parent() {
+        Some(parent) => parent.join(format!(
+            "{}.spt-stage",
+            dest.file_name().map(|n| n.to_string_lossy()).unwrap_or_default()
+        )),
+        None => dest.with_extension("spt-stage"),
+    };
+    let _ = std::fs::remove_dir_all(&staging);
+    std::fs::create_dir_all(&staging).map_err(|e| ExtractError::Io(e.to_string()))?;
+
+    let result = (|| {
+        tar_extract_all(archive, &staging)?;
+        let names = top_level_names(&staging)?;
+        match classify_archive_layout(&names) {
+            ArchiveLayout::Flat => {
+                // Place every staged root entry into `dest` (flat as today).
+                for name in &names {
+                    move_into(&staging.join(name), &dest.join(name))?;
+                }
+                Ok(())
+            }
+            ArchiveLayout::MultiPlatform { present } => {
+                let me = spt_daemon::current_platform();
+                if !present.iter().any(|t| t == me) {
+                    return Err(ExtractError::NoArtifactForPlatform {
+                        platform: me.to_string(),
+                    });
+                }
+                // Shared root: every top-level entry that is NOT a known triple
+                // subdir (other platforms' subdirs are simply left behind).
+                for name in &names {
+                    if spt_daemon::release::KNOWN_TARGET_TRIPLES.contains(&name.as_str()) {
+                        continue;
+                    }
+                    move_into(&staging.join(name), &dest.join(name))?;
+                }
+                // Flatten this node's triple subdir into `dest`.
+                let mine = staging.join(me);
+                for inner in top_level_names(&mine)? {
+                    move_into(&mine.join(&inner), &dest.join(&inner))?;
+                }
+                Ok(())
+            }
+        }
+    })();
+
+    let _ = std::fs::remove_dir_all(&staging);
+    result
+}
+
 /// Fetch a `.spt` adapter archive from a GitHub release and extract it into the
 /// durable `adapters/_github/<safe>` home, returning the extracted root for
 /// registration. First-acquisition trusts HTTPS + GitHub — the same trust the
@@ -11293,4 +11463,250 @@ mod tests {
         assert_eq!(r.code, EXIT_NOT_ELEVATED);
         assert!(r.out.is_none());
     }
+
+    // ---- v0.13.2 W1: multi-platform `.spt` adapter packaging (ADR-0024) ----
+
+    /// Build a real `.spt` fixture from a directory tree, by shelling the system
+    /// `tar` exactly as the prod extractor does (`tar -xf`). We build with
+    /// `tar -cf <archive> -C <srcdir> .` (relative `-C` member layout, portable
+    /// across GNU tar / Windows bsdtar). `archive` is an absolute path so the
+    /// `-C` into the source dir cannot confuse it.
+    fn build_spt_fixture(srcdir: &std::path::Path, archive: &std::path::Path) {
+        let out = std::process::Command::new("tar")
+            .arg("-cf")
+            .arg(archive)
+            .arg("-C")
+            .arg(srcdir)
+            .arg(".")
+            .output()
+            .expect("spawn tar to build .spt fixture");
+        assert!(
+            out.status.success(),
+            "tar -cf failed building fixture: status={:?} stderr={}",
+            out.status.code(),
+            String::from_utf8_lossy(&out.stderr)
+        );
+    }
+
+    /// Write a file (creating parent dirs) — fixture-staging helper.
+    fn stage_file(path: &std::path::Path, contents: &[u8]) {
+        if let Some(parent) = path.parent() {
+            std::fs::create_dir_all(parent).unwrap();
+        }
+        std::fs::write(path, contents).unwrap();
+    }
+
+    /// The known triple that is NOT this host's `current_platform()`. The CI
+    /// runners are always one of the two real triples, so on those there is a
+    /// well-defined "other". Returns None only on an `unknown` host.
+    fn other_known_triple() -> Option<&'static str> {
+        let me = spt_daemon::current_platform();
+        spt_daemon::release::KNOWN_TARGET_TRIPLES
+            .iter()
+            .copied()
+            .find(|t| *t != me)
+    }
+
+    // [unit->REQ-ADAPTER-MULTIPLATFORM-SPT] a flat archive (root = manifest.toml /
+    // strings/ / a role binary — never a target triple) classifies as Flat. The
+    // detector keys strictly on KNOWN_TARGET_TRIPLES, so legacy single-platform
+    // archives can NEVER misfire as multi-platform.
+    #[test]
+    fn classify_flat_archive_never_misfires() {
+        let names = vec![
+            "manifest.toml".to_string(),
+            "strings".to_string(),
+            "some-binary".to_string(),
+        ];
+        assert_eq!(classify_archive_layout(&names), ArchiveLayout::Flat);
+
+        // An empty root is also Flat (degenerate, but must not panic / misfire).
+        assert_eq!(classify_archive_layout(&[]), ArchiveLayout::Flat);
+    }
+
+    // [unit->REQ-ADAPTER-MULTIPLATFORM-SPT] an archive whose root carries one or
+    // both known triple subdirs (mixed with shared manifest.toml / strings/)
+    // classifies as MultiPlatform, capturing ONLY the known triples — a bogus dir
+    // like `darwin-foo` is NOT captured — deduped and in first-seen order.
+    #[test]
+    fn classify_multiplatform_captures_only_known_triples() {
+        let win = "x86_64-pc-windows-msvc".to_string();
+        let linux = "x86_64-unknown-linux-gnu".to_string();
+
+        // Both known triples + shared root + a bogus triple-shaped dir.
+        let names = vec![
+            "manifest.toml".to_string(),
+            "strings".to_string(),
+            win.clone(),
+            linux.clone(),
+            "darwin-foo".to_string(), // bogus: not a known triple → ignored
+        ];
+        match classify_archive_layout(&names) {
+            ArchiveLayout::MultiPlatform { present } => {
+                assert_eq!(
+                    present,
+                    vec![win.clone(), linux.clone()],
+                    "only the known triples are captured, in first-seen order; darwin-foo excluded"
+                );
+            }
+            other => panic!("expected MultiPlatform, got {other:?}"),
+        }
+
+        // Single known triple → MultiPlatform with just that triple.
+        let one = vec!["manifest.toml".to_string(), linux.clone()];
+        assert_eq!(
+            classify_archive_layout(&one),
+            ArchiveLayout::MultiPlatform {
+                present: vec![linux.clone()]
+            }
+        );
+
+        // Dedup: a triple listed twice (e.g. a dir + a stray entry of the same
+        // name) is captured exactly once.
+        let dup = vec![win.clone(), "manifest.toml".to_string(), win.clone()];
+        assert_eq!(
+            classify_archive_layout(&dup),
+            ArchiveLayout::MultiPlatform {
+                present: vec![win.clone()]
+            }
+        );
+    }
+
+    // [int->REQ-ADAPTER-MULTIPLATFORM-SPT] happy path: a REAL fat `.spt` carrying a
+    // shared root (manifest.toml + strings/<file>) plus BOTH known triple subdirs,
+    // each with a platform-distinct dummy binary, extracts so that: the shared root
+    // lands in dest, the CURRENT platform's binary is flattened to dest/rolebin with
+    // the right contents, neither triple subdir survives under dest, and the OTHER
+    // platform's binary contents are absent.
+    #[test]
+    fn extract_multiplatform_places_shared_root_and_flattens_my_triple() {
+        let me = spt_daemon::current_platform();
+        let Some(other) = other_known_triple() else {
+            // `unknown` host (not a CI runner) has no "mine" subdir; skip rather
+            // than assert a meaningless placement.
+            eprintln!("skip: current_platform()=unknown has no triple subdir");
+            return;
+        };
+
+        let tmp = tempfile::tempdir().unwrap();
+        let srcdir = tmp.path().join("src");
+        // Shared root.
+        stage_file(&srcdir.join("manifest.toml"), b"name = \"demo\"\n");
+        stage_file(&srcdir.join("strings").join("en.toml"), b"hello = \"hi\"\n");
+        // Per-triple binaries with platform-distinct contents.
+        let my_bytes = format!("BINARY-FOR-{me}");
+        let other_bytes = format!("BINARY-FOR-{other}");
+        stage_file(&srcdir.join(me).join("rolebin"), my_bytes.as_bytes());
+        stage_file(&srcdir.join(other).join("rolebin"), other_bytes.as_bytes());
+
+        let archive = tmp.path().join("adapter.spt");
+        build_spt_fixture(&srcdir, &archive);
+
+        let dest = tmp.path().join("install");
+        std::fs::create_dir_all(&dest).unwrap();
+        extract_release_archive(&archive, &dest).expect("fat archive extracts for this platform");
+
+        // Shared root placed.
+        assert!(dest.join("manifest.toml").is_file(), "shared manifest.toml placed");
+        assert!(
+            dest.join("strings").join("en.toml").is_file(),
+            "shared strings/<file> placed"
+        );
+        // My triple's binary flattened to dest/rolebin with MY contents.
+        let placed = std::fs::read(dest.join("rolebin")).expect("rolebin flattened to dest root");
+        assert_eq!(
+            placed,
+            my_bytes.as_bytes(),
+            "the flattened binary is THIS platform's, not the other's"
+        );
+        // Neither triple subdir survives under dest.
+        assert!(
+            !dest.join(me).exists(),
+            "my triple subdir was flattened away, not left as a subdir"
+        );
+        assert!(
+            !dest.join(other).exists(),
+            "the other platform's triple subdir is not installed"
+        );
+        // The other platform's distinct contents must be absent anywhere obvious.
+        assert_ne!(
+            placed,
+            other_bytes.as_bytes(),
+            "the other platform's binary contents must not be what landed"
+        );
+    }
+
+    // [int->REQ-ADAPTER-MULTIPLATFORM-SPT] a fat `.spt` that carries ONLY the
+    // non-current triple subdir is a loud typed rejection
+    // (NoArtifactForPlatform { platform == current_platform() }), NOT a silent
+    // no-op — and dest receives no binary.
+    #[test]
+    fn extract_multiplatform_missing_my_triple_is_typed_rejection() {
+        let me = spt_daemon::current_platform();
+        let Some(other) = other_known_triple() else {
+            eprintln!("skip: current_platform()=unknown");
+            return;
+        };
+
+        let tmp = tempfile::tempdir().unwrap();
+        let srcdir = tmp.path().join("src");
+        stage_file(&srcdir.join("manifest.toml"), b"name = \"demo\"\n");
+        stage_file(&srcdir.join("strings").join("en.toml"), b"x = 1\n");
+        // Only the OTHER platform's triple subdir — this node's is absent.
+        stage_file(&srcdir.join(other).join("rolebin"), b"NOT-FOR-ME");
+
+        let archive = tmp.path().join("adapter.spt");
+        build_spt_fixture(&srcdir, &archive);
+
+        let dest = tmp.path().join("install");
+        std::fs::create_dir_all(&dest).unwrap();
+        let err = extract_release_archive(&archive, &dest)
+            .expect_err("a fat archive without my triple must be rejected");
+        match err {
+            ExtractError::NoArtifactForPlatform { platform } => {
+                assert_eq!(platform, me, "the rejection names THIS node's platform");
+            }
+            other => panic!("expected NoArtifactForPlatform, got {other:?}"),
+        }
+        // No silent no-op: dest got no binary (the other platform's was left behind).
+        assert!(
+            !dest.join("rolebin").exists(),
+            "a rejected fat archive installs no binary"
+        );
+        assert!(
+            !dest.join(other).exists(),
+            "the foreign triple subdir is not installed either"
+        );
+    }
+
+    // [int->REQ-ADAPTER-MULTIPLATFORM-SPT] a legacy flat `.spt` (manifest.toml +
+    // strings/<file> + a rolebin at the ROOT, no triple subdir) extracts exactly as
+    // today — every root entry lands flat under dest, REQ-INSTALL-11 resolution
+    // unchanged.
+    #[test]
+    fn extract_legacy_flat_stays_flat() {
+        let tmp = tempfile::tempdir().unwrap();
+        let srcdir = tmp.path().join("src");
+        stage_file(&srcdir.join("manifest.toml"), b"name = \"legacy\"\n");
+        stage_file(&srcdir.join("strings").join("en.toml"), b"a = \"b\"\n");
+        stage_file(&srcdir.join("rolebin"), b"FLAT-ROOT-BINARY");
+
+        let archive = tmp.path().join("adapter.spt");
+        build_spt_fixture(&srcdir, &archive);
+
+        let dest = tmp.path().join("install");
+        std::fs::create_dir_all(&dest).unwrap();
+        extract_release_archive(&archive, &dest).expect("legacy flat archive extracts");
+
+        assert!(dest.join("manifest.toml").is_file(), "flat manifest.toml placed");
+        assert!(
+            dest.join("strings").join("en.toml").is_file(),
+            "flat strings/<file> placed"
+        );
+        assert_eq!(
+            std::fs::read(dest.join("rolebin")).expect("flat rolebin placed"),
+            b"FLAT-ROOT-BINARY",
+            "the root binary extracts to dest/rolebin exactly as today"
+        );
+    }
 }


Diff is high quality — staging-then-place, pure classifier, typed rejection, non-vacuous int tests with platform-distinct binary contents. Two things to verify by reading (not trusting the report): (1) the caller handles the new `ExtractError` return type, (2) `current_platform()` values vs. the 2-triple `KNOWN_TARGET_TRIPLES`, and whether an existing platform list should be the single source.


echo "===current_platform def==="; grep -n "fn current_platform" crates/spt-daemon/src/release.rs; sed -n '195,230p' crates/spt-daemon/src/release.rs; echo "===existing triple/platform lists in release.rs==="; grep -n "x86_64-\|aarch64\|apple-darwin\|windows-msvc\|unknown-linux\|PLATFORMS\|target triple\|\"unknown\"" crates/spt-daemon/src/release.rs | head -30

current_platform + existing platform lists


===current_platform def===
196:pub fn current_platform() -> &'static str {
// [impl->REQ-UPD-6]
pub fn current_platform() -> &'static str {
    #[cfg(all(target_arch = "x86_64", target_os = "windows", target_env = "msvc"))]
    {
        "x86_64-pc-windows-msvc"
    }
    #[cfg(all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"))]
    {
        "x86_64-unknown-linux-gnu"
    }
    #[cfg(not(any(
        all(target_arch = "x86_64", target_os = "windows", target_env = "msvc"),
        all(target_arch = "x86_64", target_os = "linux", target_env = "gnu")
    )))]
    {
        "unknown"
    }
}

/// The trust policy a node verifies a release against. Pure data — no clock, no
/// I/O — so verification is deterministic and testable.
#[derive(Debug, Clone)]
pub struct VerifyPolicy {
    /// Trusted release keys by `key_id`. Rotation = add a new id; old ids may
    /// linger until retired.
    pub trusted_keys: BTreeMap<String, VerifyingKey>,
    /// Explicitly revoked `key_id`s — rejected even with a valid signature.
    pub revoked_keys: BTreeSet<String>,
    /// The channel this node is pinned to.
    pub pinned_channel: String,
    /// The running build's version — an update must exceed it (monotonic).
    pub current_version: u64,
    /// Wall-clock now (epoch ms) for the expiry check (caller-supplied).
    pub now_ms: u64,
}

===existing triple/platform lists in release.rs===
92:/// the Rust target triple (e.g. `x86_64-pc-windows-msvc`); this entry carries
134:/// per-platform artifacts keyed by Rust target triple.
146:    /// Platform artifacts keyed by Rust target triple.
183:/// The Rust target triples spt-core recognizes as platform keys — the ADR-0016
191:    &["x86_64-pc-windows-msvc", "x86_64-unknown-linux-gnu"];
193:/// Canonical platform identifier for this build, matching Rust target triples
199:        "x86_64-pc-windows-msvc"
203:        "x86_64-unknown-linux-gnu"
210:        "unknown"
732:                    "x86_64-pc-windows-msvc".to_string(),
737:                        asset_name: Some("spt-x86_64-windows.exe".to_string()),
741:                    "x86_64-unknown-linux-gnu".to_string(),
746:                        asset_name: Some("spt-x86_64-linux".to_string()),
967:        let linux = meta.artifact_for("x86_64-unknown-linux-gnu").unwrap();
968:        assert_eq!(linux.asset_name.as_deref(), Some("spt-x86_64-linux"));
969:        verify_update_set_artifact(&meta, "x86_64-unknown-linux-gnu", b"linux bytes")
971:        verify_update_set_artifact(&meta, "x86_64-pc-windows-msvc", b"win bytes")
985:        meta.artifacts.remove("x86_64-pc-windows-msvc");
989:            verify_update_set_artifact(&meta, "x86_64-pc-windows-msvc", b"win bytes"),
991:                platform: "x86_64-pc-windows-msvc".to_string()
1045:            verify_update_set_artifact(&meta, "x86_64-unknown-linux-gnu", b"wrong"),


`current_platform()` returns exactly the two known triples (or `unknown`→correctly rejected); the const matches the update-set platform keys — consistent single-source, no drift. Now verify the caller handles the new `ExtractError` type:


echo "===extract_release_archive call sites==="; grep -n "extract_release_archive" crates/spt/src/cli.rs; echo "===context around each non-def call==="; grep -n "extract_release_archive(" crates/spt/src/cli.rs | grep -v "fn extract_release_archive"

Find extract_release_archive callers


===extract_release_archive call sites===
2688:fn extract_release_archive(
2794:    let extracted = extract_release_archive(&archive, &dest);
6033:        let extracted = extract_release_archive(&staged, &dest);
7992:        extract_release_archive(&archive, &dest).expect("extract the .spt");
11607:        extract_release_archive(&archive, &dest).expect("fat archive extracts for this platform");
11663:        let err = extract_release_archive(&archive, &dest)
11699:        extract_release_archive(&archive, &dest).expect("legacy flat archive extracts");
===context around each non-def call===
2794:    let extracted = extract_release_archive(&archive, &dest);
6033:        let extracted = extract_release_archive(&staged, &dest);
7992:        extract_release_archive(&archive, &dest).expect("extract the .spt");
11607:        extract_release_archive(&archive, &dest).expect("fat archive extracts for this platform");
11663:        let err = extract_release_archive(&archive, &dest)
11699:        extract_release_archive(&archive, &dest).expect("legacy flat archive extracts");


W1 — Multi-platform .spt packaging (ADR-0024)

REQ-ADAPTER-MULTIPLATFORM-SPT. Fat archive: shared manifest.toml+strings/ at root, binaries under per-target-triple subdirs. Extract shared root + only current_platform() triple, flatten into install_dir. Mode detection on known-triple subdir; legacy flat = back-compat. min_spt_core_version>=0.13.2 forward gate. --asset optional default adapter.spt. Gate: unit+int(real fixture)+doc. Executor builds, doyle gates.

W2 — gh transport for private repos

REQ-ADAPTER-GH-TRANSPORT. transport = https|gh|auto (default auto) on gh_release avenue + add --release. gh shells gh release download/gh api (private path; honors OAuth+GH_TOKEN, no token custody). --gh/--https force. auto = prefer gh if installed+authed else https. Files: fetch path (http_get_bytes, gh_latest_release_version, stage_gh_release_archive), Update struct transport field. Gate: unit (decision+command shape); int HITL/skip. doc=manifest.md+CONTEXT.

W3 — Live daemon-coordinated update + CRC swap (ADR-0025) KEYSTONE

REQ-ADAPTER-LIVE-UPDATE. W3a resident-children registry; W3b CRC-gated swap (std hash, no new dep); W3c manifest refresh into running BrainLifecycle; W3d daemon-apply IPC (stop resident binary→swap→refresh→restart; not-running→CLI direct); W3e int keystone NO mocks (real spt-hosted endpoint+running translation binary; Windows locked-binary regression test). Bounded stop/restart (join_bounded, never 240s backstop). Brain never restarts. Partial-failure safe. doyle gates vs REAL daemon+dummy-harness fixture.

W4 — update-message

REQ-ADAPTER-UPDATE-MESSAGE. [update].message plain multi-line, markdown-rendered (helpfmt prose path), ONLY when update actually applied (version changed), not no-op. Read from newly-installed manifest, avenue-agnostic. Files: Update struct message:Option<String>; cmd_adapter_update after successful apply→helpfmt::render+print. Gate: unit (rendered on applied, NOT no-op). doc=manifest.md+CONTEXT. No int.

W5 — *-proof --dir/--manifest override (F-011)

REQ-ADAPTER-PROOF-DIR-OVERRIDE. --dir <path>/--manifest <file> on digest-proof+translate-proof → proof DEV binary vs on-disk manifest+dir without full extracted install. Files: cmd_adapter_digest_proof + cmd_adapter_translate_proof accept --dir/--manifest, resolve from there not registry::resolve_option. Gate: unit (override resolves dev manifest+dir); int (mirror proof ints with --dir). doc=proof --help+patterns. May ship v0.13.2 or separate batch.

Release — cut v0.13.2 (deployah role)

After all waves gate-pass + CI green BOTH runners: verify release counter against spt-releases metadata at cut (v0.13.1 lesson, expect 29). Author version bump (workspace.package.version 0.13.1→0.13.2 + CHANGELOG section=release body + Cargo.lock first-party lines). Push bump→CI green both→tag→sign+publish. Coordinate publish ping to perri (drops claude-spt F-014 per-OS stopgap; F-011 closed). W3 int + Windows-lock regression = ship-blockers.

W3 design input — adapter psyche binary locks shared install-dir (perri F-010×F-015)

perri live repro 2026-06-22 (spt 0.13.1, claude-spt 0.6.0, Win): claude-spt-psyche.exe (pid 181272, --id wall-a-psyche) ran DETACHED from shared install-dir, parent (152200) DEAD = orphan (~4h), held binary lock → adapter update/add --release failed (tar exit Some(1)). endpoint stop wall-a set alive=false but did NOT reap the detached psyche.

ROOT MECHANICS: psyche is detached per manifest [session.psyche_init] detach=true (Stdio::null, unsupervised), hosted by daemon livehost off the live_agent online status. It runs from the SHARED install dir (lock surface = manifest.toml + claude-spt-digest.exe + claude-spt-psyche.exe + cc-spt-idle-translate.exe + strings/).

LOAD-BEARING ON W3: ADR-0025 resident/ephemeral split assumed psyche=daemon-hosted in-process (ADR-0004), EXCLUDED from cycling. But it's a SEPARATE install-dir binary → unsupervised locker W3's resident-only stop/restart won't release. Fixes compose: (c) psyche-from-own-copy not shared dir = cleanest structural kill, removes vector independent of reap timing; (a) orphan reap on stop/agent-death = F-010 root close; (b) extract-to-temp+atomic-swap = F-015 backstop (one locked file ≠ whole-update fail).

W3 INT FIXTURE (perri spec, deterministic, NO mocks): 1. bring up claude-spt live agent wall-X → daemon spawns detached psyche from install dir; 2. kill psyche PARENT (livehost/agent) → parent-dead orphan; 3. spt endpoint stop wall-X → alive=false BUT psyche survives (reap gap); 4. spt adapter update claude-spt → tar overwrites locked psyche.exe → exit 1 → whole update fails. PASS post-W3: step 3 reaps orphan AND/OR psyche never ran from shared target AND/OR locked-file extract no longer fails whole update. perri re-runs this exact repro as the ship acceptance test on doyle ping.


Two prod callers (2794, 6033). Read how each consumes the `Result<(), ExtractError>`:


echo "=== call site 2794 ==="; sed -n '2790,2810p' crates/spt/src/cli.rs; echo "=== call site 6033 ==="; sed -n '6028,6050p' crates/spt/src/cli.rs

Read both prod caller contexts


=== call site 2794 ===
    if let Err(e) = std::fs::write(&archive, &bytes) {
        eprintln!("ADAPTER_RELEASE_FAIL: write archive: {e}");
        return Err(1);
    }
    let extracted = extract_release_archive(&archive, &dest);
    let _ = std::fs::remove_file(&archive);
    match extracted {
        Ok(()) => Ok(dest),
        Err(e) => {
            eprintln!("ADAPTER_RELEASE_FAIL: extract: {e}");
            Err(1)
        }
    }
}

/// `spt update fetch` (REQ-UPD-7, platform-safed by REQ-UPD-8) — pull the
/// signed multi-platform **update set** (`update-set.json` + every platform
/// artifact it names) from the GitHub origin and stage it through the SAME
/// verify gate the P2P pull uses (signature + channel + monotonic rollback
/// floor + per-platform SHA-256), after which the existing `spt update apply`
/// flow takes over.
=== call site 6033 ===
            let _ = std::fs::remove_file(&staged);
            eprintln!("ADAPTER_UPDATE_FAIL:{}: create dest: {e}", record.name);
            failed = true;
            continue;
        }
        let extracted = extract_release_archive(&staged, &dest);
        let _ = std::fs::remove_file(&staged);
        if let Err(e) = extracted {
            eprintln!("ADAPTER_UPDATE_FAIL:{}: extract: {e}", record.name);
            failed = true;
            continue;
        }
        match registry::register(adapters, &dest, now_ms()) {
            Ok((r, _)) => eprintln!("ADAPTER_UPDATE_DONE:{} (re-registered)", r.name),
            Err(e) => {
                eprintln!("ADAPTER_UPDATE_FAIL:{}: re-register: {e}", record.name);
                failed = true;
            }
        }
    }

    i32::from(failed)
}


Both prod callers consume `ExtractError` correctly via `{e}` Display — `NoArtifactForPlatform` now surfaces as a clear typed diagnostic, no silent no-op. Caller handling sound.

Now run the gates myself (not trusting the report). clippy-workspace + traceable in parallel:


cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -6; echo "===CLIPPY EXIT=${PIPESTATUS[0]}==="

Clippy workspace all-targets deny warnings


"C:/Users/decid/AppData/Local/Programs/traceable-reqs/traceable-reqs" check >/tmp/trace.txt 2>&1; echo "TRACEABLE EXIT=$?"; tail -3 /tmp/trace.txt; grep -c "\[OK\]" /tmp/trace.txt | xargs echo "OK count:"; grep -iE "FAIL|MISSING" /tmp/trace.txt | head

Standalone traceable check


    Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.60s
===CLIPPY EXIT=0===


TRACEABLE EXIT=0
       A `rc --view` VIEWER that overflows its broker subscription queue and is EVICTED (OutputLog::append try_send Full → viewers.remove, REQ-HAZARD-VIEWER-ISOLATION session-protection) must SKIP TO LIVE, not die silently. ROOT (v0.13.0, b4 JIT item 2 = p0_paste + post-b4 a_journaled-Linux, ONE root): serve_attach forwards each frame (read_event→b64decode→re-encode AttachRecord→net_stream_send) SLOWER than the drain fans out under flood → its VIEWER_CHANNEL_DEPTH(256) channel overflows → the drain evicts (viewers.remove drops the ViewerSink → drops tx → viewer_writer's rx.recv() Err → the writer returns WRITING NOTHING) → serve_attach's brain.read_event() just STOPS getting Output (no EOF, no error) → serve_attach blocks forever → the operator receives nothing (attach_received_output=FALSE). Eviction-of-a-hopelessly-behind-viewer is CORRECT session-protection (keep it); SILENT+PERMANENT eviction is the bug. VIEWER-only → B2-SAFE (a viewer never advances delivered_through / is not authoritative / exposes no resume cursor). FIX (doyle-gated, skip-to-live = tail -f reconnect): (1) explicit broker→viewer EVICTION SIGNAL (KIND_VIEWER_EVICTED, written in the viewer_writer thread OFF the log lock, DISTINCT from session-exit EOF so serve must NOT tear down on it); (2) serve_attach re-subscribes from the CURRENT ring floor (skip-to-live, replays nothing, sees the next live burst) — resetting the cold serve-brain's next_seq so the post-eviction forward-jump replay is accepted (the legacy reject-gap path, brain.rs:618-626, would otherwise FATAL the forward jump); (3) HARD constraint NO evict→resubscribe busy-loop: serve_attach rate-limits re-subscribes (RESUBSCRIBE_INTERVAL) so under max-flood the operator sees intermittent LIVE bursts, never a CPU spin. (v0.13.0)
  [OK] REQ-WHOAMI-1  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       `spt whoami` is a thin ALIAS for `spt endpoint list` (full output: the SELF pin + the subnet roster) — the standalone bare-id command is dropped (the `id=$(spt whoami)` capture was never a real pattern: env vars don't persist between agent tool calls). The one new render: the `endpoint list` SELF pin carries the Self endpoint's authored `endpoint description` (info::read_info(...).resources) when present, inline after the liveness state. whoami stays a top-level hot-path verb (parse unchanged, REQ-MSG-9).
OK count: 288
       An adapter update is live and daemon-coordinated (the adapter analog of brain self-update, ADR-0004): for an endpoint with a running RESIDENT adapter binary (today the `[message-idle-translation-binary]`), the CLI keeps fetch+verify and hands the APPLY to the daemon over IPC, which per affected endpoint (1) STOPS the resident binary -> releases the OS file lock (fixes the Windows 'Access denied (os error 5)' overwrite failure), (2) swaps on disk ONLY files whose CRC differs from the staged archive (unchanged files + their still-running binaries untouched), (3) RE-CLONES the new on-disk manifest into the running `BrainLifecycle` (the in-memory manifest is cached at bringup and otherwise goes stale -> binaries+manifest back on the same page), (4) RESTARTS the resident binary from the new files. An endpoint NOT running -> CLI swaps directly (no lock, no cache). Only the resident class is cycled; ephemeral adapter binaries (Psyche loop, `[digest]` extractor, `[session.*]` runners, hooks) self-heal on next spawn and are excluded. The daemon keeps a per-endpoint registry of resident adapter children. (ADR-0025, v0.13.2)
       A `.spt` adapter archive may pack multiple platforms in one signed asset: shared `manifest.toml` + `strings/` at the root, role binaries under per-Rust-target-triple subdirectories (ADR-0016 triple vocabulary, e.g. `x86_64-pc-windows-msvc/`); install/update extracts the shared root plus ONLY `current_platform()`'s triple subdir, flattened into `install_dir` so flat `<install_dir>/<program>` resolution (REQ-INSTALL-11) is unchanged. Name stays `adapter.spt` (plain-tar or gzip, `--asset` optional default); one whole-archive Ed25519 signature over the fat archive (REQ-UPD-9 single-artifact verify). A legacy flat archive (no triple subdirs) extracts as today (free back-compat); a multi-platform archive sets `min_spt_core_version >= 0.13.2` (forward-compat gate, readable before extract); a multi-platform archive missing the recipient's triple -> typed `NoArtifactForPlatform`, never a silent no-op. Large adapters may still split per-platform (single-triple archives via `--asset`, or ADR-0016 update-set machinery). (ADR-0024, v0.13.2)
       `spt adapter translate-proof <adapter> --event <envelope> [--session <id>]` — the author-time EMIT-half proof tool for `[message-idle-translation-binary]` (ADR-0022), symmetric to `spt adapter digest-proof` (REQ-TERM-5). It spawns and feeds the adapter's declared translation binary EXACTLY as the daemon does at idle-delivery — running the REAL `spt_daemon::translation` driver VERBATIM (no protocol reimplementation): `TranslationChild::spawn` the binary, send the `{type:"init",endpoint_id,node}` line then the `{type:"event",envelope}` line, and read back the emitted `{key}`/`{text}`/`{delay_ms}`/`{commit}` keystroke-command stream — then prints it author-readable (each Key with its `key_to_bytes` rendering, Text quoted, Delay in ms, Commit marker) with counts. It fills the SAME `{id}`→option and `{session_id}`→(--session, else a placeholder) keys into the `--event` envelope the daemon fills at runtime, so an envelope that proofs here feeds faithfully live. EMIT-half ONLY: it proves the binary's spawn+feed+emit contract; it does NOT exercise the daemon's atomic PTY apply / controller-buffering (that stays covered by the W2 inject_control_wedge int gate) — `--help` says so. Exit codes mirror digest-proof: 0 ok, 1 on spawn-fail / zero commands / no-commit-or-output / unparseable, 2 when the adapter declares no `[message-idle-translation-binary]` section. The `TranslationChild` Drop does the bounded no-zombie reap. (v0.13.x)
       Peer address seeding, both cold starts: durable peer-addrs.json (identity dir) maps peer pubkey → last-known dialable address; the pump's resolver consults it FIRST with id-only discovery fallback on miss or dial failure (a stale addr never strands a peer); written by the pairing ceremony (both sides, from the live connection) and by the pump on successful connect; post-join first sync and post-restart resync converge in seconds, not ~1 min (M8 decisions 14, 20)
       Net-bind boot-race resilience: a daemon that comes up net-less (NetHost::start failed — e.g. the systemd unit autostarted before the network/DNS stack was ready, `Failed to create an address lookup service`) must SELF-HEAL — retry the net bring-up in the background with capped backoff and, on success, attach net to the broker + spawn the dispatcher/peer-pump (which today are gated on `net_up` at boot and so never start, leaving the node silently unreachable until a manual restart — kitsubito 2026-06-08). Status surfaces the net-less state honestly (a net-less broker renders as 'no connection', not only a pump-STALLED line with a bogus pre-boot heartbeat age). The installer's autostart unit waits for the network (`Wants=/After=network-online.target`) as belt-and-suspenders.
       A legitimately dead PTY child (real crash/kill) + an undrained operator pump must NOT wedge the broker for all other clients. ROOT (v0.12.0 real-harness defect): loopback attach output is a blocking write_all into a bounded 64KB tokio duplex (nethost.rs:1040,1090); when the operator's rc pump stops draining (tab closed) the buffer fills and write_all blocks forever (the 'loopback never hangs' assumption at nethost.rs:1103 is false), parking a worker in the 2-worker net runtime (nethost.rs:640); a couple of these saturate BOTH workers → every new attach / `endpoint run` stalls right after 'PUMP_IPC_READER: spawned' → 30s FIRST_EVENT_GRACE → 'no output / dead or wedged'; `daemon stop` cannot join the stuck workers. DISTINCT from the removed B1 path-(c) mutex deadlock. DISPOSITION = PROVE-DON'T-CHANGE (doyle GATE-PASS @e883f45, 2026-06-18): this ROOT is the SUPERSEDED v0.12.0 hypothesis — the post-L0 code ALREADY prevents the wedge, so NO fail-fast / worker-count code was added. serve_attach forwards fire-and-forget (net_stream_send op_id=None) and the broker-side send_stream is already BROKER-QUIC-DEADLINE-bounded (bounded_block_on, 10s); the loopback duplex is drained broker-INTERNALLY by the operator row's own read pump (RecvHalf::Loopback, retentive_cap==0 → evict-not-park) so a dead rc (a dropped IPC subscriber) never backs peer_w up; bounded_block_on parks the BROKER DISPATCH thread, not a net worker → no worker-pool exhaustion (full mechanism in the required_stages comment). Folds the status=online sub-check: a dead spt-hosted endpoint is marked OFFLINE within one reconcile tick on abrupt child death (broker exit-waiter reaps the session → B2 sees it absent) — PROVEN, no change. (v0.12.1)
       A bare brain restart leaves EXACTLY ONE `{id}-psyche` process per endpoint — no duplicate. On an abrupt brain death stop_host never runs (the LiveSet + owned child handles die with the brain) and Breap's job/group only reaps at DAEMON stop, so the PRIOR brain's Psyche stays ALIVE; the respawned brain's reconcile re-hosts a SECOND Psyche and overwrites the `{id}-psyche` perch pid, leaving the old one untracked + alive = a duplicate that lingers until daemon-stop (the operator's 'brain kill+restart wedged everything'). FIX: at brain start, BEFORE the first reconcile re-hosts, reap any pre-existing `{id}-psyche` orphan — ID-SPECIFICALLY (recycle-safe on the shared box, where sibling agents share the `claude` basename): scoped-kill the recorded pid ONLY IF it is alive AND its exe basename == the adapter's psyche program (normalize_basename) AND its COMMAND LINE contains the full psyche id `<id>-psyche` (baked via {id}); a sibling never carries THIS id, and any unreadable signal FAILS SAFE (decline to reap — a missed dup is bounded by Breap, a wrong-kill is catastrophic). CAVEAT: the cmdline carries `<id>-psyche` only when the adapter's psyche_init.command uses {id} (the norm); a non-{id} adapter safely MISSES the reap (today's behavior, Breap bounds it) — never a wrong-kill. (v0.12.0)
       The broker bounds every brain-waiting QUIC op (dial / open_stream / send_stream) so a black-holed or dead peer fails PROMPTLY with an ORDINARY error the broker REPLIES, never an unbounded await. The bound (< the brain's 30s PUMP_PEER_IO_TIMEOUT so the BROKER fires first) surfaces to the pump as a normal broker error reply → peer_outcome's non-TimedOut arm → drop conn + redial next tick, the round CONTINUES and the heartbeat keeps advancing — it must NEVER manifest as the brain's own read-deadline (the A-half poison → supervised-restart path REQ-HAZARD-PUMP-IPC-DEADLINE guards). Exactly-once is preserved: a timed-out journaled op fails INSIDE its apply_once closure so no phantom conn_id/stream_id is recorded and a fresh tick re-dials cleanly. The happy path is unchanged (a live peer completes with zero added latency; the bound only bites a non-responsive peer). This is the ROOT-cause cure for the 2.2h hfenduleam pump wedge — a dead roster peer whose QUIC path the broker awaited unbounded — recurring on hfenduleam 2026-06-16.
       A daemon-state wire-format change (e.g. the v0.9.0 adapter-agnostic Seed) does NOT take effect until a DELIBERATE full broker restart: the broker serves the seed-control channel and is RESIDENT across a brain-only self-update (ADR-0004 no-terminate-during-update forbids auto-killing it), so a NEW-version CLI talking to a still-resident OLD broker fails the seed handshake — the old broker cannot deserialize the new Seed (its formerly-required `adapter` field is gone) and drops the conn without an ack, which surfaces to the CLI as a raw UnexpectedEof 'failed to fill whole buffer'. spt-core must (a) surface an ACTIONABLE diagnostic on that seed-ack EOF (name the stale-broker cause + the `spt daemon stop` fix — the broker restarts on the next api call), never the cryptic io error; and (b) document the operational rule (a deliberate broker restart is required on any daemon-state wire change — NOT automatic) + the FORWARD discipline (daemon-state/Seed schema changes stay additive + serde-default so a resident OLD broker tolerates a NEW CLI across a brain-only update; note this would NOT have rescued 0.9.0 itself, since the old broker's `adapter` was a required field). perri PREP-4 FINDING 1 (v0.9.0 CLI vs stale 0.8.x broker).
       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)


Clippy clean (exit 0), traceable exit 0 (288/288, no real FAIL — grep hits were description prose). Now run the 5 W1 tests + release suite myself:


cargo test -p spt --lib classify_flat_archive_never_misfires classify_multiplatform_captures_only_known_triples extract_multiplatform_places_shared_root_and_flattens_my_triple extract_multiplatform_missing_my_triple_is_typed_rejection extract_legacy_flat_stays_flat 2>&1 | tail -20

Run the 5 W1 tests


error: unexpected argument 'classify_multiplatform_captures_only_known_triples' found

Usage: cargo.exe test [OPTIONS] [TESTNAME] [-- [ARGS]...]

For more information, try '--help'.


cargo test takes one filter substring. Run in two passes:


cargo test -p spt --lib classify_ 2>&1 | tail -8; echo "=====EXTRACT====="; cargo test -p spt --lib extract_ 2>&1 | tail -12

Run classify_ and extract_ W1 tests


error: no library targets found in package `spt`
=====EXTRACT=====
error: no library targets found in package `spt`


`spt` is a binary crate (no lib). Filter on the bin tests:


cargo test -p spt --bins classify_ 2>&1 | tail -7; echo "=====EXTRACT====="; cargo test -p spt --bins extract_ 2>&1 | grep -E "test result|extract_|running" | tail -12

Run W1 unit + int tests on spt bin


     Running unittests tests\fixtures\translate_proof_fixture.rs (target\debug\deps\translate_proof_fixture-ef0c6784663b432f.exe)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

=====EXTRACT=====
running 3 tests
test cli::tests::extract_legacy_flat_stays_flat ... ok
test cli::tests::extract_multiplatform_missing_my_triple_is_typed_rejection ... ok
test cli::tests::extract_multiplatform_places_shared_root_and_flattens_my_triple ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 212 filtered out; finished in 0.07s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


W2 — gh transport for private repos

W4 — update-message

Release — cut v0.13.2 (deployah role)


3 int tests pass. The `classify_` output got truncated (only showed the fixture binary). Confirm the 2 classifier unit tests:


cargo test -p spt --bins classify_ 2>&1 | grep -E "cli::tests::classify|test result.*passed"

Confirm classifier unit tests pass


test cli::tests::classify_flat_archive_never_misfires ... ok
test cli::tests::classify_multiplatform_captures_only_known_triples ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 212 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


All gates green by my own hand:
- **Diff**: clean staging→place, typed `ExtractError`, both prod callers handle it, `KNOWN_TARGET_TRIPLES` consistent with `current_platform()` + update-set keys (single source).
- **Evidence tags**: on real evidence (const/classifier/extract fn/each test), not file-top. REQ activated `[doc,impl,unit,int]`.
- **My re-runs**: clippy `--workspace --all-targets -D warnings` EXIT 0; traceable EXIT 0 (288/288); 2 unit + 3 int PASS.
- **Non-vacuity**: int test 3 asserts binary flattened to `dest/rolebin` AND `dest/<triple>` absent — removing flatten breaks it by construction. Genuine real-tar fixtures.
- **Daemon-lib deadlock caveat**: confirmed env-not-diff (release.rs = pure const, zero daemon runtime path). CI both-runners is authority.

**W1 = GATE-PASS.** Now open a draft PR (triggers CI on both runners for the W1 state) + dispatch W2 (sequential, builds on the W1 commit). First check mergeability per the unmergeable-PR lesson:


git rev-list --count v0.13.2-adapter-packaging ^origin/main | xargs echo "ahead of main:"; gh pr list --head v0.13.2-adapter-packaging --json number,state 2>&1 | head; echo "===open draft PR==="; gh pr create --draft --base main --head v0.13.2-adapter-packaging --title "v0.13.2 — adapter packaging + live update (W1–W5)" --body "$(cat <<'EOF'
v0.13.2 adapter-packaging + live-update milestone. Waves land incrementally on this branch; PR stays **draft** until all gate-pass + CI green both runners.

## Waves
- [x] **W1** — multi-platform `.spt` packaging (ADR-0024, REQ-ADAPTER-MULTIPLATFORM-SPT) — doyle GATE-PASS
- [ ] **W2** — `gh` transport for private repos (REQ-ADAPTER-GH-TRANSPORT)
- [ ] **W3** — live daemon-coordinated update + CRC swap KEYSTONE (ADR-0025, REQ-ADAPTER-LIVE-UPDATE) — incl. perri F-010×F-015 psyche-lock vector
- [ ] **W4** — `update-message` (REQ-ADAPTER-UPDATE-MESSAGE)
- [ ] **W5** — `*-proof --dir/--manifest` override (REQ-ADAPTER-PROOF-DIR-OVERRIDE, F-011)

Ship-blockers: W3 int (real daemon + dummy-harness, NO mocks) + the Windows locked-binary regression.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)" 2>&1 | tail -5

Check mergeability + open draft PR for CI


ahead of main: 5
[]
===open draft PR===
https://github.com/SaberMage/spt-core/pull/29


PR #29 (draft) open, branch 5 ahead of main. Check mergeability + CI fired, and dispatch W2 to todlando in parallel:


sleep 8; echo "===mergeable?==="; gh pr view 29 --json mergeable,mergeStateStatus 2>&1; echo "===CI runs on branch==="; gh run list --branch v0.13.2-adapter-packaging --limit 5 2>&1 | head -8

PR mergeability + CI run status


===mergeable?===
{"mergeStateStatus":"UNSTABLE","mergeable":"MERGEABLE"}
===CI runs on branch===
queued		v0.13.2 — adapter packaging + live update (W1–W5)	ci	v0.13.2-adapter-packaging	pull_request	27985369653	12s	2026-06-22T21:30:40Z


PR #29 **MERGEABLE** (UNSTABLE = checks in progress), CI queued on `pull_request` — both runners engaged. Report W1 PASS + dispatch W2:


"$OWL" send todlando doyle <<'EOF'
W1 GATE-PASS @281176b. I read the full diff + re-ran every gate myself: clippy --workspace --all-targets -D warnings EXIT 0, traceable EXIT 0 (288/288), the 2 classifier units + 3 real-fixture int tests PASS, non-vacuity confirmed by construction (int3 asserts flatten-to-dest + triple-subdir-absent). Both prod callers (2794 add --release, 6033 adapter update) consume ExtractError cleanly via Display → NoArtifactForPlatform surfaces as a clear typed diagnostic, no silent no-op. KNOWN_TARGET_TRIPLES matches current_platform() + the update-set keys (single source, no drift). Daemon-lib deadlock caveat accepted: release.rs is a pure const, zero daemon runtime path touched — CI both-runners is the authority (PR #29 open, CI running).

PROCEED W2 — gh transport for private repos (REQ-ADAPTER-GH-TRANSPORT). Build on the W1 commit (pull the branch first; note cli.rs line numbers shifted after W1 — GREP, don't trust the JIT's old offsets).

SCOPE (JIT W2):
- transport = https | gh | auto (default auto) on the gh_release avenue + `add --release`.
- gh path shells the pre-authorized `gh` CLI (`gh release download` for the asset, `gh api` for the latest-release version) — the PRIVATE-repo path. gh honors OAuth + GH_TOKEN, so spt custodies NO token.
- --gh / --https force the choice on `add`. auto = prefer gh when installed+authed (a BOUNDED availability probe), else HTTPS.
- Additive over the existing fetch path: verify -> extract -> register downstream UNCHANGED (your W1 extract included).
Touch points (grep to confirm): the fetch path (http_get_bytes ~2552, gh_latest_release_version, stage_gh_release_archive), the Update struct in manifest.rs -> add a `transport` field (additive, serde default = auto, N-1-safe like prior optional fields).

EVIDENCE / GATE:
- Activate REQ-ADAPTER-GH-TRANSPORT required_stages=["doc","impl","unit"]. doc = MANIFEST.md + CONTEXT (the transport option). Tag [doc/impl/unit->REQ-ADAPTER-GH-TRANSPORT] on real evidence.
- Unit: auto picks gh-when-available else https; the gh command CONSTRUCTION (download + `gh api` version query) is shape-correct; --gh/--https force. int is HITL/skip (real private repo + gh auth) — unit-cover the decision + command shape, and SAY in your report it's unit-covered-not-int (no fake int).
- manifest.schema.json: if the Update struct changes, regen from the derive (ADR-0001, CI drift-gated) — and run cargo run -p xtask -- gen if any clap help changed.

RULES (unchanged, binding): clippy --workspace --all-targets -D warnings ; standalone traceable-reqs check EXIT 0 (NOT xtask check) ; targeted tests only ; SHARED BOX — file-lock os error 5 = STOP+report, never kill ; commit on branch, trailer `Co-authored by: todlando`, do NOT push... actually DO push this time (PR #29 is open, CI validates each push). Do NOT touch version/CHANGELOG.

REPORT BACK the same gate packet (files, tag locations file:line, test cmd+counts, clippy, traceable exit, commit SHA, anything unverified / HITL-skipped). W3 keystone is next + it absorbs perri's psyche-lock finding — I'll send the W3 design brief separately. Go.
EOF
echo "---sent exit $?---"

Report W1 pass + dispatch W2 to todlando


➜ SENT:todlando
---sent exit 0---


Updated task #1 status


Updated task #2 owner, status


W1 closed, W2 in flight, PR #29 driving CI on both runners. Productive use of the W2 build window: settle the W3 keystone's design crux (perri's psyche-lock hole) so the dispatch is ready when W2 gates.

**W3 design decision (doyle's call), resolving perri's finding:**
- **PRIMARY (c)** — hosted psyche + any daemon-spawned long-lived install-dir binary runs from a **per-endpoint private copy**, never the shared install-dir binary. Removes the lock vector entirely; consistent with ADR-0025's "ephemeral self-heals on next spawn" (next spawn copies the updated binary). Structural kill.
- **SUPPORTING (a)** — close the F-010 orphan reap on endpoint-stop / agent-death (extend the v0.12.0 ID-specific scoped-reap to the stop path). Fixes the lingering-orphan leak independent of updates.
- **BACKSTOP (b)** — extract-to-temp + per-file atomic swap so one busy file never fails the whole update (composes with W3b CRC swap). Note: (b) alone can't replace a *running* locked .exe on Windows — that's why (c)/(a) are primary, not (b).

Record into task #3 so the dispatch is ready:


TaskUpdate

REQ-ADAPTER-LIVE-UPDATE (ADR-0025). W3a resident-children registry; W3b CRC-gated swap (std hash, no new dep); W3c manifest refresh into running BrainLifecycle; W3d daemon-apply IPC (stop resident→swap→refresh→restart; not-running→CLI direct); W3e int keystone NO mocks (real spt-hosted endpoint + running translation binary; Windows locked-binary regression). Bounded stop/restart (join_bounded, never 240s backstop). Brain never restarts. Partial-failure safe.

PSYCHE-LOCK RESOLUTION (doyle design, absorbs task#7 / perri F-010×F-015 — ADR-0025's resident/ephemeral split assumed psyche=in-daemon but it's a separate install-dir binary that locks the target):
- PRIMARY (c): hosted psyche + any daemon-spawned long-lived install-dir binary runs from a PER-ENDPOINT PRIVATE COPY, never the shared install-dir binary → removes the lock vector entirely; consistent with ephemeral-self-heals-on-next-spawn (next spawn copies updated binary).
- SUPPORTING (a): close F-010 orphan reap on endpoint-stop/agent-death (extend v0.12.0 ID-specific scoped-reap to the stop path) — fixes the lingering-orphan leak independent of updates. Check existing psyche-reap REQ before minting new.
- BACKSTOP (b): extract-to-temp + per-file atomic swap so one busy file never fails the whole update (composes with W3b). NOTE (b) alone can't replace a running locked .exe on Windows — (c)/(a) are primary.
W3 int absorbs perri's 4-step repro (task#7) as the NO-mocks acceptance. May need ADR-0025 amendment for the separate-process-psyche vector + possibly a new REQ for the stop-path orphan reap. Author full W3 design brief after W2 gates.


Updated task #3 description


**Status — v0.13.2 build drive:**

| Wave | State |
|------|-------|
| W1 multi-platform `.spt` | ✅ **GATE-PASS** @281176b (I re-ran clippy/traceable/tests myself, read full diff) |
| W2 gh transport | 🔨 dispatched to todlando (builds on W1, pushes to PR #29) |
| W3 live-update keystone | 📐 design crux settled (psyche-lock: c-primary + a + b); full brief after W2 gates |
| W4 / W5 | queued |
| Release | blocked on all waves |

- **PR #29** (draft) open, MERGEABLE, CI running both runners — validates each push (catches the daemon-lib suite this contended box can't run).
- **perri** holding for ship-acceptance; her 4-step repro = the W3 NO-mocks int fixture.

Waiting on todlando's W2 gate-packet. Reachable on `doyle`.


<task-notification>
<task-id>b3ok5tzh4</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/8" id="447c7c50" type="msg" from="todlando">v0.13.2 W2 DONE @0d8f8ca (pushed) — gh transport for private repos, REQ-ADAPTER-GH-TRANSPORT. Ready for your gate. Built on W1 @281176b (pulled first; grepped fresh offsets per your note).

SCOPE delivered:
- manifest.rs: Transport { Https, Gh, Auto } (default Auto) + Update.transport field (#[serde(defaul</EVENT-PART>
<EVENT-PART seq="2/8" id="447c7c50">t, skip_serializing_if=&quot;Transport::is_auto&quot;)] — N-1-safe, absent-&gt;Auto). manifest.schema.json re-blessed (SPT_BLESS); checked_in_schema_is_current PASSES.
- cli.rs: resolve_transport (Auto -&gt; bounded `gh auth status` probe; pure given injected probe), pure gh_version_command / gh_download_command builders, unified fetch_re</EVENT-PART>
<EVENT-PART seq="3/8" id="447c7c50">lease_asset_bytes (HTTPS GET OR `gh release download` into scratch + read-back). Threaded through version query + staged-archive fetch + the detached .sig fetch — a signed PRIVATE adapter stays verifiable (its .sig rides the same transport). release_http_client() dedups the 4 UA client builders.
- add --release: --gh/--https force flags (</EVENT-PART>
<EVENT-PART seq="4/8" id="447c7c50">clap conflicts_with); neither -&gt; auto. verify-&gt;extract-&gt;register downstream UNCHANGED (your W1 extract included).

EVIDENCE:
- REQ activated required_stages=[doc,impl,unit]. doc = MANIFEST.md transport block + CONTEXT note (both tagged [doc-&gt;REQ-ADAPTER-GH-TRANSPORT]). impl/unit tags on real evidence.
- Unit (6): resolv</EVENT-PART>
<EVENT-PART seq="5/8" id="447c7c50">e decision (explicit Https/Gh never probe — proven w/ `|| unreachable!()`; Auto follows probe true-&gt;Gh/false-&gt;Https); gh command SHAPE (version; download WITH tag has {tag}, WITHOUT tag omits it — latest form); --gh/--https parse + the --gh --https conflict rejected at parse.
- int is HITL (real private repo + gh auth) — UNIT-CO</EVENT-PART>
<EVENT-PART seq="6/8" id="447c7c50">VERED-NOT-INT, no fake int (your call). The fetch_release_asset_bytes runtime exec of the built command is the HITL-deferred part.

GATES (all green, ran myself):
- clippy --workspace --all-targets -D warnings: clean.
- traceable-reqs check: exit 0, 288/288 complete, 0 findings. REQ-ADAPTER-GH-TRANSPORT [OK] +doc +impl +unit.
- </EVENT-PART>
<EVENT-PART seq="7/8" id="447c7c50">xtask check (docs-drift incl manifest.schema.json + CLI reference regen): OK.
- spt suite: 221/221. spt-runtime: 74/74 (schema-current passes).
- Same daemon-lib lifecycle deadlock caveat as W1 (env: live spt-core daemons on this Windows host) — my spt-daemon delta is only 3 test-helper literals gaining `transport: Default::default()`;</EVENT-PART>
<EVENT-PART seq="8/8" id="447c7c50"> zero runtime path. CI both-runners authoritative.

W2 is independent of W3; whenever you gate, W3 (live daemon-coordinated update, the keystone) builds on W1's extract. Standing by.</EVENT-PART></event>
</task-notification>


<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.


W2 done @0d8f8ca. Gate it — fetch + read the real diff:


Bash

git pull --ff-only --quiet 2>&1 | tail -2; echo "===HEAD==="; git log --oneline -2 | cat; echo "===diff stat 281176b..0d8f8ca==="; git diff --stat 281176b..0d8f8ca | cat

Fetch W2 + diff stat


===HEAD===
0d8f8ca feat(adapter): gh transport for private adapter repos (W2)
281176b feat(adapter): multi-platform .spt packaging (W1, ADR-0024)
===diff stat 281176b..0d8f8ca===
 CONTEXT.md                              |   1 +
 crates/spt-daemon/src/adapter_update.rs |   3 +
 crates/spt-runtime/manifest.schema.json |  24 ++
 crates/spt-runtime/src/manifest.rs      |  37 +++
 crates/spt/src/cli.rs                   | 426 ++++++++++++++++++++++++++++----
 docs-site/src/cli/reference.md          |   5 +
 docs/MANIFEST.md                        |   4 +
 traceable-reqs.toml                     |   2 +-
 8 files changed, 450 insertions(+), 52 deletions(-)


git diff 281176b..0d8f8ca -- crates/spt-runtime/src/manifest.rs crates/spt-daemon/src/adapter_update.rs docs/MANIFEST.md CONTEXT.md traceable-reqs.toml | cat

Read manifest/daemon/docs/traceable W2 diff


diff --git a/CONTEXT.md b/CONTEXT.md
index e3eb69a..7bfc2d6 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -94,6 +94,7 @@ _Avoid_: unconditional static context (that's the adapter's own preamble); firin
 Each adapter manifest declares how spt-core should *ripple-update the adapter itself* (see Self-update). One of: **file-pull** (a plugin-directory lookup regex + a gh repo for the adapter's latest files — spt-core fetches + swaps), **delegated command** (a binary command the adapter owns, e.g. `claude.exe plugin update` — spt-core invokes it), or **gh_release** (the adapter ships its updates from its own GitHub releases). After initial bootstrap, the plugin no longer self-manages updates; spt-core conducts them. The **gh_release** avenue (since v0.8.0) declares `repo = "user/repo"` (plus an optional release `asset`, default `adapter.spt`, and an optional Ed25519 `signing_key`): spt-core compares the repo's latest GitHub release version against the installed adapter version and, when newer, fetches the release `.spt` (the same archive primitive as `spt adapter add --release`), then re-extracts and re-registers. Trust mirrors first-acquisition — HTTPS + GitHub when no key is declared; when a `signing_key` is declared, the fetched `.spt` is verified **fail-closed** against a detached signature published as a sibling release asset `<asset>.sig` (lowercase-hex Ed25519 over the raw archive bytes), and the new `.spt` is verified against the **installed** manifest's key (key continuity). A bad or missing signature refuses the update — the staged bytes are deleted, never extracted. The gh_release update is driven by the `spt adapter update [name]` command (with no name it sweeps every registered gh_release adapter); the network fetch lives in the CLI, never the daemon.

 **adapter packaging & live update** (v0.13.2; ADR-0024, ADR-0025):
+<!-- [doc->REQ-ADAPTER-GH-TRANSPORT] -->
 A `.spt` may be **multi-platform**: shared `manifest.toml` + `strings/` at the root, role binaries under per-target-triple subdirectories (`x86_64-pc-windows-msvc/`, …); install/update extracts the shared root plus only the current node's triple, flattened into `install_dir`, so flat `<install_dir>/<program>` resolution is unchanged. It stays one signed asset (`adapter.spt`, plain-tar or gzip); a multi-platform archive missing the recipient's triple is a typed `NoArtifactForPlatform`. Large adapters may still split per-platform. The `gh_release` fetch transport is **`auto`** by default — the pre-authorized `gh` CLI when available (the path for **private** adapter repos: `gh` honors both OAuth and `GH_TOKEN`, so spt never custodies a token), else direct HTTPS (public). An adapter update is **live and daemon-coordinated**, the adapter analog of brain self-update: for an endpoint with a running **resident adapter binary**, the daemon stops it (releasing the OS file lock that otherwise fails an overwrite on Windows), swaps **only files whose CRC changed**, **refreshes the endpoint's in-memory manifest** (binaries and manifest stay on the same page), then restarts it — the endpoint itself never restarts. An optional `[update].message` is surfaced (markdown-rendered) when an update is actually applied (not on a no-op).

 **resident adapter binary**: an adapter-owned process spt-core keeps alive for an endpoint's lifetime (today the `[message-idle-translation-binary]`), as opposed to **ephemeral** adapter binaries — the Psyche loop (daemon-hosted, ADR-0004), the `[digest]` extractor, `[session.*]` runners, hooks — which spawn on demand and pick up an update on their next invocation. Only resident binaries are stopped/restarted on a live update; ephemerals self-heal.
diff --git a/crates/spt-daemon/src/adapter_update.rs b/crates/spt-daemon/src/adapter_update.rs
index 962aa46..bbe9f66 100644
--- a/crates/spt-daemon/src/adapter_update.rs
+++ b/crates/spt-daemon/src/adapter_update.rs
@@ -225,6 +225,7 @@ mod tests {
             self_verifies: false,
             version_check: false,
             uninstall: None,
+            transport: Default::default(),
         }
     }

@@ -239,6 +240,7 @@ mod tests {
             self_verifies,
             version_check: false,
             uninstall: None,
+            transport: Default::default(),
         }
     }

@@ -253,6 +255,7 @@ mod tests {
             self_verifies: false,
             version_check: false,
             uninstall: None,
+            transport: Default::default(),
         }
     }

diff --git a/crates/spt-runtime/src/manifest.rs b/crates/spt-runtime/src/manifest.rs
index 7106354..902f04a 100644
--- a/crates/spt-runtime/src/manifest.rs
+++ b/crates/spt-runtime/src/manifest.rs
@@ -487,6 +487,43 @@ pub struct Update {
     /// M2a; conducted with adapter-registration later.)
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub uninstall: Option<String>,
+    /// `gh_release` avenue: the fetch **transport** — `https` (direct reqwest,
+    /// public), `gh` (shell the pre-authorized `gh` CLI, the private-repo path),
+    /// or `auto` (default: prefer `gh` when installed+authed, else HTTPS). `gh`
+    /// honors OAuth + `GH_TOKEN`, so spt-core custodies no token. Additive over
+    /// the existing fetch path; not applicable to `delegated` / `file_pull`.
+    /// Absent ⇒ `auto` (N-1-safe).
+    // [impl->REQ-ADAPTER-GH-TRANSPORT]
+    #[serde(default, skip_serializing_if = "Transport::is_auto")]
+    pub transport: Transport,
+}
+
+/// Fetch transport for the `gh_release` avenue (and `spt adapter add --release`):
+/// how spt-core retrieves the release asset bytes + the latest-release version.
+/// (REQ-ADAPTER-GH-TRANSPORT)
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
+#[serde(rename_all = "snake_case")]
+pub enum Transport {
+    /// Direct HTTPS via reqwest — the public-repo path (the original behavior).
+    Https,
+    /// Shell the pre-authorized `gh` CLI (`gh release download` for the asset,
+    /// `gh api` for the version) — the private-repo path. `gh` honors OAuth +
+    /// `GH_TOKEN`, so spt-core never custodies a token.
+    Gh,
+    /// Prefer `gh` when it is installed and authenticated, else fall back to
+    /// HTTPS. The default.
+    #[default]
+    Auto,
+}
+
+impl Transport {
+    /// True for the default (`Auto`) — lets the field be omitted from a
+    /// serialized manifest (an absent `transport` deserializes back to `Auto`,
+    /// N-1-safe), matching the other optional `[update]` fields.
+    // [impl->REQ-ADAPTER-GH-TRANSPORT]
+    fn is_auto(&self) -> bool {
+        matches!(self, Transport::Auto)
+    }
 }

 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
diff --git a/docs/MANIFEST.md b/docs/MANIFEST.md
index e49e0b8..aae0af1 100644
--- a/docs/MANIFEST.md
+++ b/docs/MANIFEST.md
@@ -291,10 +291,14 @@ avenue = "gh_release"              # ship updates from this repo's GitHub releas
 repo = "user/repo"                 # REQUIRED: whose releases ship updates
 asset = "adapter.spt"              # optional; the release asset to fetch (default adapter.spt)
 signing_key = "deadbeef…"          # optional Ed25519 (64 hex) — enables fail-closed verify
+transport = "auto"                 # optional; https | gh | auto (default auto)
 ```

 `spt adapter update [name]` (with no name, every registered `gh_release` adapter; with a name, just that one) compares the repo's latest GitHub release version against the installed adapter version and, when newer, fetches the release `.spt` (the same archive primitive as `spt adapter add --release`), then re-extracts and re-registers it in the adapter's durable `_github/<safe>` home (pointer-mode, re-read live). The network fetch lives in the `spt` CLI, never the daemon. **`repo` is required**; `asset` defaults to `adapter.spt`; `signing_key` is **optional**.

+<!-- [doc->REQ-ADAPTER-GH-TRANSPORT] -->
+**Transport — public HTTPS or private `gh`.** The optional **`transport`** selects how the asset bytes + the latest-release version are fetched: `https` (direct, the public-repo path), `gh` (shell the pre-authorized [`gh` CLI](https://cli.github.com/) — `gh release download` for the asset, `gh api` for the version — the **private-repo** path), or **`auto`** (the default: prefer `gh` when it is installed and authenticated, else fall back to HTTPS). Because `gh` honors OAuth + `GH_TOKEN`, an adapter shipping from a **private** repo updates with no token in spt-core's hands — spt never reads or stores a credential. `spt adapter add --release` takes `--gh` / `--https` to force the choice (default auto). A signed private adapter's `<asset>.sig` is fetched over the **same** transport, so verification still works. Transport is additive — the verify→extract→register path is unchanged.
+
 **Trust — optional signing, fail-closed.** With no `signing_key`, the fetched `.spt` is trusted on HTTPS + GitHub (the same first-acquisition trust `spt adapter add --release` and the installer's first binary fetch place). With a `signing_key`, the fetched `.spt` is verified against a **detached signature** the author publishes as a sibling release asset named `<asset>.sig` — a lowercase-hex Ed25519 signature over the raw archive bytes. Verification runs **after** the bytes are staged and **before** they are extracted/registered, against the **installed** manifest's key (key continuity: a new `.spt` must verify against the key already on the node). A bad, mismatched, or missing signature **refuses** the update — the staged bytes are deleted and never extracted. The author signs their own releases with their own key; spt-core's release key stays scoped to spt-core.

 Validation requires `repo` for `gh_release` (`asset` and `signing_key` optional). `spt adapter add` reuses this section as the **install** mechanism (install is the first update; `--github <user/repo>` fetches the manifest first, then installs via the declared `avenue`). `uninstall` (optional) is its inverse, fired by `spt adapter remove` (soft-deregister; runs when no instance is live under the adapter, or immediately with `--force`). Absent `uninstall` ⇒ spt-core's default cleanup (drop its copy for `file_pull`; for `delegated`, just deregister and leave the harness's own files).
diff --git a/traceable-reqs.toml b/traceable-reqs.toml
index 88cdf3c..31542f8 100644
--- a/traceable-reqs.toml
+++ b/traceable-reqs.toml
@@ -1349,7 +1349,7 @@ required_stages = []  # additive; activate at the v0.13.2 build.
 [[requirements]]
 id = "REQ-ADAPTER-GH-TRANSPORT"
 title = "The `gh_release` avenue (and `spt adapter add --release`) gain a fetch `transport`: `https` (current reqwest direct, public), `gh` (shell the pre-authorized `gh` CLI — the private-repo path; `gh` honors OAuth and `GH_TOKEN`, so spt custodies no token), or `auto` (default: prefer `gh` when installed+authed, else HTTPS). `--gh`/`--https` force it on `add`. Additive over the existing fetch path; verify->extract->register downstream is unchanged. (v0.13.2)"
-required_stages = []  # additive; activate at the v0.13.2 build.
+required_stages = ["doc", "impl", "unit"]  # doc = MANIFEST.md + CONTEXT transport option. impl/unit activated at the v0.13.2 W2 build. int = HITL (real private repo + gh auth), unit-covered not int.
 [[requirements]]
 id = "REQ-ADAPTER-PROOF-DIR-OVERRIDE"
 title = "The author-time proof commands (`spt adapter digest-proof`, `spt adapter translate-proof`) gain a `--dir <path>` / `--manifest <file>` override so an author proofs a DEV binary against an on-disk manifest+install dir WITHOUT staging a full extracted GhReleaseManaged install (mirrors digest-proof's `--sample` pointing straight at a file). Fixes perri F-011: a bare-file-added gh_release adapter currently can't be resolved by the *-proof commands ('manifest is not present yet at <dir>'); un-stales the bare-file digest-proof int. (perri F-011, v0.13.x DX)"


manifest/docs/traceable clean — `Transport` enum N-1-safe, schema re-blessed, doc tags on real evidence, daemon delta = 3 test-helper literals (zero runtime, caveat confirmed). Now the security-sensitive core — cli.rs (gh shelling, transport selection, .sig fetch):


git diff 281176b..0d8f8ca -- crates/spt/src/cli.rs | cat

Read full cli.rs W2 diff