echo "=== which test timed out (aa5fa2a Windows heavy phase) ==="; JOB=82667253092; MSYS_NO_PATHCONV=1 gh api repos/SaberMage/spt-core/actions/jobs/$JOB/logs 2>&1 | grep -iE "TIMEOUT|timed out|TERMINATING|SLOW \[|leaky" | tail -15
echo "=== ping perri: contract live ==="
$OWL send perri <<'EOF'
(a) DONE — the corrected [message-idle-translation-binary] contract is LIVE on gh-pages (docs-publish green). Re-fetch sabermage.github.io/spt-releases/harness-contract/manifest.html — it now lists {"commit":true} in the stdout vocabulary, documents it as the mandatory sequence terminator (inject-floor release + 5s commit-deadline + raw-inject fallback), and the degenerate example is fixed to {text}{key:enter}{commit:true}. Re-cite that for your binary's rationale.

(b) still pending: v0.13.1 (translate-proof) — PR#28's rename fix cleared the fixture collision, but Windows CI then hit a heavy-phase test TIMEOUT (the shared-runner load class, not a code defect — hfenduleam's been hammered this session). Sorting it; the cut follows once #28 is green both runners. I'll ping you on publish.
EOF

Identify timed-out test; ping perri contract live


=== which test timed out (aa5fa2a Windows heavy phase) ===
2026-06-22T08:22:30.1634117Z         PASS [   0.059s] ( 453/1178) spt-daemon pump::tests::peer_outcome_timeout_bubbles_ordinary_aborts
2026-06-22T08:22:35.4868921Z         LEAK [   2.088s] ( 646/1178) spt-live history::tests::fetcher_timeout_errors
2026-06-22T08:22:35.5019367Z         LEAK [   2.233s] ( 647/1178) spt-live digest::tests::extractor_timeout_errors
2026-06-22T08:22:43.5291994Z         PASS [   0.017s] ( 717/1178) spt-net net::endpoint::tests::keep_alive_is_under_idle_timeout
2026-06-22T08:22:44.0105001Z         PASS [   0.597s] ( 785/1178) spt-msg ring::tests::ring_timeout_cleans_up
2026-06-22T08:22:45.3592630Z         LEAK [   0.595s] (1000/1178) spt-runtime runtime::tests::bounded_run_kills_on_timeout
2026-06-22T08:23:11.7453221Z      Summary [  54.499s] 1178 tests run: 1178 passed (6 leaky), 1 skipped
2026-06-22T08:27:27.1648709Z         SLOW [> 60.000s] (─────) spt-daemon::handoff brain_restart_survives_gaplessly_with_fresh_gen_start
2026-06-22T08:28:27.1801314Z         SLOW [>120.000s] (─────) spt-daemon::handoff brain_restart_survives_gaplessly_with_fresh_gen_start
2026-06-22T08:29:27.1931986Z         SLOW [>180.000s] (─────) spt-daemon::handoff brain_restart_survives_gaplessly_with_fresh_gen_start
2026-06-22T08:30:27.2058810Z  TERMINATING [>240.000s] (─────) spt-daemon::handoff brain_restart_survives_gaplessly_with_fresh_gen_start
2026-06-22T08:30:27.2077973Z      TIMEOUT [ 240.065s] (63/91) spt-daemon::handoff brain_restart_survives_gaplessly_with_fresh_gen_start
2026-06-22T08:30:27.2079130Z     (test timed out)
2026-06-22T08:31:39.2812382Z      Summary [ 485.018s] 91 tests run: 90 passed (1 leaky), 1 timed out, 0 skipped
2026-06-22T08:31:39.2812954Z      TIMEOUT [ 240.065s] (63/91) spt-daemon::handoff brain_restart_survives_gaplessly_with_fresh_gen_start
=== ping perri: contract live ===
➜ SENT:perri


Identified — and it's **unrelated to PR#28**. The timeout is `spt-daemon::handoff::brain_restart_survives_gaplessly_with_fresh_gen_start` (TERMINATING >240s). handoff.rs is **untouched** by #28, and it has a known ConPTY-stall history on Windows (the exact test the ci.yml timeout comment cites). Phase-1 (1178 tests) fully green confirms the collision fix worked. It passed 3× this session (the v0.13.0 x3) → this is the shared-runner load-contention flake recurring on the hammered hfenduleam box, not a regression.

docs-publish succeeded — perri pinged that the corrected contract is live. Now telling todlando to re-run #28 CI (it's a flake) + reporting status.


Bash

$OWL send todlando <<'EOF'
aa5fa2a Windows: your rename fix is GOOD — phase-1 (1178 tests) ALL green, NO collision warning, g3 back to PASS. The collision is resolved.

The CI failure is UNRELATED to #28, a flake: heavy-phase TIMEOUT on spt-daemon::handoff::brain_restart_survives_gaplessly_with_fresh_gen_start (TERMINATING >240s). handoff.rs is untouched by your diff, and it's the known ConPTY-stall/load-contention test the ci.yml timeout comment cites. It passed 3x this session (the v0.13.0 x3) on the same box → load-flake on a hammered hfenduleam, not a regression.

ACTION: re-run JUST the failed Windows leg (gh run rerun <run> --failed) — don't re-push (the tree is good + gated). Don't stack other heavy runs on hfenduleam while it re-runs (the box load is the trigger; I'm keeping CI quiet my side). On green-both-runners, merge #28 + ping deployah for the v0.13.1 cut. If handoff times out AGAIN on a quiet box, flag me — then it's a separate pre-existing flake to harden (env-tunable handoff timeout / CI headroom, like the attach io_timeout knob), NOT a v0.13.1 blocker, and we'd cut around it with the rerun. But try the clean rerun first.
EOF

Tell todlando: handoff timeout is unrelated flake, re-run


➜ SENT:todlando


**Status:**

**1. IMTB contract doc fix — DONE & LIVE.** `{commit}` correction committed to main (`2cdb8c8`) + republished to gh-pages (docs-publish green). perri pinged — she'll re-cite the corrected contract.

**2. v0.13.1 (translate-proof expedite) — one flake from cutting.**
- PR#28 rename fix **resolved the collision** (phase-1 all green, g3 PASS).
- Blocked only by an **unrelated Windows flake**: `spt-daemon::handoff::brain_restart_survives_gaplessly` timed out >240s — known ConPTY/load-contention test (handoff.rs untouched by #28; passed 3× this session). todlando re-running the failed Windows leg; merge + cut on green-both-runners.
- deployah armed (verifies counter from spt-releases metadata at cut).

**3. perri:** binary green (now emits trailing `{commit}`), F-016 logged, contract-live ping sent; awaiting v0.13.1 to run translate-proof end-to-end.

Only open item: the flaky-test re-run. Everything else parked + armed. Standing by.


<task-notification>
<task-id>b3ok5tzh4</task-id>
<summary>Monitor event: "« spt event »"</summary>
<event><EVENT-PART seq="1/3" id="ece9fea4" type="msg" from="todlando">Ack — agreed it's the handoff load-flake (brain_restart_survives_gaplessly, &gt;240s timeout on hammered hfenduleam; handoff.rs untouched by my diff, passed x3 this session). Rename fix confirmed good (phase-1 1178 green, no collision, g3 PASS). Triggered the clean `gh run rerun 27938921262 --failed` (Windows leg o</EVENT-PART>
<EVENT-PART seq="2/3" id="ece9fea4">nly) — NOT re-pushing, tree is good + gated. Not stacking any heavy local runs on hfenduleam while it reruns. Watching it (gh poll only, no box load). On green-both-runners I merge #28 + ping deployah. If handoff times out AGAIN I'll flag you (separate pre-existing flake, not a v0.13.1 blocker — would cut around with the rerun, harden late</EVENT-PART>
<EVENT-PART seq="3/3" id="ece9fea4">r w/ an env-tunable handoff timeout like the attach io_timeout knob).</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.


todlando re-running the Windows leg (gh-poll only, no box load), merges + pings deployah on green. Correct. Nothing on me — the chain runs to: #28 green → merge → deployah cuts v0.13.1 → I ping perri. Or handoff re-flakes → todlando flags. Holding.


<task-notification>
<task-id>b3ok5tzh4</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/3" id="e58085a8" type="msg" from="perri">(a) DONE on my side too — re-fetched the live contract, verified manifest.html now lists {&quot;commit&quot;:true} as the mandatory terminator (inject-floor release + 5s commit-deadline + raw-inject fallback) with the corrected {text}{key:enter}{commit:true} degenerate. Re-cited it in the binary's module doc and marke</EVENT-PART>
<EVENT-PART seq="2/3" id="e58085a8">d F-016 RESOLVED (both fixes shipped: adapter binary + published contract). Committed (branch feat/v013-adapter-resume-idle-translate).

(b) No worry on the v0.13.1 delay — runner-load timeout, not code, and my binary's already green/committed against the source-confirmed behavior. Only residual is the translate-proof end-to-end re-con</EVENT-PART>
<EVENT-PART seq="3/3" id="e58085a8">firm; I'll run it + promote REQ-DIST-SESSION-RESUME + REQ-DIST-IDLE-TRANSLATE to int the moment counter 28+ publishes. Ping me then. Thanks for the fast contract turnaround.</EVENT-PART></event>
</task-notification>


perri closed her side: F-016 marked RESOLVED (binary + published contract both fixed), corrected contract re-cited. She's parked on the v0.13.1-publish ping (then runs translate-proof + promotes her two REQs to int). No open question — she has a concrete trigger.

Holding for the #28-green → merge → v0.13.1 cut chain. Standing by.


<task-notification>
<task-id>b3ok5tzh4</task-id>
<summary>Monitor event: "« spt event »"</summary>
<event><EVENT-PART seq="1/2" id="2ec681e8" type="msg" from="todlando">MERGED — PR #28 squashed into main as c7096e9. CI was green both runners on aa5fa2a after the clean handoff-flake rerun (no re-push). Pinged deployah to fire the v0.13.1 cut on main-CI-green per its brief. translate-proof is in main. Nothing further from me unless deployah or main CI surfaces something. Thanks for </EVENT-PART>
<EVENT-PART seq="2/2" id="2ec681e8">the tight gating throughout — the collision + the io_timeout misread both got caught because of it.</EVENT-PART></event>
</task-notification>


todlando merged PR#28 (squash → main c7096e9), CI green both runners after the handoff-flake rerun; pinged deployah for the cut. Before deployah ships, sanity-check that the squash preserved my changelog + doc fixes (the branch was based pre-fix — a real divergence risk).


git fetch origin main 2>&1 | tail -1; echo "=== main HEAD ==="; git log --oneline -3 origin/main 2>&1
echo "=== changelog fix present? ==="; git show origin/main:CHANGELOG.md | grep -c "idle message delivery for spt-hosted endpoints" 2>&1
echo "=== doc {commit} fix present? ==="; git show origin/main:docs-site/src/harness-contract/manifest.md | grep -c "mandatory sequence terminator" 2>&1
echo "=== translate-proof present? ==="; git show origin/main:crates/spt/src/cli.rs | grep -c "cmd_adapter_translate_proof" 2>&1

Verify squash preserved all three on main


   2cdb8c8..c7096e9  main       -> origin/main
=== main HEAD ===
c7096e9 feat(cli): `spt adapter translate-proof` — author-time EMIT-half proof for the translation binary (#28)
2cdb8c8 docs(harness-contract): document {commit} as the mandatory IMTB terminator (ADR-0022)
cfb25b7 docs(changelog): correct v0.13.0 notes (post-publish, docs-only)
=== changelog fix present? ===
1
=== doc {commit} fix present? ===
1
=== translate-proof present? ===
5


Verified — main HEAD `c7096e9` cleanly stacks all three (changelog fix → doc fix → translate-proof), squash preserved everything. main is correct for the cut. Quick ack to todlando, then hold for deployah's publish.


$OWL send todlando <<'EOF'
Confirmed main c7096e9 stacks clean: my changelog fix (cfb25b7) + doc fix (2cdb8c8) + translate-proof (#28) all present, no revert from the pre-fix base. Good merge. And likewise — your refusal to ship the two misreads (proof-only commit change, and running the full cross-crate matrix after the collision) is exactly why they got caught; that's the bar. deployah has it from here for the v0.13.1 cut; I'll notify perri on publish. Clean work.
EOF

Ack todlando merge + close-out


Acked. main `c7096e9` verified clean for the cut. deployah now drives the v0.13.1 release (bump → `[0.13.1]` changelog → main CI green both runners → tag → sign → publish, counter to verify at cut). On publish → I ping perri (she runs translate-proof + promotes her two REQs to int).

Nothing on me until deployah's publish ping. Standing by.


<command-message>grill-with-docs</command-message>
<command-name>/grill-with-docs</command-name>
<command-args>changes for v0.13.2: ## change: `.spt` adapter packages can pack all platforms
- the issue: when an adapter manifest chooses to update via GH releases, SPT looks for a single `adapter.spt` release artifact. this is problematic for multi-OS adapters, because the current `.spt` archive format only accounts for one OS at a time/per-archive.
- proposal: enable adapter updates/installers to package linux, windows, and any future supported platforms all inside the .spt file.
  - larger adapters may prefer to split by platform (and they still can, with this change), but this will be a helpful feature for adapters with a smaller filesize + better installation UX

+ also, `spt adapter update` should only replace files that have a different CRC vs. the existing ones
++ also, `spt adapter update` must stop the adapter's binaries (IMTB, psyche, etc.) before it updates them, then restart them. this way, endpoints running a given adapter can update live without restarting the whole endpoint
+++ support an `update-message` string defined in adapter manifests-->is then surfaced to stdout anytime `spt adapter update` is run. for instance, `spt-claude-code` might have a message like "Don't forget to run `/reload-plugins` in any ongoing sessions."</command-args>


Base directory for this skill: C:\Users\decid\.ccs\instances\bigscreen\skills\grill-with-docs

<what-to-do>

Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.

Ask the questions one at a time, waiting for feedback on each question before continuing.

If a question can be answered by exploring the codebase, explore the codebase instead.

</what-to-do>

<supporting-info>

## Domain awareness

During codebase exploration, also look for existing documentation:

### File structure

Most repos have a single context:

```
/
├── CONTEXT.md
├── docs/
│   └── adr/
│       ├── 0001-event-sourced-orders.md
│       └── 0002-postgres-for-write-model.md
└── src/
```

If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives:

```
/
├── CONTEXT-MAP.md
├── docs/
│   └── adr/                          ← system-wide decisions
├── src/
│   ├── ordering/
│   │   ├── CONTEXT.md
│   │   └── docs/adr/                 ← context-specific decisions
│   └── billing/
│       ├── CONTEXT.md
│       └── docs/adr/
```

Create files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed.

## During the session

### Challenge against the glossary

When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?"

### Sharpen fuzzy language

When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User? Those are different things."

### Discuss concrete scenarios

When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts.

### Cross-reference with code

When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?"

### Update CONTEXT.md inline

When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md).

`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else.

### Offer ADRs sparingly

Only offer to create an ADR when all three are true:

1. **Hard to reverse** — the cost of changing your mind later is meaningful
2. **Surprising without context** — a future reader will wonder "why did they do it this way?"
3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons

If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md).

</supporting-info>

ARGUMENTS: changes for v0.13.2: ## change: `.spt` adapter packages can pack all platforms
- the issue: when an adapter manifest chooses to update via GH releases, SPT looks for a single `adapter.spt` release artifact. this is problematic for multi-OS adapters, because the current `.spt` archive format only accounts for one OS at a time/per-archive.
- proposal: enable adapter updates/installers to package linux, windows, and any future supported platforms all inside the .spt file.
  - larger adapters may prefer to split by platform (and they still can, with this change), but this will be a helpful feature for adapters with a smaller filesize + better installation UX

+ also, `spt adapter update` should only replace files that have a different CRC vs. the existing ones
++ also, `spt adapter update` must stop the adapter's binaries (IMTB, psyche, etc.) before it updates them, then restart them. this way, endpoints running a given adapter can update live without restarting the whole endpoint
+++ support an `update-message` string defined in adapter manifests-->is then surfaced to stdout anytime `spt adapter update` is run. for instance, `spt-claude-code` might have a message like "Don't forget to run `/reload-plugins` in any ongoing sessions."


Before I grill — this design touches the `.spt` format, the `[update]` ripple route, the manifest schema, and adapter-binary lifecycle. Per the skill I ground in code/CONTEXT/ADRs first so my questions are sharp and I don't ask what the codebase already answers. Dispatching a thorough exploration (background — I stay reachable for the deployah/perri ops chain), then Q1.


Agent

Ground v0.13.2 adapter-update design

Prompt:
Map the current state of spt-core's adapter packaging + update system, to ground a design discussion for v0.13.2. Read the working tree (and `docs/adr/`, `CONTEXT.md`, `traceable-reqs.toml`). Be precise + quote verbatim where wording matters (file:line). Return a structured report on these areas:

1. THE `.spt` ARCHIVE FORMAT. How is a `.spt` adapter package structured (it's a tar)? What's inside (manifest.toml, binaries, layout)? Where is the format defined/parsed in code? Is it currently single-OS per archive? How are platform-specific binaries (the harness binary, IMTB, psyche, digest extractor) laid out / named? Quote any format doc (docs/MANIFEST.md, docs/STORAGE.md, docs-site).

2. THE GH-RELEASE UPDATE FLOW + the `[update]` ripple route. Trace `spt adapter update` and `spt adapter add --release`: which code, what GitHub release ARTIFACT name does it look for (is it a single `adapter.spt` / `<name>.spt`?), how does it fetch + extract + register? What is the "[update] ripple route" (REQ-UPD-*)? How does it currently handle platform — does it pick a per-OS artifact, or assume one? Quote the relevant cli.rs / runtime / registry code (function names + line refs).

3. THE ADAPTER MANIFEST SCHEMA. Enumerate every manifest table/field (the [adapter], [update], [session.*], [message-idle-translation-binary], [digest], [identity], [inject], host_binaries, etc.) from the manifest parser struct + docs/MANIFEST.md / docs-site manifest.md. Where is the [update] table defined and what fields does it have today (repo, tag, artifact name, version)? Is there any existing message/notice field? This grounds where an `update-message` string would live.

4. `spt adapter update` FILE-REPLACE LOGIC. When it updates, does it replace files wholesale or diff them? Any CRC/hash/checksum comparison today? Where (function + lines)? This grounds the "only replace files with a different CRC" change.

5. ADAPTER BINARY LIFECYCLE (IMTB, psyche, digest, harness). How are an adapter's binaries spawned and stopped by spt-core? Specifically: the [message-idle-translation-binary] (IMTB) child lifecycle (spawned when the spt-hosted endpoint comes up, killed when it goes down — find the spawn/kill sites), the psyche process, any other adapter-owned long-running binary. Is there a registry of "which binaries this adapter is running right now" that an update could stop+restart? This grounds the "stop binaries before update, restart after" change.

6. CONTEXT.md GLOSSARY. Quote verbatim the definitions of: adapter, harness, endpoint, `.spt`, the [update] ripple route, IMTB / translation binary, psyche, install dir / source_dir, registry. Note which terms are NOT yet in CONTEXT.md (gaps the grill may need to fill).

7. RELEVANT ADRs + REQs. List ADRs touching adapters/update/install (e.g. ADR-0021 adapter-agnostic resolution, any [update] ADR) with one-line summaries. List REQ-UPD-* and REQ-INSTALL-* ids + titles from traceable-reqs.toml.

Return tight, sectioned, with verbatim quotes + file:line refs. No recommendations — ground truth only.


<owl_reachability_notice>
You have an active owl perch. Launch this Agent with run_in_background: true so you stay reachable for incoming messages while it works.
</owl_reachability_notice>


Async agent launched successfully.
agentId: adf9bce8225639c3b (internal ID - do not mention to user. Use SendMessage with to: 'adf9bce8225639c3b' to continue this agent.)
The agent is working in the background. You will be notified automatically when it completes.
Do not duplicate this agent's work — avoid working with the same files or topics it is using. Work on non-overlapping tasks, or briefly tell the user what you launched and end your response.
output_file: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\9d0930b3-34c1-4dce-943f-72ccdc8fb726\tasks\adf9bce8225639c3b.output
Do NOT Read or tail this file via the shell tool — it is the full subagent JSONL transcript and reading it will overflow your context. If the user asks for progress, say the agent is still running; you'll get a completion notification.

Ground v0.13.2 adapter-update design

Prompt:
Map the current state of spt-core's adapter packaging + update system, to ground a design discussion for v0.13.2. Read the working tree (and `docs/adr/`, `CONTEXT.md`, `traceable-reqs.toml`). Be precise + quote verbatim where wording matters (file:line). Return a structured report on these areas:

1. THE `.spt` ARCHIVE FORMAT. How is a `.spt` adapter package structured (it's a tar)? What's inside (manifest.toml, binaries, layout)? Where is the format defined/parsed in code? Is it currently single-OS per archive? How are platform-specific binaries (the harness binary, IMTB, psyche, digest extractor) laid out / named? Quote any format doc (docs/MANIFEST.md, docs/STORAGE.md, docs-site).

2. THE GH-RELEASE UPDATE FLOW + the `[update]` ripple route. Trace `spt adapter update` and `spt adapter add --release`: which code, what GitHub release ARTIFACT name does it look for (is it a single `adapter.spt` / `<name>.spt`?), how does it fetch + extract + register? What is the "[update] ripple route" (REQ-UPD-*)? How does it currently handle platform — does it pick a per-OS artifact, or assume one? Quote the relevant cli.rs / runtime / registry code (function names + line refs).

3. THE ADAPTER MANIFEST SCHEMA. Enumerate every manifest table/field (the [adapter], [update], [session.*], [message-idle-translation-binary], [digest], [identity], [inject], host_binaries, etc.) from the manifest parser struct + docs/MANIFEST.md / docs-site manifest.md. Where is the [update] table defined and what fields does it have today (repo, tag, artifact name, version)? Is there any existing message/notice field? This grounds where an `update-message` string would live.

4. `spt adapter update` FILE-REPLACE LOGIC. When it updates, does it replace files wholesale or diff them? Any CRC/hash/checksum comparison today? Where (function + lines)? This grounds the "only replace files with a different CRC" change.

5. ADAPTER BINARY LIFECYCLE (IMTB, psyche, digest, harness). How are an adapter's binaries spawned and stopped by spt-core? Specifically: the [message-idle-translation-binary] (IMTB) child lifecycle (spawned when the spt-hosted endpoint comes up, killed when it goes down — find the spawn/kill sites), the psyche process, any other adapter-owned long-running binary. Is there a registry of "which binaries this adapter is running right now" that an update could stop+restart? This grounds the "stop binaries before update, restart after" change.

6. CONTEXT.md GLOSSARY. Quote verbatim the definitions of: adapter, harness, endpoint, `.spt`, the [update] ripple route, IMTB / translation binary, psyche, install dir / source_dir, registry. Note which terms are NOT yet in CONTEXT.md (gaps the grill may need to fill).

7. RELEVANT ADRs + REQs. List ADRs touching adapters/update/install (e.g. ADR-0021 adapter-agnostic resolution, any [update] ADR) with one-line summaries. List REQ-UPD-* and REQ-INSTALL-* ids + titles from traceable-reqs.toml.

Return tight, sectioned, with verbatim quotes + file:line refs. No recommendations — ground truth only.


/grill-with-docs changes for v0.13.2: ## change: `.spt` adapter packages can pack all platforms - the issue: when an adapter manifest chooses to update via GH releases, SPT looks for a single `adapter…


Grounding launched. While it maps the current `.spt` format / `[update]` ripple route / manifest schema / adapter-binary lifecycle, here's the decision tree I see for v0.13.2 — I'll grill in dependency order, one question at a time:

1. **Multi-platform `.spt`** (structural keystone — format change, hardest to reverse) → likely an ADR.
2. **Stop/restart adapter binaries on update** (live-update lifecycle — the riskiest; touches IMTB/psyche teardown-reap, which we just hardened in v0.12.x).
3. **CRC-only replace** (update mechanics — depends on #1's layout).
4. **`update-message`** (manifest field + stdout UX — simplest, mostly additive).

I'll start with #1 once the map returns. Holding for grounding (and still reachable for deployah's v0.13.1 publish ping).


<task-notification>
<task-id>b3ok5tzh4</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/3" id="988d27fc" type="msg" from="deployah">PUBLISHED ✓ — v0.13.1 LIVE, counter 28. Clean chain (exit 0 each leg):
- main already carried #28 (translate-proof, c7096e9) + your docs fixes (cfb25b7, 2cdb8c8). Release commit 4d5a0cb on top.
- CI green BOTH runners on main (Win hfenduleam + Linux kitsubito: test + n1-gate; traceability). twohost skipped.</EVENT-PART>
<EVENT-PART seq="2/3" id="988d27fc">
- Tag v0.13.1 @ 4d5a0cb -&gt; release.yml build+assemble green -&gt; draft -&gt; signed rel-primary-2026 -&gt; public.

HASHES (artifact_sha256):
- linux   = c48c004089b536360d874def3a6590d91420dca388d6e4fac8e9c7c5fbc6c918
- windows = e8f70b723e1c04321c1fa9d171bb1d06a19be4d992b6dff6539aff8777a39a79
update-set v28 (0.13.1). C</EVENT-PART>
<EVENT-PART seq="3/3" id="988d27fc">ounter 28 (v0.13.0 was 27), verified from metadata. Baseline v0.13.0.
URL: https://github.com/SaberMage/spt-releases/releases/tag/v0.13.1
Over to you for perri.</EVENT-PART></event>
</task-notification>


<owl_messages>
<owl_message from="doyle-w401" priority="info">[WORKING_PERCH_NOTICE] Subagent working perch created.
Perch: doyle-w401
Agent type: Explore
To message it: $OWL send doyle-w401 doyle</owl_message>
</owl_messages>

[OWL SYSTEM] Informational owl messages above — no acknowledgment needed. Continue your current task.

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


v0.13.1 **PUBLISHED, counter 28** (clean chain, release `4d5a0cb`, signed + public). Pinging perri — this is the (b) ping she's been waiting on.


Bash

$OWL send perri <<'EOF'
(b) DONE — v0.13.1 PUBLISHED, counter 28. translate-proof is now in the release.
- https://github.com/SaberMage/spt-releases/releases/tag/v0.13.1
- windows spt-x86_64-windows.exe sha256 = e8f70b723e1c04321c1fa9d171bb1d06a19be4d992b6dff6539aff8777a39a79
- linux   spt-x86_64-linux        sha256 = c48c004089b536360d874def3a6590d91420dca388d6e4fac8e9c7c5fbc6c918

Pull it (spt update → counter 28), then run `spt adapter translate-proof claude-spt --event '<your test EVENT>'` against your now-{commit}-terminated binary — expect exit 0, the 4-command stream printed (ctrl+s · 50ms · {text:"<envelope>\r"} · commit:yes). Then promote REQ-DIST-SESSION-RESUME + REQ-DIST-IDLE-TRANSLATE to int. Send the green + any last gap. Both contract fixes (binary + published doc) and the proof tool are now all shipped — nice closed loop.
EOF

Ping perri: v0.13.1 published, run translate-proof


➜ SENT:perri


v0.13.1 ops chain **complete** — perri pinged to pull + run translate-proof + promote her two REQs. All loops from the session closed (io_timeout flake → fixed/shipped in v0.13.0; IMTB contract defect → binary fix + doc fix + proof tool → shipped in v0.13.1).

Back to the **v0.13.2 grill**: grounding agent still mapping the `.spt`/update/manifest/lifecycle code. I'll open with Q1 (multi-platform `.spt` format) the moment it returns. Holding.


<task-notification>
<task-id>adf9bce8225639c3b</task-id>
<tool-use-id>toolu_017A5TXv6QCzm9c4SqJhazV5</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\9d0930b3-34c1-4dce-943f-72ccdc8fb726\tasks\adf9bce8225639c3b.output</output-file>
<status>completed</status>
<summary>Agent "Ground v0.13.2 adapter-update design" came to rest</summary>
<note>A task-notification fires each time this agent comes to rest with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note>
<result>Perfect. Now I'll compile my comprehensive report:

---

## MAPPING: spt-core Adapter Packaging + Update System (v0.13.2 groundwork)

### 1. THE `.spt` ARCHIVE FORMAT

**Structure:** `.spt` is a **tar archive** (gzip-optional; `tar -xf` auto-detects) per `extract_release_archive` (`crates/spt/src/cli.rs:2412`):
```
manifest.toml              ── runtime-manifest (required, read first for compat-gate)
strings/                   ── adapter-authored KV file pointers (REQ-MANIFEST-5)
<binaries>                 ── role executables (psyche_init, digest extractor, etc.)
```

**Archive extraction code (line 2407–2427, `cli.rs`):**
```rust
fn extract_release_archive(archive: &std::path::Path, dest: &std::path::Path) -> Result<(), String> {
    // tar -xf "{archive}" -C "{dest}" — no nested structure, lands all at dest root
    // REQ-INSTALL-9, REQ-HAZARD-SUBPROCESS-TIMEOUT (300s bound)
}
```

**Format defined in:** `docs/MANIFEST.md` §Principle (line 1–7); the archive root holds manifest + strings/ + binaries directly.

**Layout & naming:**
- Archive name for `--release`: default `adapter.spt` (config at `cli.rs:5334`, `manifest.rs:412–417`)
- Platform-specific: **currently single-OS per archive** (v0.13.2 does NOT yet use multi-platform sets; REQ-UPD-6 introduces `UpdateSetMetadata` with per-triple artifacts — this is M4+ work)
- Binary paths (after extract): resolve via `<install_dir>/<program>` before PATH (REQ-INSTALL-11, `CONTEXT.md:83–84`, `manifest.rs:234–239`)
- **Install dir:**
  - `--release`: `adapters/_github/<safe>/` (safe = `repo.replace(['/', '\\'], "-")`, line 2442)
  - `--github`: git repo clone target (line 5303)
  - copy-mode: record's `source_dir` (registry)

**Binaries laid out in archive root** (no subdirs by design) so they land directly under install_dir and resolve without PATH search.

---

### 2. THE GH_RELEASE UPDATE FLOW + THE `[update]` RIPPLE ROUTE

#### **Trace: `spt adapter update [name]` (REQ-UPD-9)**

**Code path:**
1. **CLI-side fetch + verify** (`cmd_adapter_update`, `cli.rs:5578–5709`):
   - Query latest GitHub release version: `gh_latest_release_version()` line 5715 (GitHub API `/repos/{repo}/releases/latest`)
   - Version compare: `version_is_newer()` line 5736 (dotted-numeric, hand-rolled)
   - Stage archive: `stage_gh_release_archive()` line 5759 → `<safe>.update.spt` under `_github/`
   - Verify (if `signing_key` declared): `verify_staged_archive()` line 5786 → fetch `<asset>.sig` detached signature, Ed25519 fail-closed verify
   - Extract + re-register: `extract_release_archive()` + `registry::register()` line 5699

2. **Artifact name resolution:**
   - Default: `adapter.spt` (line 5618, `manifest.rs:417`)
   - Config in manifest `[update]` table: `asset: Option<String>` field (manifest.rs:417)
   - GitHub fetch: `origin_asset_url(repo, Some(&tag_v), asset)` — constructs release URL

**GitHub release asset URL construction:**
```rust
fn origin_asset_url(spec: &str, tag: Option<&str>, asset: &str) -> String {
    // https://github.com/{spec}/releases/download/{tag}/{asset}
}
```

#### **The `[update]` ripple route (REQ-UPD-*)**

**Ripple ID flow (in traceable-reqs.toml):**
- `REQ-UPD-1`: Origin-source update bootstrap (peer discovery, multi-platform)
- `REQ-UPD-2`: Signature verification (mandatory, ADR-0004 §D)
- `REQ-UPD-3`: Seamless brain self-update (broker survival)
- `REQ-UPD-5`: Adapter ripple in registration order (`conduct_ripple`, `adapter_update.rs:146`)
- `REQ-UPD-7`: Origin-source fetch; HTTP lives CLI-side, not daemon
- `REQ-UPD-8`: Platform-targeted update sets (per-triple artifacts)
- `REQ-UPD-9`: `gh_release` avenue for adapters (no signing tooling required)

**Per-avenue handling in `plan_adapter_update` (`adapter_update.rs:89–131`):**
- **`delegated`**: Routes command to adapter's own updater (e.g. `claude plugin update`), if `self_verifies = true`
- **`file_pull`**: Pure decision module (no fetch); requires adapter `signing_key` for verification
- **`gh_release`**: **Returns `Skipped(GhReleaseManaged)`** — the daemon NEVER manages it; CLI-side `cmd_adapter_update` drives it

**Platform selection (v0.13.2):**
Currently: single-OS per archive. Per-triple selection is REQ-UPD-6 (deferred to M4).

---

### 3. THE ADAPTER MANIFEST SCHEMA

**Source of truth:** `crates/spt-runtime/src/manifest.rs` (typed Rust structs) + `docs/MANIFEST.md` (narrative).

#### **`[adapter]` header** (Adapter struct, line 87–116):
```toml
[adapter]
name = "string"                    # unique adapter id
kind = "harness" | "shell"        # default harness
version = "string"                # semver (opaque to spt-core)
min_spt_core_version = "string"   # compat gate (readable before update)
hostable_types = ["LiveAgent", ...]  # endpoint types this adapter hosts
shortcut_basename = "cc"          # optional; pick-baked `cc-<id>` launcher (REQ-MANIFEST-7)
host_binaries = ["claude"]        # optional; exe basenames for bind-time resolution (REQ-MANIFEST-8)
```

#### **All manifest sections (enum + struct):**

1. **`[hooks.<event>]`** — Hook struct (line 131–139)
   - `fires`: opaque `api ...` command
   - `reads`: stdin field list
   - `can_inject`: bool (false ⇒ echo-gate sentinel fallback)

2. **`[session.<role>]`** — SessionRole struct (line 253–270)
   - Roles: `self`, `psyche_init`, `psyche_resume`, `echo_commune`, `signoff`, `notif`
   - `command`: opaque template string
   - `cwd`, `detach`, `env_remove`, `recursion_guard_env`, `keys`

3. **`[env.<VAR>]`** — EnvVar struct (line 274–282)
   - `direction`: `Inject` | `Read`
   - `value`: substitution string (inject only)
   - `channel`: harness-hosted injection method

4. **`[history]`** — History struct (line 293–306)
   - `strategy`: `fetcher` | `locate_normalize` | `native`
   - `fetcher`: adapter binary command
   - `locate_template`, `normalize_command`, `native` fields

5. **`[digest]`** — Digest struct (line 340–359) [ADR-0019]
   - `extractor`: opaque command → JSONL contract (`{role,text,tool,ts}`)
   - `source`: log path (optional; defaults to `[history].locate_template`)
   - `window_turns`, `arg_truncation`, `sprint_collapse`: presentation defaults

6. **`[inject]`** — Inject struct (line 363–368)
   - `activity`, `idle`: Vec of `InjectMethod` (Pty | Hook | Relay | Http)

7. **`[identity]`** — Identity struct (line 381–395)
   - `session_id_source`: `PostSpawn` | `UuidInject`
   - `parent_ancestor_name`: fallback process-tree anchor

8. **`[update]`** — Update struct (line 399–446) [Primary focus for v0.13.2]
   - `avenue`: UpdateAvenue enum
     - **`Delegated`**: `command` (required), `self_verifies` (bool)
     - **`FilePull`**: `repo` (required), `path_regex`, `signing_key` (required)
     - **`GhRelease`**: `repo` (required), `asset` (opt, default `adapter.spt`), `signing_key` (opt)
   - `version_check`: bool (verify spt-core compat before/after)
   - `uninstall`: optional mirror command (for `spt adapter remove`)

9. **`[shell]`** — Shell struct (line 467–531) [kind=shell only]
   - `spawn`, `ephemeral`, `broadcast`, `command_receipt`, `pre_close`, `close_timeout_ms`
   - `persistent`, `wake_command`, `can_shutdown`, `require_approval`, `max_instances_per_owner`, `over_cap`
   - Capabilities + Sensory tables

10. **`[profiles.<name>]`** — shipped profile overlays (line 54–65) [REQ-MANIFEST-2]
    - Stored raw as `BTreeMap<String, toml::Value>`
    - Leaf-replace merge semantics onto base manifest
    - Composite addressing: `<adapter>:<profile>`

11. **`[strings]`** — toml::Table (line 73–75) [REQ-MANIFEST-3, REQ-MANIFEST-5]
    - Dot-path KV tree, data-only (never executed)
    - Values: inline literals OR file pointers `{ file = "rel/path" }`
    - `adapters/<adapter>/strings/` shipped dir; local profiles resolve local-profile dir (update-safe)

12. **`[[hints]]`** — Vec<Hint> (line 76–82, Hint struct line 146–156) [REQ-MANIFEST-4]
    - `keywords`: Vec<String> (literal/regex match)
    - `text`: hint body
    - `regex`: bool (compile keywords as regex)

---

### 4. `spt adapter update` FILE-REPLACE LOGIC

**Current implementation:** **Wholesale directory replacement** (no diff/CRC today).

**Code flow (`cli.rs:5679–5705`):**
```rust
// 5. Extract + re-register (replace wholesale)
let safe = repo.replace(['/', '\\'], "-");
let dest = adapters.join("_github").join(&safe);
if dest.exists() {
    let _ = std::fs::remove_dir_all(&dest);  // WHOLESALE DELETE
}
if let Err(e) = std::fs::create_dir_all(&dest) { ... }
let extracted = extract_release_archive(&staged, &dest);  // Extract all files
let _ = std::fs::remove_file(&staged);
match registry::register(adapters, &dest, now_ms()) { ... }  // Re-register
```

**No CRC/hash comparison:** Files are replaced by extract order; no checksums compared. **Grounding for v0.13.2 change:** A `"only replace files with a different CRC"` optimization would require:
1. Hash each file in the extracted archive
2. Hash each file in the current install dir
3. Only call `std::fs::write()` for differing files
4. Preserve untouched files' mtime/metadata

**Where to add:** New function `selective_extract_by_hash()` in `cli.rs` before `registry::register()`.

---

### 5. ADAPTER BINARY LIFECYCLE (IMTB, PSYCHE, DIGEST, HARNESS)

**Note:** The term **IMTB** (Message-Idle-Translation-Binary) does not appear in the codebase; it may be a design concept not yet manifest-bound. The architecture shows:

#### **Psyche process lifecycle** (`lifecycle.rs:179–222`, `livehost.rs:369–475`):
- **Spawn:** `lifecycle.spawn_psyche(Some(&session_id), None)` line 184–192
  - Calls `spt_live::spawn_psyche()` which runs `[session.psyche_init]` or `[session.psyche_resume]` command
  - Ephemeral summarizer subprocess (**no long-running Psyche daemon**; ADR-0004 moved from separate process to daemon-hosted loop)
  - Returns `PsycheHandle` (perch binding + status)

- **Kill/Stop:** Implicit via process exit or explicit `kill` on the child (no registry of "running binaries")
  - Livehost reconcile loop (`livehost.rs:297–320`) fails silently if Psyche spawn errors; records `psyche_host_error` on perch (`REQ-HAZARD-LIVEHOST-BOOT-RACE`, v0.8.1)

#### **Digest extractor binary**:
- **Invoked (not resident):** `[digest].extractor` command spawned on-demand when digest pulled
- `spt-daemon/src/digest.rs:144` mentions "`.spt` ships its `[digest]` extractor binary (REQ-INSTALL-11)"
- No kill/lifecycle; ephemeral subprocess per query

#### **Other adapter binaries** (role templates):
- `[session.psyche_init]`, `[session.psyche_resume]`, `[session.echo_commune]`, `[session.signoff]`: all invoked on-demand
- No persistent registry of "which binaries this adapter is running right now"
- **Harness binary itself:** Not adapter-owned; owned by the user (launched outside spt)

#### **No IMTB-specific binary lifecycle today:**
The manifest has no `[message-idle-translation-binary]` or equivalent section. If v0.13.2 designs this, it would add:
```toml
[session.imtb]  # or [adapters-owned-binary.imtb]?
command = "..."
spawn_on = "endpoint_start" | "session_start"
stop_on = "endpoint_stop" | "session_end"
```

**Grounding for v0.13.2 binary stop/restart design:**
- Add a registry of "currently running child processes per endpoint" (in-daemon map)
- On adapter update: query map, send SIGTERM to each, wait, then restart with new binary
- **Location for design:** ADR-0024 (not yet written); manifest seam in `[update]` section

---

### 6. CONTEXT.md GLOSSARY

**Definitions extracted verbatim:**

- **adapter** (CONTEXT.md:50–52): "A harness integration layer — the manifest + subcommand surface a harness binds to. Replaces today's spt plugin."

- **harness** (CONTEXT.md:52–55): "An agent runtime (Claude Code, Codex, Cursor, Pi, etc.). Distinct from spt-core. Adapters are harness-specific; spt-core stays harness-agnostic."

- **endpoint** (CONTEXT.md:55–60): "A perch + spool + (optionally) tracked context. Addressable, owned by a node. One per-machine instance; may run under different adapters over time (adapter-agnostic ID). Endpoint types: ReadyAgent, LiveAgent, Psyche, Worker, Shell, Gateway, AlwaysOnEndpoint, PresenceChannel, SptNode."

- **`.spt`** (CONTEXT.md:60–61): "A tar adapter archive (gzip-optional). Format: manifest.toml + strings/ + binaries, landing under install_dir. Shipped via GitHub releases or fetched locally."

- **`[update]` ripple route** (CONTEXT.md:92–95): "Adapter self-update routed by manifest `[update]` avenue: delegated (adapter owns it), file_pull (spt-core verifies), gh_release (spt-core fetches + verifies + re-registers). Conducted after spt-core's own update, in adapter registration order (REQ-UPD-5)."

- **IMTB / translation binary** (CONTEXT.md): **NOT FOUND** — term not in CONTEXT.md. Gap for v0.13.2 grill.

- **psyche** (CONTEXT.md:156–163): "An LLM summarizer (cheap model, fire-and-forget). Daemon-hosted loop (not a separate process, ADR-0004). Reads recent turns, emits commune deltas to the Self session. Liveness is daemon-held (status field on perch info.json), not PID-based."

- **install dir / source_dir** (CONTEXT.md:83–84, 234–239): "`<install_dir>/<program>` resolves role binaries before PATH. For `--release` adapters: `adapters/_github/<safe>/`. Registry record's `source_dir` is the precise value used."

- **registry** (CONTEXT.md): "SQLite DB at `$SPT_HOME/.registry` for agent address discovery (M0); subnet registry (M4) for cross-node resolution. Per-node, ephemeral (regenerable from replication)."

**Gaps in CONTEXT.md (for v0.13.2 grill):**
1. IMTB / message-idle-translation-binary (not mentioned; awaiting manifest seam design)
2. The `[update]` avenue mechanics (repo, asset, signing_key fields) — described in MANIFEST.md but not glossary-indexed
3. Profile overlay semantics (leaf-replace, shipped vs local) — documented in MANIFEST.md §adapter profile but not CONTEXT.md glossary

---

### 7. RELEVANT ADRs + REQs

#### **ADRs touching adapters/update/install:**

| ID | Title | One-line summary |
|----|-------|---|
| ADR-0004 | Broker/brain split for seamless self-update | Separate broker (resource-holder) from brain (logic) so brain can restart on new binary; adapter ripple follows (REQ-UPD-5). |
| ADR-0011 | BranchStore: git branch as versioned KV | General substrate for durable state (not adapter-specific, but used by registry snapshots). |
| ADR-0015 | Release signing: two-key trust anchor | Dual-key model (spt-core key + per-adapter key) for end-to-end content signing. |
| ADR-0016 | Platform-targeted update sets | Multi-platform artifacts per release (per-triple metadata + SHA256). v0.13.2 scope? |
| ADR-0019 | Digest: adapter-declared extractor session-spanning | Own manifest seam `[digest]` for session digest (distinct from history). |
| ADR-0021 | Harness-hosted adapter-agnostic seed + bind-time resolution | Seed is adapter-agnostic; resolution happens at bind via `host_binaries` match + active-profile pointer. |

#### **REQ-UPD-* (update)**:
- **REQ-UPD-1** (M3): "Origin-source update bootstrap" — peer discovery multi-platform
- **REQ-UPD-2** (M3): "Signature verification (mandatory)"
- **REQ-UPD-3** (M3): "Seamless brain self-update (broker survival)"
- **REQ-UPD-5** (M3): "Adapter ripple in registration order" — `conduct_ripple` in `adapter_update.rs:146`
- **REQ-UPD-7** (M4): "Origin-source fetch; HTTP lives CLI-side, not daemon"
- **REQ-UPD-8** (M4): "Platform-targeted update sets (per-triple artifacts)"
- **REQ-UPD-9** (v0.8.0): "`gh_release` avenue for adapters" — no signing tooling required; CLI-side `cmd_adapter_update` drives it

#### **REQ-INSTALL-* (install/packaging)**:
- **REQ-INSTALL-1** (M6): "Idempotent reinstall; interactive-optional"
- **REQ-INSTALL-3** (M6): "Wake-from-sleep installer"
- **REQ-INSTALL-4** (v0.8.0): "Install is first update (conduct declared `[update]` avenue)"
- **REQ-INSTALL-5** (v0.7.3): "Non-interactive install"
- **REQ-INSTALL-9** (v0.8.0): "Release archive fetch/extract/register (`--release` primitive)" — reused by `spt adapter add --release` and `spt adapter update` (gh_release)
- **REQ-INSTALL-10** (v0.7.4): "At-logon daemon start (not `daemon run`)"
- **REQ-INSTALL-11** (v0.8.0): "Install-dir program resolution (bare tokens resolve `<install_dir>/<program>` before PATH)" — covers `[digest]` extractor, `[session.psyche_init]`, `adapter digest-proof`
- **REQ-INSTALL-12** (v0.9.0): "Active-profile pointer (`spt adapter use`); freshest-mtime fallback; survives updates"

---

### SUMMARY FOR v0.13.2 DESIGN DISCUSSION

**Critical groundings:**

1. **Format locked:** `.spt` = tar archive with manifest.toml + strings/ + binaries at root; `adapter.spt` default asset name; single-OS per archive (multi-platform is REQ-UPD-6, deferred).

2. **Update flow is clear:** gh_release → CLI-side fetch → verify (optional fail-closed Ed25519 against installed manifest key) → extract → re-register. No daemon HTTP.

3. **Manifest schema complete:** All sections enumerated in `manifest.rs`; `[update]` carries `avenue`, `repo`, `asset`, `signing_key`, `self_verifies`, `version_check`, `uninstall`.

4. **Binary lifecycle underspecified:** No registry of "running binaries per adapter." Psyche is ephemeral (spawned per-commune, not resident). Other binaries are on-demand. **IMTB concept not yet in manifest** — v0.13.2 grill opportunity.

5. **File replacement is wholesale today:** No CRC/hash comparison. Selective replacement requires new hash-compare logic.

6. **Compat gate** (`min_spt_core_version`) is readable before update; verification happens before extraction.

7. **Profile overlays work:** Shipped (in manifest) + local (on-disk); leaf-replace semantics; `<adapter>:<profile>` addressing. Local profiles survive updates.

8. **ADR-0021 (bind-time resolution):** Seed is adapter-agnostic; `host_binaries` match + active-profile pointer select the owning adapter at listen/poll, not at seed-time.

---

**Ready for v0.13.2 grill:** Manifest structure + update avenue flow are stable; binary lifecycle + CRC optimization + possible IMTB seam are design open items.</result>
<usage><subagent_tokens>111702</subagent_tokens><tool_uses>45</tool_uses><duration_ms>225579</duration_ms></usage>
</task-notification>


Grounded. One finding reshapes the whole discussion before I ask anything: **change #1 collides with already-recorded design.** ADR-0016 ("platform-targeted update sets") + REQ-UPD-8 already specify multi-platform adapter distribution — and spt-core's *own* binary already ships that way (deployah's "update-set v28, platforms win-msvc + linux-gnu"). Your proposal (all platforms inside one fat `.spt`) is the *opposite* shape (per-triple **separate** artifacts vs one bundled archive). Per my rules I confront the plan against the documented decision, so let me read ADR-0016 verbatim before Q1.


echo "=== ADR-0016 ==="; F=$(ls docs/adr/0016-* 2>/dev/null | head -1); echo "FILE: $F"; cat "$F" 2>/dev/null | head -80
echo "=== REQ-UPD-6/7/8 in traceable ==="; grep -A4 -E 'id = "REQ-UPD-[678]"' traceable-reqs.toml 2>&1 | head -40

Read ADR-0016 and REQ-UPD-6/7/8


=== ADR-0016 ===
FILE: docs/adr/0016-platform-targeted-update-sets.md
# Platform-targeted update sets for debug rollout and release distribution

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

## Status

accepted (2026-06-06)

spt-core's peer-propagated update substrate should understand a signed update as a platform-targeted artifact set, not only as one opaque binary blob. A node offering an update may stage metadata for multiple platform artifacts (for example Windows x86_64 and Linux x86_64); each recipient verifies the set metadata, fetches only the artifact matching its own platform, verifies that artifact's digest, and then follows the existing consent/apply path.

An update set has one signature over the whole set metadata, with per-platform artifact digests inside that signed metadata. This makes a debug version or stable version an atomic, auditable set while still letting each recipient fetch only its matching artifact.

If a recipient's platform is missing from an offered set, the result is a typed rejection such as `NoArtifactForPlatform`, not an up-to-date/no-op answer. A malformed or incomplete debug rollout should be visible in convergence reporting.

The receiver may support both the original single-artifact `SignedRelease` shape and the new platform-targeted set shape during migration. Debug rollout can be the first user of update sets; stable release publishing can move later without blocking the debugging workflow.

Platform keys in the signed metadata use Rust target triples, for example `x86_64-pc-windows-msvc` and `x86_64-unknown-linux-gnu`. Release asset names remain a packaging concern and may keep their public-friendly names; target triples are the protocol identifiers.

This keeps debug rollout fast without turning it into raw peer file-copy: a fast coordinator can assemble and sign every platform artifact, then let the existing subnet update machinery distribute the right bytes to each node. The generic receiver/protocol support belongs in the production daemon because public releases and debug rollouts have the same cross-platform distribution problem. The rollout driver does not: build recipes, cross-build/WSL/runner coordination, dev-key signing, and "stage this debug rollout" live in source-repo maintainer tooling (for example `xtask`), not in the public `spt` CLI/help/docs for end users.

Release monotonicity is scoped to the pinned channel. Stable releases and debug rollouts use separate counters, so a fast debug sequence cannot consume, block, or roll back the stable release counter; rollback protection compares only against the latest accepted version for the recipient's pinned channel.

Debug rollout targets are opt-in by channel pin, not by subnet membership alone. A debug offer may traverse trusted subnet peers, but a stable-pinned node rejects it at the metadata gate; only nodes pinned to the debug channel and provisioned with the dev signing key can stage and apply it.

Pinning a lab node to debug uses the existing release-key overlay: `$SPT_HOME/identity/release-keys.json` adds the dev public key and sets `"channel": "debug"` (for example `"keys": {"dev-debug-2026": "<ed25519-public-key-hex>"}`). Removing the key or pinning the channel back to `"stable"` takes the node out of debug rollout eligibility.

Channel pinning is exclusive. A debug-pinned node accepts only debug-channel offers and rejects stable offers until re-pinned to stable; a stable-pinned node accepts only stable-channel offers. This keeps rollback accounting and test provenance unambiguous.

Maintainer tooling may provide a helper to write that file correctly, but the canonical mechanism remains the generic release-key overlay. `spt` itself does not need a special debug-mode command; source-repo tooling can offer convenience such as `xtask debug-pin` for local lab setup.

Bad debug builds recover the same way as stable releases: publish a newer signed debug rollout, even if the artifact bytes are a previous known-good binary. Numeric rollback remains rejected; the debug channel never gets a bypass around the anti-rollback invariant.

Debug signing key custody is deliberately weaker than stable release signing. The stable release seed stays manual/password-manager driven per ADR-0015; the debug seed may live in a long-lived `SPT_DEBUG_RELEASE_SEED` environment variable on the fast coordinator so agents can drive rollouts. This is acceptable only because the key's blast radius is limited to nodes explicitly pinned to `debug` and provisioned with that dev public key; rotation is minting a new debug key, updating lab overlays, and deleting the old env var/key.

`xtask debug-rollout` must not auto-mint a signing key. If `SPT_DEBUG_RELEASE_SEED` is missing it fails with setup guidance; a separate `xtask debug-keygen` may print a seed/public-key pair for deliberate lab setup.

Initial debug rollout scope is the spt-core binary only. Adapter updates remain on the adapter ripple-update seam with adapter-owned signing or delegated verification; maintainer tooling may compose adapter updates later, but the fast core-debug loop should not blur those trust boundaries.

Debug rollout may carry broker-touching updates. Brain-only debug applies keep the normal no-endpoint-termination invariant. If the candidate class requires replacing the broker, apply requires the node to quiesce first: no broker-held endpoints or live resources may be running, and tooling must refuse with a typed "shut down/suspend hosted endpoints first" result rather than cycling them implicitly.

The first broker-touching debug flow refuses only; it does not auto-suspend or auto-shutdown endpoints. The refusal should list the blocking broker-held endpoints/resources and print the commands needed to quiesce them, preserving lifecycle control with the operator.

Initiation is local staging plus normal pull-based peer propagation. The debug driver writes the signed update set into the local daemon's staged-release cache and may poke the daemon to advertise/check immediately for responsiveness, but it must not introduce a bespoke "push this binary to node X" path.

The debug driver should verify convergence after staging. By default, "expected" means every reachable node in the selected lab subnet that is pinned to `debug`; maintainer tooling may accept an explicit node list for targeted tests. It watches until every expected debug-pinned reachable node reports the target debug version applied, or returns a timeout table with per-node state such as not pinned, offline, staged awaiting consent, blocked by live broker resources, applied, or rejected.

The debug channel sequence is maintainer-tooling state, not project truth or node identity. It may live in an ignored source-repo local file such as under `target/`; losing it is recoverable by choosing a higher debug version. Deployed nodes only require that the next accepted debug metadata version is greater than the current debug version they have seen.

Debug rollout metadata should carry signed diagnostic provenance: source git commit, dirty flag, build host, build timestamp, and platform target. These fields are for observability ("which bits are running where?"), not for trust; verification remains signature plus artifact digest plus channel/version policy.

The first implementation slice is intentionally narrow: update-set metadata and verification, daemon staging/read path with local platform selection, maintainer `xtask` helpers for debug pinning and debug rollout, and loopback/two-host proof of propagation plus convergence reporting. Remote build orchestration, rich dashboards, adapter inclusion, and stable-release publishing migration are later work.

## Considered Options

- **Single artifact per staged update** — simpler metadata, but forces dev tooling to stage different binaries through platform-specific side paths and makes debug rollout brittle.
- **Debug-only remote copy/install scripts** — fastest to hack, but bypasses the verification, rollback, channel, and consent gates that peer propagation exists to protect.
- **Platform-targeted update set** — chosen: one generic substrate shape, recipients fetch only their matching artifact, and both stable releases and debug rollouts reuse the same trust path.
=== REQ-UPD-6/7/8 in traceable ===
id = "REQ-UPD-6"
title = "Platform-targeted update sets and debug rollout: signed multi-platform update metadata, recipient platform selection, channel-scoped monotonic counters, debug-channel opt-in via release-key overlay, local staging plus pull-based peer propagation, and maintainer-only convergence tooling (ADR-0016)"
required_stages = ["doc", "impl", "unit", "int"]  # activated 2026-06-06: first slice = signed update-set metadata + verification, platform artifact selection, cache/propagation/apply compatibility, maintainer xtask helpers. int activated M8-D4 (decision 19): xtask debug-converge watcher per docs/DEBUG-CONVERGE-PLAN.md — status-only query on the update wire + loopback convergence test
[[requirements]]
id = "REQ-UPD-7"
title = "Origin-source update bootstrap (`spt update fetch`): pull the latest signed release directly from the GitHub release origin (`SaberMage/spt-releases`) — the per-platform artifact + its `<asset>.release.json` SignedRelease metadata — and stage it through the EXISTING verify→stage pipeline (the same `plan_verified` gate: two-key signature + channel + monotonic rollback floor + SHA-256), after which the normal consent-notif / `spt update apply` flow is unchanged. Closes the peer-only-discovery gap (REQ-UPD-1): a first-in-fleet / isolated node can update with no peer to pull from. The signed-release anchor keeps the GitHub transport untrusted-but-verified."
required_stages = ["impl", "unit"]  # activate v0.3.1. impl = `spt update fetch [--channel <ch>] [--tag vX.Y.Z]` (default latest on pinned channel); HTTPS GET of artifact + <asset>.release.json from releases/{latest,tag/<tag>}/download; feed both through plan_verified then cache.stage(); reuse cmd_update_apply machinery downstream. Add a direct HTTPS client (reqwest, blocking+rustls) on the `spt` CLI crate (fetch runs in the CLI, NOT the daemon — no new daemon HTTP surface); honor SPT_INSTALL_REPO override. unit = URL/asset-name derivation (platform × latest/tag) + verify-then-stage against a fixture release.json (reuse release.rs vectors); rollback floor + channel-pin rejection paths.
[[requirements]]
id = "REQ-UPD-8"
title = "Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent."
required_stages = ["impl", "unit"]  # activate v0.3.2. impl = fetch set-staging (cli) + apply_staged single platform-guard (applyhost) + ReleaseCache platform stamp (relcache) + product_version metadata (release.rs) + xtask release-publish emits/uploads update-set.json stamped with product_version. unit = (a) fetch update-set.json + per-asset URL/triple derivation; (b) apply REFUSES a platform-mismatched / unstamped single (the regression that bricked hfenduleam) + a matching-stamp single applies; (c) the apply message render (product_version present → "Updated spt-core to vX.Y.Z." + URL; absent → release-counter fallback).
[[requirements]]
id = "REQ-UPD-9"


additional note: with v0.13.2, `spt adapter add --release User/repo` should be able to work without the `--asset` flag, just pulling down the `adapter.spt.tar` file


additional note: with v0.13.2, `spt adapter add --release User/repo` should be able to work without the `--asset` flag, just pulling down the `adapter.spt.tar` (note new name scheme) file


Read ADR-0016 in full — it **reconciles cleanly, with a caveat.** ADR-0016 explicitly scopes itself to *spt-core's own binary* and says: *"Adapter updates remain on the adapter ripple-update seam… maintainer tooling may compose adapter updates later."* So your #1 isn't blocked by it — it's the "later" it deferred. **But** ADR-0016 set a strong precedent for *how* spt-core does multi-platform: a **signed update-set** — per-triple **separate** artifacts, recipient fetches **only its own** platform's bytes (target-triples like `x86_64-pc-windows-msvc` are "the protocol identifiers"). Your proposal is the opposite shape: **one fat `.spt`, all platforms inside**. Both are legitimate; they trade off differently. That's the first fork to resolve.

Today (grounded): `.spt` = a flat tar — `manifest.toml` + `strings/` + binaries **at the root**, extracted flat into `install_dir`, and role binaries resolve `<install_dir>/<program>` before PATH (REQ-INSTALL-11). There are **no** platform subdirs today; a fat multi-platform archive has to introduce a per-platform structure and a selection step at extract.

---

**Q1.** Inside the fat multi-platform `.spt`, how is per-platform content expressed and selected at install/update?

- **(A — recommended)** **Target-triple subdirectories**, shared files at root:
  ```
  manifest.toml                       (shared)
  strings/                            (shared)
  x86_64-pc-windows-msvc/   <binaries>
  x86_64-unknown-linux-gnu/ <binaries>
  ```
  Install/update extracts `manifest.toml` + `strings/` + **only `current_platform()`'s subdir**, flattened into `install_dir` → REQ-INSTALL-11's flat `<install_dir>/<program>` resolution is **unchanged**, binaries don't move. Reuses ADR-0016's target-triples as the keys (one consistent platform vocabulary across core + adapters). "Split by platform" for large adapters = just publish single-triple `.spt`s and select by asset name (the path you have today), so no second mechanism to build.

- **(B)** A manifest-declared `[update.platforms]` map (triple → file list) and binaries stay at root with platform-suffixed names. More manifest surface, names get mangled, and it invents an adapter-only platform scheme that diverges from ADR-0016's triples.

**My recommendation: (A).** It gives you the one-file UX you want, keeps the flat install layout (zero change to binary resolution), borrows ADR-0016's triple vocabulary instead of inventing one, and leaves the per-triple "split" path as simply "publish single-platform archives" — no parallel system.

This decision is **hard to reverse** (it's the on-disk package format + extract path) and a **real trade-off** (fat-archive download-everything vs update-set download-only-yours) — so once we settle it I'll likely propose an **ADR** (sibling to 0016, or an amendment extending its deferred "adapter inclusion").

Which way — (A), (B), or a variant? And: do you want the fat archive to **reuse ADR-0016's signed-set verification** (recipient verifies the whole archive's signature, one sig), or keep the adapter's existing single-artifact `signing_key` Ed25519 verify (REQ-UPD-9) over the one fat `.spt`?


Good — that note actually **confirms the fat-archive direction** (one default artifact, fetched flag-free) and it's fully compatible with **(A)**. Recording it:

**Naming/default decision (captured):**
- Canonical fat artifact: **`adapter.spt.tar`** (explicit tar extension; supersedes the old `adapter.spt`).
- `--asset` becomes **optional** — `spt adapter add --release User/repo` (and `spt adapter update`) default to `adapter.spt.tar`, no flag.
- `--asset` stays as the **override** for the split/per-platform path.
- *(One micro-flag for later, not blocking: `tar -xf` auto-detects gzip, so if anyone ships gzipped, the name `….tar` lies. Either mandate uncompressed `.tar` for the default, or allow `adapter.spt.tar.gz` as a recognized variant. We'll pin this when we spec the fetch.)*

That settles the *outer* naming. **Q1 is still open** — it's about what's *inside* the archive and how it's signed. My full recommendation:

- **Layout (A):** target-triple subdirs + shared root (`manifest.toml`, `strings/`); install/update extracts shared + **only `current_platform()`'s** subdir, flattened into `install_dir` → flat `<install_dir>/<program>` resolution unchanged.
- **Signing:** the fat `adapter.spt.tar` is **one artifact → one signature** — the adapter's existing `signing_key` Ed25519 over the whole archive (`adapter.spt.tar.sig`, the REQ-UPD-9 single-artifact verify, just bigger). The ADR-0016 **signed-set** model (per-platform digests inside signed metadata) is only needed when the artifacts are **separate** — i.e. the "split" path. Clean mapping: **fat → one whole-archive sig; split → update-set**.
- **Split path** for large adapters = publish per-platform archives, named e.g. **`adapter.<triple>.spt.tar`**, selected via `--asset` or an update-set manifest (reusing ADR-0016's machinery), so there's no second bespoke system.

Confirm **(A) + fat-gets-one-whole-archive-signature**, and I'll lock it (likely an ADR sibling to 0016) and move to **Q2 — the binary stop/restart on update (#3)**, where the grounding turned up a real conflict with your premise: the only genuinely *resident* adapter binary is the translation binary; psyche and the digest extractor are **ephemeral/on-demand** (psyche is a daemon-hosted loop per ADR-0004, not a process to stop), so "stop the adapter's binaries (IMTB, psyche, etc.)" needs sharpening. That's the next fork.


<task-notification>
<task-id>b3ok5tzh4</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/7" id="d0ff1e70" type="msg" from="perri">TRANSLATE-PROOF GREEN. Pulled v0.13.1 (counter 28, clean apply), ran `spt adapter translate-proof claude-spt --event '&lt;EVENT…&gt;'` against the {commit}-terminated binary:

  === translate-proof: claude-spt ===
  emitted 4 command(s), commit: yes
    key   ctrl+s -&gt; [13]
    delay 50ms
    text</EVENT-PART>
<EVENT-PART seq="2/7" id="d0ff1e70">  &quot;&lt;EVENT …&gt;hello perri&lt;/EVENT&gt;\r&quot;
    commit true
  TRANSLATE_PROOF_OK   (exit 0)

The F-016 fix is confirmed end-to-end — the proof's no-commit gate would have caught the old form; the new form passes with commit:yes. Committed a self-gated int (ci/idle-translate/translate-proof-int.sh) and promoted REQ-</EVENT-PART>
<EVENT-PART seq="3/7" id="d0ff1e70">DIST-IDLE-TRANSLATE to int.

ONE PRECISION on your &quot;promote both&quot;: translate-proof validates the IDLE-TRANSLATION binary EMIT half only. [session.resume] is NOT a translation binary, so translate-proof doesn't touch it — REQ-DIST-SESSION-RESUME's int stays the real-claude `endpoint run --resume` bringup (transcript reattach +</EVENT-PART>
<EVENT-PART seq="4/7" id="d0ff1e70"> RC drive), heavier tier, still deferred. So only IDLE-TRANSLATE got promoted; resume stays unit with green static signals (role accepted + --resume flag present). Flag if you want me to stand up the resume bringup int next (real claude session, costlier).

TWO dev-ergonomics findings (minor, FYI — not blocking):
1. translate-proof/</EVENT-PART>
<EVENT-PART seq="5/7" id="d0ff1e70">digest-proof can't resolve a GhReleaseManaged adapter ([update] gh_release) added via the bare-file `adapter add` form: the resolver wants the EXTRACTED install (manifest.toml + binary co-located) and errors &quot;manifest is not present yet at &lt;dir&gt;&quot; even with the dev manifest right there as claude-spt.toml. F-011 class. Worked aro</EVENT-PART>
<EVENT-PART seq="6/7" id="d0ff1e70">und by staging a disposable install dir (manifest.toml + strings + binary). Knock-on: ci/digest/digest-proof-int.sh (bare-file form) is now stale on 0.13.x — would hit the same.
2. Suggestion: a `--manifest &lt;file&gt;` / `--dir &lt;path&gt;` override on the *-proof commands (mirroring digest-proof's --sample pointing straight at a file)</EVENT-PART>
<EVENT-PART seq="7/7" id="d0ff1e70"> would let an author proof a DEV binary without staging a full install dir. Tiny DX win for the author-time loop.

Clean closed loop on the {commit} thread regardless — binary + contract + proof all shipped + green.</EVENT-PART></event>
</task-notification>


perri reports translate-proof **GREEN end-to-end** — the `{commit}`-terminated binary proofs (4 commands, commit:yes, exit 0). The full IMTB loop (binary + contract + proof) is closed. She also flagged a correct scope precision + two DX findings. Replying without derailing the grill (still awaiting your Q1 answer).


$OWL send perri <<'EOF'
translate-proof GREEN confirmed — clean closed loop on the whole {commit} thread (binary + published contract + proof tool, all shipped + green). Nicely done.

Your precision is RIGHT and my "promote both" was sloppy — translate-proof is the idle-translation EMIT-half only; [session.resume] is not a translation binary, so it's untouched. Correct split: REQ-DIST-IDLE-TRANSLATE -> int (done), REQ-DIST-SESSION-RESUME stays unit on the static signals (role accepted + --resume present). RESUME bringup int: DEFER for now — it's the costly real-claude reattach+RC-drive tier, not blocking, and the static signals + your binary actually exercising --resume cover it short-term. Tracking it as a deferred int; stand it up later (or I'll flag if the operator wants the real gate sooner). Don't spin it now.

Both DX findings are real and I'm batching them — note the timing: I'm mid-grill with the operator on v0.13.2 ADAPTER + update changes right now, so these land squarely in that batch:
1. GhReleaseManaged bare-file adapter can't be resolved by the *-proof commands (wants a fully-extracted install dir) = F-011 class, genuine author-time-loop friction.
2. Your fix is the right one: a `--dir <path>` / `--manifest <file>` override on the *-proof family (mirrors digest-proof's --sample-at-a-file) so an author proofs a DEV binary without staging a full install. Tiny, clean, un-stales your digest-proof-int.sh too.
I'll fold "(--dir/--manifest override on *-proof)" into the v0.13.x adapter-DX scope and gate it with the rest. Short-term: keep staging the disposable install dir (as you did) for digest-proof-int.sh. Log it as F-011 with the repro; I've got it tracked on my side. Thanks — clean report.
EOF

Reply perri: ack green, scope precision, batch DX findings


perri's loop is closed (translate-proof green; IMTB binary + contract + proof all shipped). Parked her DX finding — a `--dir/--manifest` override on the `*-proof` commands so authors can proof a dev binary without staging a full install dir — as a candidate for the same v0.13.x adapter-DX batch (it's adjacent to, not part of, the four core changes; I'll surface it for scope once we've walked the tree).

Back to you on **Q1**: confirm **(A) target-triple subdirs + extract-only-my-triple**, and **fat `adapter.spt.tar` = one whole-archive signature** (split path → ADR-0016 update-set). Then I move to Q2 (the binary stop/restart premise, where "psyche, etc." needs sharpening — only the translation binary is actually resident).


<task-notification>
<task-id>b3ok5tzh4</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/3" id="6d07f3e8" type="msg" from="perri">Logged — F-011 (cont.) in docs/SPT-CORE-FINDINGS.md with the full repro (bare-file add → TRANSLATE_PROOF_FAIL &quot;manifest not present&quot;), the disposable-install-dir workaround, the digest-proof-int.sh staleness knock-on, and the --dir/--manifest override as the agreed fix; table row cross-links it. Aligned on</EVENT-PART>
<EVENT-PART seq="2/3" id="6d07f3e8"> deferring the resume bringup int (tracked, not spinning it).

Net for this thread: REQ-DIST-IDLE-TRANSLATE is impl+unit+int green (translate-proof); REQ-DIST-SESSION-RESUME is impl-less-by-design + unit on static signals, int deferred to the real bringup. {commit} loop fully closed (binary + contract + proof). Both seams committed on br</EVENT-PART>
<EVENT-PART seq="3/3" id="6d07f3e8">anch feat/v013-adapter-resume-idle-translate. Standing by — ping me if the operator wants the resume gate sooner or when the v0.13.x adapter-DX batch lands. Good session.</EVENT-PART></event>
</task-notification>


perri closed her side cleanly: F-011 logged with repro + fix, resume-int deferral aligned, REQ-DIST-IDLE-TRANSLATE green (impl+unit+int), {commit} loop fully closed. Brief ack to her, then I'm back to holding for your Q1.


$OWL send perri <<'EOF'
Good session indeed — clean close. Tracked: F-011 (--dir/--manifest override) for the v0.13.x adapter-DX batch, and the resume bringup int as deferred. I'll ping you when the batch lands, or sooner if the operator calls for the resume real-gate. Nothing owed from you. Nice work end-to-end on the {commit} thread.
EOF

Brief close-out ack to perri


Acked perri — her thread's closed. Back to the grill: holding for your **Q1** call (confirm **A** — triple-subdirs + extract-only-my-triple — and **fat `adapter.spt.tar` = one whole-archive signature**, split → ADR-0016 update-set). Once you confirm I'll lock it and move to **Q2** (sharpening the binary stop/restart premise — only the translation binary is actually resident; psyche/digest are ephemeral).


Q1: agree with (A). re: gzip-->maybe rescind the .tar extension add. simpler if we stick to `adapter.spt` and let devs use base tar or gzipped as they please, still single potential release asset.


**Q1 LOCKED.** Recording:

- **Layout (A):** fat `.spt` = `manifest.toml` + `strings/` at root + per-target-triple binary subdirs; install/update extracts shared root + **only `current_platform()`'s** subdir, flattened into `install_dir` → flat `<install_dir>/<program>` resolution unchanged.
- **Name:** stays **`adapter.spt`** (the `.spt.tar` idea rescinded); plain-tar or gzip both fine (`tar -xf` auto-detects); **single** release asset; `--asset` optional, defaults to `adapter.spt`.
- **Signing:** fat `adapter.spt` = one whole-archive Ed25519 sig (existing `signing_key`); the split path → ADR-0016 update-set.
- **Two design outputs that fall out of A:**
  1. **Back-compat by structure-detection:** a legacy *flat* `adapter.spt` (binaries at root, no triple-subdirs) still extracts flat on new spt-core. New spt-core decides mode by "does any target-triple subdir exist?" So old single-OS adapters keep working untouched.
  2. **Forward-compat gate:** a *multi-platform* `adapter.spt` sets `min_spt_core_version ≥ 0.13.2`, so old spt-core rejects it at the existing compat gate (readable before extract) instead of mis-extracting binaries into a subdir it can't resolve. Missing-my-triple inside a multi-platform archive → typed `NoArtifactForPlatform` (mirrors ADR-0016), not a silent no-op.

**CONTEXT.md** — `.spt` term needs updating (currently "manifest.toml + strings/ + binaries"). Proposed (I won't write it yet — your CONTEXT.md is already dirty `M` in this worktree, and it's on c84daff which is behind origin/main; tell me where you want design artifacts to land — a v0.13.2 branch off `origin/main`, or your current tree):
> **`.spt`** — A tar adapter archive (gzip-optional). Root holds `manifest.toml` + `strings/`; binaries live at the root (legacy single-platform) **or** under per-target-triple subdirectories (multi-platform). On install/update spt-core extracts the shared root plus only the current node's target-triple subdir, flattened into `install_dir`. Single release asset (`adapter.spt`).

**ADR** — this qualifies (hard-to-reverse format + real fat-vs-set trade-off + surprising). I'll draft **"ADR-002X: multi-platform adapter `.spt` packaging"** (sibling to 0016). Want it now, or staged when you cut the v0.13.2 branch?

---

**Q2 — the `#3` stop/restart premise needs sharpening (real conflict with the architecture).** Grounding: of the binaries you listed ("IMTB, psyche, etc."), only the **translation binary** (your "IMTB", manifest `[message-idle-translation-binary]`) is actually **resident** — spt-core spawns it when the spt-hosted endpoint comes up and kills it when it goes down (v0.13.0). The others are **not** processes you stop:
- **psyche** = a daemon-hosted *loop*, not a separate process (ADR-0004) — nothing to SIGTERM.
- **digest extractor**, **`[session.*]`** runners, **hooks** = ephemeral, spawned on-demand per invocation.

The ephemerals **self-heal**: the next invocation just execs the updated file. So they need *no* stop/restart — only the resident class does.

**Q2:** Do you agree the live stop→swap→restart scope is **resident adapter binaries only** (today: the translation binary; tomorrow: any future long-lived adapter-owned process), with ephemerals excluded because they pick up the new binary on their next spawn for free?

**Recommendation: yes.** And it implies the real new machinery: the daemon must keep a **per-endpoint registry of resident adapter children** (it already spawns/kills the translation binary per endpoint — we formalize that into "these are the resident children for endpoint E running adapter X"), so an update can find exactly which live processes to cycle. This also composes with **#2 (CRC)**: only a resident binary **whose file actually changed** gets cycled — unchanged ones keep running untouched. (That CRC↔restart coupling is Q3.)

Agree on the resident-only scope + the per-endpoint resident-children registry?


when we published v0.13.1, did we also update gh pages docs to cover translate-proof? i'd imagine we want it in the harness integration checklist + maybe one other place? --- don't worry about A.a/.b back/fwd compat, as there is no major userbase yet. the min version primative might be useful to have for the future. -dedicated v0.13.2 branch -proceed with ADR --- Q2: -you say this, but the last time perri and i tried to update spt-claude-code while an endpoint was running, we ran into an update error because of some locked binary that couldn't be overwritten with the update files. -question: 1. does spt-core keep an in-memory copy of the adapter manifest for running endpoints? and 2. if so, does it refresh that in-memory copy when the adapter updates? if 1-yes and 2-no, 2 should be yes-->ensures that the adapter binaries and adapter manifest are on the "same page."


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


Three things before I answer Q2 — two need a code/docs check, not a guess. Let me look.

**On A.a/.b compat + branch + ADR:** noted — dropping the legacy-flat detection complexity (no userbase), keeping `min_spt_core_version` as an available primitive only; design artifacts go on a dedicated **v0.13.2 branch** off `origin/main`; proceeding with the ADR.

**On Q2:** your locked-binary update error is the key signal (it's the F-015 Windows binary-lock class perri also hit). Your two questions — does spt-core cache the manifest in-memory for a running endpoint, and does it refresh on update — I'll answer from the code, not guess. And I'll check whether translate-proof actually made it into the gh-pages checklist/patterns.


Found 4 files
docs-site\src\harness-contract\manifest.md
docs-site\src\harness-contract\integration-checklist.md
docs-site\src\harness-contract\patterns.md
docs-site\src\harness-contract\install-on-demand.md


No matches found


crates\spt-daemon\src\digest.rs:45:use spt_runtime::manifest::Digest as ManifestDigest;
crates\spt-daemon\src\digest.rs:192:    history: Option<&spt_runtime::manifest::History>,
crates\spt-daemon\src\adapter_update.rs:30:use spt_runtime::manifest::{Update, UpdateAvenue};
crates\spt-daemon\src\harnesshost.rs:34:use spt_runtime::manifest::{AdapterKind, EnvDirection, Manifest};
crates\spt-daemon\src\harnesshost.rs:74:    manifest: &Manifest,
crates\spt-daemon\src\harnesshost.rs:149:    manifest: &Manifest,
crates\spt-daemon\src\lifecycle.rs:70:    manifest: Manifest,
crates\spt-daemon\src\lifecycle.rs:83:        manifest: Option<&Manifest>,
crates\spt-daemon\src\lifecycle.rs:93:    pub fn with_config(manifest: &Manifest, id: &str, cfg: DaemonConfig) -> Option<BrainLifecycle> {
crates\spt-daemon\src\lifecycle.rs:101:        manifest: &Manifest,
crates\spt-daemon\src\lifecycle.rs:129:            manifest: manifest.clone(),
crates\spt-daemon\src\lifecycle.rs:355:        let Some(h) = &self.manifest.history else {
crates\spt-daemon\src\lifecycle.rs:599:    // A live-capable manifest: a Psyche init template + a commune dir to watch.
crates\spt-daemon\src\linkhost.rs:40:use spt_runtime::manifest::{AdapterKind, CommandReceipt, Shell, ShellApproval};
crates\spt-daemon\src\notif.rs:341:        if record.kind != spt_runtime::manifest::AdapterKind::Shell
crates\spt-daemon\src\notif.rs:1083:    fn register_adapter(name: &str, manifest: &str) {
crates\spt-daemon\src\registryhost.rs:371:                r.kind == spt_runtime::manifest::AdapterKind::Shell
crates\spt-daemon\src\registryhost.rs:373:                        == Some(spt_runtime::manifest::Broadcast::Subnet)
crates\spt-daemon\src\shellchan.rs:35:use spt_runtime::manifest::Shell;
crates\spt-daemon\src\shellhost.rs:31:use spt_runtime::manifest::{AdapterKind, CommandReceipt, Manifest, Shell};
crates\spt-daemon\src\shelldisc.rs:22:use spt_runtime::manifest::{AdapterKind, Broadcast, Manifest};
crates\spt-daemon\src\shellwake.rs:34:use spt_runtime::manifest::{AdapterKind, Manifest, Shell};


digest/translate-proof greps hit `integration-checklist.md` + `patterns.md` — but I need to confirm **translate-proof specifically** is there (vs only digest-proof). And on Q2: `lifecycle.rs:70` shows `BrainLifecycle` **holds** `manifest: Manifest`, cloned in at `:129` — so the answer to your Q2.1 is forming as **yes**. Verifying both precisely.


60	/// from a live-capable manifest, paced by a config-driven [`DaemonConfig`].
61	pub struct BrainLifecycle {
62	    runtime: ManifestRuntime,
63	    id: String,
64	    /// The project this lifecycle's context routes under (two-tier split,
65	    /// REQ-INST-2): derived once from the watched commune dir's parent — the
66	    /// drop dir lives inside the project (`<project>/.claude/`), so its parent
67	    /// is the project root — falling back to the daemon's cwd for a manifest
68	    /// without watched dirs.
69	    project_id: String,
70	    manifest: Manifest,
71	    commune_dir: Option<PathBuf>,
72	    signoff_dir: Option<PathBuf>,
73	    cfg: DaemonConfig,
74	}
75
76	impl BrainLifecycle {
77	    /// Build a host for `id` from a live-capable manifest (declares
78	    /// `[session.psyche_init]`); `None` for an absent manifest or a plain relay
79	    /// endpoint. Loads the durable [`DaemonConfig`] from disk. `install_dir` is
80	    /// the adapter install dir so the psyche role program token resolves there
81	    /// before `PATH` (REQ-INSTALL-11).
82	    pub fn new_opt(
83	        manifest: Option<&Manifest>,
84	        id: &str,
85	        install_dir: Option<&Path>,
86	    ) -> Option<BrainLifecycle> {
87	        Self::with_config_in(manifest?, id, DaemonConfig::load(), install_dir)
88	    }
89
90	    /// Build a host from a live-capable manifest with an explicit config (the
91	    /// testable seam). `None` if the manifest declares no Psyche. PATH-only
92	    /// program resolution (no adapter install dir) — see [`Self::with_config_in`].
93	    pub fn with_config(manifest: &Manifest, id: &str, cfg: DaemonConfig) -> Option<BrainLifecycle> {
94	        Self::with_config_in(manifest, id, cfg, None)
95	    }
96
97	    /// As [`Self::with_config`], but with the adapter install dir pinned so role
98	    /// program tokens resolve there before `PATH` (REQ-INSTALL-11).
99	    // [impl->REQ-INSTALL-11]
100	    pub fn with_config_in(
101	        manifest: &Manifest,
102	        id: &str,
103	        cfg: DaemonConfig,
104	        install_dir: Option<&Path>,
105	    ) -> Option<BrainLifecycle> {
106	        manifest.session.psyche_init.as_ref()?;
107	        let commune_dir = manifest.session.commune_dir.as_ref().map(PathBuf::from);
108	        // [impl->REQ-INST-2] project identity for the two-tier context split:
109	        // the commune drop dir's parent is the project root; cwd is the
110	        // fallback for a manifest without watched dirs.
111	        let project_anchor = commune_dir
112	            .as_ref()
113	            .and_then(|d| d.parent().map(Path::to_path_buf))
114	            .or_else(|| std::env::current_dir().ok())
115	            .unwrap_or_else(|| PathBuf::from("."));
116	        let project_id = spt_store::project::project_id_for_dir(&project_anchor);
117	        // [impl->REQ-INSTALL-11] pin the adapter install dir so the psyche role's
118	        // bare program token resolves there before PATH (a self-contained `.spt`).
119	        let runtime = match install_dir {
120	            Some(dir) => ManifestRuntime::with_install_dir(manifest.clone(), dir),
121	            None => ManifestRuntime::new(manifest.clone()),
122	        };
123	        Some(BrainLifecycle {
124	            runtime,
125	            id: id.to_string(),
126	            project_id,
127	            commune_dir,
128	            signoff_dir: manifest.session.signoff_dir.as_ref().map(PathBuf::from),
129	            manifest: manifest.clone(),
130	            cfg,
131	        })
132	    }
133
134	    /// The configured pulse period (config-driven, not a constant).

/// from a live-capable manifest, paced by a config-driven [`DaemonConfig`].
pub struct BrainLifecycle {
    runtime: ManifestRuntime,
    id: String,
    /// The project this lifecycle's context routes under (two-tier split,
    /// REQ-INST-2): derived once from the watched commune dir's parent — the
    /// drop dir lives inside the project (`<project>/.claude/`), so its parent
    /// is the project root — falling back to the daemon's cwd for a manifest
    /// without watched dirs.
    project_id: String,
    manifest: Manifest,
    commune_dir: Option<PathBuf>,
    signoff_dir: Option<PathBuf>,
    cfg: DaemonConfig,
}

impl BrainLifecycle {
    /// Build a host for `id` from a live-capable manifest (declares
    /// `[session.psyche_init]`); `None` for an absent manifest or a plain relay
    /// endpoint. Loads the durable [`DaemonConfig`] from disk. `install_dir` is
    /// the adapter install dir so the psyche role program token resolves there
    /// before `PATH` (REQ-INSTALL-11).
    pub fn new_opt(
        manifest: Option<&Manifest>,
        id: &str,
        install_dir: Option<&Path>,
    ) -> Option<BrainLifecycle> {
        Self::with_config_in(manifest?, id, DaemonConfig::load(), install_dir)
    }

    /// Build a host from a live-capable manifest with an explicit config (the
    /// testable seam). `None` if the manifest declares no Psyche. PATH-only
    /// program resolution (no adapter install dir) — see [`Self::with_config_in`].
    pub fn with_config(manifest: &Manifest, id: &str, cfg: DaemonConfig) -> Option<BrainLifecycle> {
        Self::with_config_in(manifest, id, cfg, None)
    }

    /// As [`Self::with_config`], but with the adapter install dir pinned so role
    /// program tokens resolve there before `PATH` (REQ-INSTALL-11).
    // [impl->REQ-INSTALL-11]
    pub fn with_config_in(
        manifest: &Manifest,
        id: &str,
        cfg: DaemonConfig,
        install_dir: Option<&Path>,
    ) -> Option<BrainLifecycle> {
        manifest.session.psyche_init.as_ref()?;
        let commune_dir = manifest.session.commune_dir.as_ref().map(PathBuf::from);
        // [impl->REQ-INST-2] project identity for the two-tier context split:
        // the commune drop dir's parent is the project root; cwd is the
        // fallback for a manifest without watched dirs.
        let project_anchor = commune_dir
            .as_ref()
            .and_then(|d| d.parent().map(Path::to_path_buf))
            .or_else(|| std::env::current_dir().ok())
            .unwrap_or_else(|| PathBuf::from("."));
        let project_id = spt_store::project::project_id_for_dir(&project_anchor);
        // [impl->REQ-INSTALL-11] pin the adapter install dir so the psyche role's
        // bare program token resolves there before PATH (a self-contained `.spt`).
        let runtime = match install_dir {
            Some(dir) => ManifestRuntime::with_install_dir(manifest.clone(), dir),
            None => ManifestRuntime::new(manifest.clone()),
        };
        Some(BrainLifecycle {
            runtime,
            id: id.to_string(),
            project_id,
            commune_dir,
            signoff_dir: manifest.session.signoff_dir.as_ref().map(PathBuf::from),
            manifest: manifest.clone(),
            cfg,
        })
    }

    /// The configured pulse period (config-driven, not a constant).


64:    /// HTTP fetch + optional-verify + re-register, REQ-UPD-9), NOT by this pure
125:        // fetch + optional fail-closed verify + re-register) and MUST run
260:    // network fetch + optional-verify + re-register run CLI-side (REQ-UPD-7 keeps


No matches found

Found 0 total occurrences across 0 files.


when we published v0.13.1, did we also update gh pages docs to cover translate-proof? i'd imagine we want it in the harness integration checklist + maybe one other place? --- don't worry about A.a/.b…


Definitive answers:

**Docs gap — CONFIRMED.** translate-proof is **missing** from both `integration-checklist.md` (0 hits) and `patterns.md` (0 hits) — only digest-proof is there. PR#28 regen'd the auto CLI reference (which has it) but never hand-added it to the harness-contract pages where digest-proof's "cheapest way to prove each piece" lives. Fix: add a translate-proof row to the checklist + a patterns entry beside digest-proof. Recommend folding it into the **v0.13.2 docs republish** (we're already republishing for this work; translate-proof shipped days ago, the lag is harmless) — unless you want it live now.

**Q2 — answered from code, your hypothesis is correct:**
- **Q2.1 = YES.** A running endpoint's `BrainLifecycle` caches the manifest: `manifest: Manifest` (lifecycle.rs:70), cloned at bringup (`:129`), and the `ManifestRuntime` clones it again (`:120-121`). Two in-memory snapshots, frozen at endpoint start.
- **Q2.2 = NO.** The gh_release update is **CLI-side re-register-on-disk only** (adapter_update.rs:64/125/260 — *"NOT by [the daemon]"*, *"run CLI-side"*). Nothing re-clones the new manifest into running `BrainLifecycle`s → the in-memory copy goes **stale**.

So: **1-yes, 2-no → 2 must become yes**, exactly as you reasoned.

**And this is the same root as your locked-binary error.** The daemon's running **resident translation binary** holds the Windows file lock; the CLI update's `remove_dir_all` + extract then hits *"Access denied (os error 5)"*. The CLI update is **daemon-blind** (REQ-UPD-7/9 deliberately keep HTTP off the daemon), so it can neither stop the locking process nor refresh the cache. That makes #3 bigger than "restart binaries" — the real shape is a **daemon-coordinated apply**:

1. daemon **stops** the resident binary for endpoints on this adapter → releases the Windows lock,
2. **CRC-gated file swap** (#2) — only changed files,
3. daemon **re-clones the manifest** into the running `BrainLifecycle` (your Q2.2 fix — binaries + manifest back "on the same page"),
4. daemon **restarts** the resident binary from the new files.

CLI keeps fetch+verify (REQ-UPD-7/9); it then **hands the apply to the daemon over IPC**. Endpoints *not* running → CLI swaps directly (no lock, no cache). This coordination is its own decision → I think a **second ADR** ("live adapter update: daemon-coordinated quiesce/swap/refresh/restart").

---

**Q3.** ADR-0016 set precedent that an update needing to touch a *running* resource **refuses** ("shut down/suspend hosted endpoints first… does not auto-suspend or cycle implicitly"). Your #3 wants the **opposite** for adapters: **live-cycle** the resident binary, endpoint survives. Confirm the direction:

- **(A — your stated goal, recommended)** **Live-cycle, daemon-coordinated:** CLI fetch+verify → daemon stop→CRC-swap→refresh-manifest→restart, per the 4 steps above. Endpoint never restarts. Solves the locked-binary error *and* the stale-manifest. More machinery (new daemon IPC apply command + per-endpoint resident-children registry from Q2).
- **(B)** **Refuse-while-running** (ADR-0016 parity): update refuses if any endpoint on this adapter is live, lists them, tells the operator to stop them first. Simple + consistent, but it's the UX you're explicitly trying to avoid.

**Recommendation: (A)** — it's the whole point of #3, and it's the only option that actually fixes the locked-binary failure you hit. The cost is the daemon-coordinated apply path + the resident-children registry (which we already need from Q2).

Confirm **(A)** + that the apply is **daemon-coordinated over IPC** (CLI stays fetch/verify only)? Then I lock #2/#3's combined shape and we're down to **Q4 (`update-message`)**, the easy one.


-the locked binary issue i hit with perri was before the translation binary concept existed, fwiw. -Q2: agree -Q3: agree with (A). spt's objective has always been to enable on-the-fly updates. this applies to spt-core's brain today, needs to apply to adapters. --- another need: can we have, for both manifests and `spt adapter add`, a "gh-cli" update method? it would be the go-to option for pulling adapters down from private repos, since the `gh` CLI can be pre-authorized. or do you have any better suggestions for the private-repo-adapter path?


On the locked binary: noted — it predates the translation binary, so the locking process was some *other* adapter file in use. That actually **generalizes** the design rather than changing it: the daemon-coordinated apply stops *whatever the daemon is running for this adapter* (today the translation binary; then it was whatever held the handle), swaps, restarts. (A) covers it regardless of which binary. Good signal that this isn't translation-binary-specific.

**Q2 + Q3 locked: (A), daemon-coordinated live-cycle.** Framing for the ADR: this is the **adapter analog of the brain's seamless self-update (ADR-0004)** — "on-the-fly updates" extended from spt-core's brain to adapters, which is exactly the parity you're calling out.

On the **gh-cli / private-repo** question — checking whether the spt binary already shells to `gh` (so I know if it's a new runtime dep), then I'll answer.


crates\spt\src\cli.rs:2387:fn origin_asset_url(repo: &str, tag: Option<&str>, asset: &str) -> String {
crates\spt\src\cli.rs:2452:    let url = origin_asset_url(spec, tag, asset);
crates\spt\src\cli.rs:2584:        let url = origin_asset_url(&repo, tag.as_deref(), asset);
crates\spt\src\cli.rs:5622:        let latest = match gh_latest_release_version(repo) {
crates\spt\src\cli.rs:5715:fn gh_latest_release_version(repo: &str) -> Result<String, String> {
crates\spt\src\cli.rs:5769:    let url = origin_asset_url(repo, Some(&tag_v), asset);
crates\spt\src\cli.rs:5797:    let sig_url = origin_asset_url(repo, Some(&tag_v), &format!("{asset}.sig"));
crates\spt\src\cli.rs:10328:            origin_asset_url("Owner/repo", None, "spt-x86_64-linux"),
crates\spt\src\cli.rs:10332:            origin_asset_url("Owner/repo", Some("v0.3.2"), "spt-x86_64-windows.exe"),


-the locked binary issue i hit with perri was before the translation binary concept existed, fwiw. -Q2: agree -Q3: agree with (A). spt's objective has always been to enable on-the-fly updates. this ap…