EXIT=0
  [OK] REQ-ENDPOINT-UNBOUND-ATTACH  required: [doc]  stages: +doc -impl -unit -int
  [OK] REQ-RUN-MULTISUBNET-HOME  required: [doc]  stages: +doc -impl -unit -int
       A pointer-mode (delegated / GhReleaseManaged) adapter whose binary/manifest is not yet extracted is reported with a CLEAR diagnostic, never silently dropped. Today such an adapter reads its manifest LIVE from source_dir (registry.rs manifest_dir ~146/149); a deferred / un-extracted install makes load_manifest fail → registered() (~410, filter_map(.ok())) SILENTLY DROPS the row → downstream ADAPTER_UNRESOLVED + a cryptic os-error-2 on `spt adapter use`. FIX: surface a clear diagnostic at the resolver + at `adapter use` (name the adapter + the deferred/missing-manifest cause + the fix), not a silent filter-drop and not a bare os-error-2; consider an eager manifest copy at register time so host_binaries survive before the binary download completes. doyle Finding A. (post-v0.10.0)
       The brain's daemon-hosted Psyche lifecycle surfaces a host-FAILURE on the live perch (harness-diagnosable) and runs net-INDEPENDENTLY. When reconcile_once→host_one→spawn_psyche fails for a state=live_agent+status=online endpoint (e.g. the adapter's psyche binary absent from its install dir, REQ-INSTALL-11), the failure MUST be written to the perch info.json as a CURRENT-STATE field (reason + ts + attempt count; overwritten each 5s retry, CLEARED on successful host) and surfaced by `spt endpoint list`/status — never left as an eprintln on the brain's invisible stderr where a harness reading only perch state is blind. status=online stays authoritative (agent reachable; only the Psyche is missing — brain-restart rehydrate legitimately has online-without-Psyche windows), so this is a SEPARATE psyche-host-health field, never a status de-stamp. Net-independence is a locked-in invariant: spawn_live_host (brainproc.rs:230) reaches the reconcile and hosts the Psyche on a net-less/unpaired/peer-pump-STALLED node, proven by a REAL detached-daemon E2E (real broker→brain-child, real api seed+listen, real install-dir psyche binary). spt-core SURFACES the failure; the adapter owns fixing its packaging.
       A daemon-hosted Psyche that spawns then EXITS IMMEDIATELY is a host failure, surfaced like a spawn failure (closes the v0.8.1 residual masking): the REQ-HAZARD-LIVEHOST-BOOT-RACE signal stamps `psyche_host_error` only when `spawn_psyche` returns Err, NOT when the detached spawn() returns Ok but the child dies within moments (e.g. a bad-argv child exiting 2 — the F-009 case). That leaves the residual 'online + no Psyche + no cause' gap: the nested `{id}-psyche` info.json is written status=online with a real-but-DEAD pid and the PARENT perch carries NO psyche_host_error (perri's F-010: tasklist showed 0 host procs across the window while info.json read online). The host MUST confirm RESIDENCY — a hosted child not alive (or whose `{id}-psyche` perch never re-registers / has a dead pid) within N seconds of spawn is treated as a host failure: stamp the parent perch `psyche_host_error{reason:"host not resident within <n>s (psyche perch missing/dead pid)"}` (and do not leave a phantom online nested perch). Closes the last masking gap the v0.8.1 fix left open. perri's F-010 (v0.8.1 dogfood). Sibling of REQ-HAZARD-LIVEHOST-BOOT-RACE.
       Command-template substitution fills argv ELEMENTS, not a re-tokenized string: spt-core currently `fill_template`s {key} values INTO the command STRING and THEN `tokenize`s the filled string (runtime.rs:94/122), so a multi-word {key} value whitespace-SPLITS into multiple argv tokens unless the adapter hand-quotes the placeholder, and a value containing a `"` (or `;`) injects/breaks tokenization (shell-injection-adjacent). A filled value MUST become exactly ONE argv element regardless of spaces/quotes in the value. Fix: tokenize the TEMPLATE into argv FIRST, then `fill_template` EACH token, so a `{key}` slot resolves to a single element and the value never participates in tokenization (no whitespace-split, no quote/semicolon injection); preserve the missing-key / empty-command errors and `{{`/`}}` non-interpretation. perri's F-009 (v0.8.1 dogfood, argv-capture-confirmed): a multi-word `{psyche_prompt}` = "PSYCHE REVIVAL time: epoch-ms:… incoming event: (none)" arrived as argv[6..12] (7 stray tokens), the harness runner strict-parsed `--prompt` against the 2nd word, exited 2 within ~1s → phantom hosted perch. Applies to EVERY [session.<role>] template (psyche_init, extractor, notif, …); digest survives today only because its fills ({session_id}/{source}) are single-token.
       File-backed adapter [strings] (M12-W3-T3.1): a [strings] dot-path value MAY be an inline-table FILE POINTER `key = { file = "rel/path" }` resolved to the file's contents at get-string time, keeping large bodies (skill-instructions, hint text) out of the manifest. A value-position table with a `file` key IS the pointer form (reserved — cannot double as data). Per-adapter aux storage `adapters/<adapter>/strings/`; pointers resolve relative to it with CONTAINMENT (reject `..`/absolute escaping the dir). UPDATE-SAFETY: a LOCAL profile's file-pointers resolve relative to the user-owned local-profile dir (NOT adapter-shipped strings/, which adapter updates overwrite), or the local profile inlines. Validate-at-register (fail-fast on a bad/escaping/missing pointer) + LAZY read at get-string (live file edits reflect, no re-register) + skip-diagnostics on missing-at-read (no hard-crash, mirrors [digest]). Rides the same leaf-replace profile overlay as the rest of [strings].
       An inbound `spt send` is DELIVERED to an spt-hosted endpoint (brought up via `spt endpoint run` → `api bind`, broker holds its PTY, NO `api listen` relay). Today cmd_bind→establish_perch (api/startup.rs ~441) writes info.json + ready marker + controllable=Some(true) but registers NO message-listener / NO address, so deliver.rs resolve_address→None→spool (deliver.rs:132-140) and the message NEVER reaches the live PTY — the endpoint reads 'online' (ready marker) yet `spt send` silently SPOOLS ('online but not deliverable' lie). Per CONTEXT:187-188 the daemon owns the PTY and delivers, manifest-configurable per activity-state (direct PTY injection / relay / HTTP). FIX: route an inbound send for an spt-hosted target through the daemon → broker InputReq → session.write_input PTY-inject (broker.rs dispatch_input/write_input ~988-1022), the same path the brain uses; the live-delivery handshake must report Sent (not Queued) and stop the spool-only fallback for a broker-hosted, PTY-resident endpoint. Detection is local: controllable==Some(true) + spt-hosted state + resolve_address==None. = the spt-core HALF of the wall-b finding (perri owns the adapter half: bind-hook fired-zero-perch + the missing endpoint-run int test). (post-v0.10.0)
       Resuming an endpoint session that HAS conversation history brings up a BLANK session. ROOT (doyle, code-grounded + CONTEXT — case-3 spt-core MISSING feature, NOT a perri docs-miss): CONTEXT L127-129 already defines the resume-session seam ('continue-existing: resume an existing harness session under the adapter — its NATIVE resume'), and the manifest already has the resume-variant pattern (Session has BOTH psyche_init AND psyche_resume, manifest.rs:217-219) — but the agent's own session has ONLY self_ (`[session.self]`, no resume sibling). cmd_endpoint_run (cli.rs:1304) re-passes the session_id through `[session.self]` on resume (resume.unwrap_or_else(mint_session_id)), so the adapter's FRESH command (e.g. `claude --session-id ..`) runs again instead of the harness NATIVE resume (`claude -r ..`) -> CC starts a fresh transcript -> blank. spt-core forwards session_id + cwd faithfully; it just has no way to express the native-resume invocation. SECOND GAP: CC resolves a transcript by session_id + cwd, but the session ledger records only {ts, session_id, trigger} (no cwd), so picker Resume-from-history (cross-project rows) can't restore the right cwd. FIX (doyle design, V0.13.0-P2-SESSION-RESUME-DESIGN.md, mirrors psyche_init->psyche_resume exactly): (A) add a `[session.resume]` role (resume: Option<SessionRole> on Session + roles()/is_empty()); cmd_endpoint_run selects it when --resume is set AND it's declared (fill {id}/{session_id}=resumed id/{session_name} + the resume cwd), else FALL BACK to `[session.self]` (full back-compat). (B) record cwd PER ledger row (operator ruling): {ts, session_id, trigger, cwd} additive serde-default; resume cwd = resumed row cwd -> else perch info.cwd -> else current_dir (back-compat for old rows + single-project endpoints); picker threads the selected row's cwd through Outcome::Run -> cmd_endpoint_run. (C) public docs (MANIFEST + harness-contract) teach `[session.resume]` so perri builds the adapter side BLIND. Adapter follow-on (perri, AFTER spt-core ships+docs): declare `[session.resume] command = claude -r {session_id} --remote-control {id} --dangerously-skip-permissions` from the resume cwd. Completes REQ-READY-AGENT-RESUME / REQ-RUN-PICKER resume-from-history. (v0.13.0)


Traceable EXIT 0, both new REQs `+doc` (the grep's "MISSING/error" hits are title prose, not FAIL verdicts). Commit + push the design branch, return to the v0.13.2 branch:


Bash

git add docs/adr/0026-multi-subnet-home-at-endpoint-run-creation.md docs/adr/0027-unbound-endpoint-state-and-attach-on-session.md CONTEXT.md traceable-reqs.toml
git commit -q -m "$(cat <<'EOF'
docs: ADR-0026/0027 endpoint-creation-flow milestone (operator grill)

Grilled-with-docs capture of the endpoint-run creation-flow redesign
(separate from v0.13.2; a dedicated milestone). Two ADRs + CONTEXT term
+ REQ registration (doc-active via the ADRs, impl/unit/int deferred):

- ADR-0026 REQ-RUN-MULTISUBNET-HOME: resolve home at the skeleton-create
  step, pre-create the skeleton perch with it so bind inherits via the
  immutable prior-branch (no hook/env change); single-auto /
  multi-non-interactive-refuse / multi-interactive-confirm-MRU / --subnet
  override; two-level MRU lists. Fixes perri's LATENT multi-subnet bringup
  gap (not a regression). Home stays immutable (ADR-0010).
- ADR-0027 REQ-ENDPOINT-UNBOUND-ATTACH: gate attach on broker-session-
  exists (not perch-online); new STATUS_UNBOUND (attachable, not message-
  addressable); EpDisplay Unbound = hollow (+ hollow-controlled).
- CONTEXT: "Unbound endpoint" glossary term.

Co-authored by: doyle
EOF
)" && git push -u origin design/endpoint-creation-flow 2>&1 | tail -4
echo "=== back to v0.13.2 branch ==="; git checkout v0.13.2-adapter-packaging 2>&1 | tail -2

Commit + push design branch, return to v0.13.2


warning: in the working copy of 'docs/adr/0026-multi-subnet-home-at-endpoint-run-creation.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/adr/0027-unbound-endpoint-state-and-attach-on-session.md', LF will be replaced by CRLF the next time Git touches it
remote:
branch 'design/endpoint-creation-flow' set up to track 'origin/design/endpoint-creation-flow'.
To https://github.com/SaberMage/spt-core.git
 * [new branch]      design/endpoint-creation-flow -> design/endpoint-creation-flow
=== back to v0.13.2 branch ===
Your branch is up to date with 'origin/v0.13.2-adapter-packaging'.
Switched to branch 'v0.13.2-adapter-packaging'


Design branch pushed, back on v0.13.2. Reframe task #8 (now a captured milestone, not a fast-follow patch):


TaskUpdate

Endpoint-creation-flow milestone (multi-subnet home + Unbound state) — DESIGN CAPTURED

GRILL COMPLETE (operator, 2026-06-22). Design captured on branch design/endpoint-creation-flow (pushed): ADR-0026 + ADR-0027 + CONTEXT "Unbound endpoint" term + 2 REQs registered (doc-active, impl/unit/int deferred per rule 5). traceable EXIT 0. Scoped as ONE milestone (operator chose all-together over a split), to land AFTER v0.13.2.

REQ-RUN-MULTISUBNET-HOME (ADR-0026): endpoint run resolves home at the skeleton-create step + pre-creates the skeleton perch with it → bind inherits via establish_perch immutable prior-branch (no hook/env change). single-auto / multi-non-interactive-refuse (IsTerminal) / multi-interactive-confirm-MRU ("Ok to proceed? Y/n") / --subnet override+validate. MRU = two-level move-to-front LISTs (per-project + node-global fallback). Home immutable (ADR-0010). Fixes perri's LATENT multi-subnet bringup gap (NOT a regression — HOME_REFUSED ≥0.11.0, exposed by 1→2 subnet crossing). perri repro = regression test, MUST run multi-subnet.

REQ-ENDPOINT-UNBOUND-ATTACH (ADR-0027): gate attach on broker-session-exists (not perch-online); new on-disk STATUS_UNBOUND (spawn→unbound, bind→online, death→offline; lifecycle reuses exit-waiter/reconcile); unbound = attachable (rc + endpoint-run attach; headless too; clear a bind-gating prompt) but NOT message-addressable. EpDisplay Unbound = hollow + hollow-controlled (amber=HarnessOnly is taken = opposite semantics).

IMPL anchors (for the build): cmd_endpoint_run cli.rs:1384 (no skeleton today) + await_endpoint_online cli.rs:1434; establish_perch/stamp_creation_fields (api/startup.rs:183 / home.rs:122 prior-branch); assign_home home.rs:71; EpDisplay picker/model.rs:55; broker sessions map (ADR-0025 W3a). NEXT: mint impl/unit/int stages + dispatch todlando when the milestone starts (after v0.13.2 ships). perri validates multi-subnet on publish.


Updated task #8 subject, description


**Grill complete — design fully captured.** On branch `design/endpoint-creation-flow` (pushed): ADR-0026 (multi-subnet home @ creation), ADR-0027 (Unbound state + attach-on-session), CONTEXT "Unbound endpoint" term, 2 REQs registered doc-active, traceable EXIT 0. Scoped as one post-v0.13.2 milestone (your call). perri's repro is its regression test; she validates multi-subnet on publish.

**Back to driving v0.13.2 to release:**

| Step | State |
|------|-------|
| W1 / W2 | ✅ |
| **W3 keystone (all sub-waves)** | ✅ **GATE-PASS** — ship-blocker cleared (both no-mocks ints pass, ADR-0025 accepted+amended) |
| W4 update-message | 🔨 building (todlando) |
| W5 proof `--dir` | 🔨 queued after W4 |
| Release (deployah) | blocked on W4/W5 + final CI green both runners |

perri locked in as validator for both the v0.13.2 psyche-lock fix (real claude-spt re-run on publish) and the future creation-flow milestone.

Waiting on todlando's W4/W5 gate-packets. On W4+W5 gate-pass + CI green both runners, deployah cuts v0.13.2 (counter verified vs spt-releases metadata at cut). Reachable on `doyle`.


in v0.13.2, please add: -`spt adapter version <adapter name>` as an alias for `spt adapter get-string <adapter name> version` -a docs "semi-mandate"/strong encouragement for adapter devs to populate this string and keep it up to date (esp. in the integration checklist doc)


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


New v0.13.2 ask: `spt adapter version <name>` alias + a docs semi-mandate. Grounding `get-string` resolution first (does `version` read `[adapter].version` or a `[strings]` key?):


Bash

echo "=== get-string command + resolution ==="; grep -rn "get-string\|GetString\|fn cmd_adapter_get_string\|get_string" crates/spt/src/cli.rs crates/spt-runtime/src/ 2>/dev/null | head -15
echo "=== does get-string / strings resolve [adapter].version? ==="; grep -rn "fn get_string\|adapter.version\|\"version\"\|reserved.*key\|strings" crates/spt-runtime/src/manifest.rs | head -15

Ground get-string + version resolution


=== get-string command + resolution ===
crates/spt/src/cli.rs:575:    GetString {
crates/spt/src/cli.rs:5974:        AdapterCmd::GetString { option, key } => match registry::get_string(&adapters, &option, &key)
crates/spt/src/cli.rs:9639:    // [unit->REQ-MANIFEST-3] the string verbs: get-string reads the merged view
crates/spt/src/cli.rs:9654:            cmd_adapter(AdapterCmd::GetString { option: opt.into(), key: key.into() })
crates/spt/src/cli.rs:9679:            spt_runtime::registry::get_string(&adapters, "claude-spt:work", "base")
crates/spt-runtime/src/manifest.rs:88:    /// behind registration). Dot-path-readable via `spt adapter get-string`, and
crates/spt-runtime/src/profile.rs:100:/// `spt adapter get-string` after the profile overlay has merged the tree.
crates/spt-runtime/src/profile.rs:119:/// resolved to the file's contents lazily at `get-string` time, never here.
crates/spt-runtime/src/registry.rs:80:    /// `delete-profile`/`get-string` named a local profile that has no file.
crates/spt-runtime/src/registry.rs:194:/// Read a file-pointer's contents at `get-string` time (REQ-MANIFEST-5). The
crates/spt-runtime/src/registry.rs:573:/// `spt adapter get-string <adapter-option> <key.path>` resolver. Resolves the
crates/spt-runtime/src/registry.rs:578:pub fn get_string(
crates/spt-runtime/src/registry.rs:1082:    // [unit->REQ-MANIFEST-3] get-string resolves through the overlay (a local
crates/spt-runtime/src/registry.rs:1099:            get_string(&adapters, "mock-shell", "base").unwrap().and_then(|v| v.as_str().map(str::to_string)),
crates/spt-runtime/src/registry.rs:1102:        assert!(get_string(&adapters, "mock-shell", "absent").unwrap().is_none());
=== does get-string / strings resolve [adapter].version? ===
4://! **opaque strings** spt-core never parses (MANIFEST §Principle). This module
86:    /// `[strings]` — an adapter-authored KV tree of **opaque data** (spt-core
90:    /// (a shipped or local profile may override base strings). Node-local; no
91:    /// cross-node sync. (CONTEXT.md §adapter strings.)
95:    pub strings: toml::Table,
167:    /// Keywords that fire the hint — literal **case-insensitive substrings** by
173:    /// Treat `keywords` as regex patterns instead of literal substrings.
538:    /// installed adapter version and, when newer, fetch the release `.spt`
799:         templates are opaque strings spt-core never parses. Cross-field \
834:        if self.adapter.version.trim().is_empty() {


in v0.13.2, please add: -`spt adapter version <adapter name>` as an alias for `spt adapter get-string <adapter name> version` -a docs "semi-mandate"/strong encouragement for adapter devs to populate t…


573	/// `spt adapter get-string <adapter-option> <key.path>` resolver. Resolves the
574	/// option (bare = parent, else shipped/local overlay) exactly as every other
575	/// consumer, then dot-path-traverses the merged `[strings]` tree. `Ok(None)` =
576	/// the key is absent (a clean "not set", distinct from an unresolvable option).
577	// [impl->REQ-MANIFEST-3]
578	pub fn get_string(
579	    adapters_dir: &Path,
580	    option: &str,
581	    key_path: &str,
582	) -> Result<Option<Value>, RegistryError> {
583	    let (record, manifest) = resolve_option(adapters_dir, option)?;
584	    let Some(value) = profile::string_at(&manifest.strings, key_path) else {
585	        return Ok(None);
586	    };
587	    // A literal value reads as-is. A single-key `{file=…}` pointer resolves to
588	    // the file's contents (REQ-MANIFEST-5), lazily — so live edits reflect.
589	    let Some(rel) = profile::as_file_pointer(value) else {
590	        return Ok(Some(value.clone()));
591	    };
592	    // Provenance (the update-safety guard): if this key is supplied as a file
593	    // pointer by a LOCAL profile, resolve against its user-owned dir (survives
594	    // adapter updates); otherwise against the adapter-shipped `strings/` dir.
595	    let (adapter, profile_seg) = profile::split_option(option);
596	    let base = match profile_seg {
597	        Some(p) if local_profile_supplies_pointer(adapters_dir, adapter, p, key_path) => {
598	            local_profiles_dir(adapters_dir, adapter)
599	        }
600	        _ => shipped_strings_dir(adapters_dir, &record),
601	    };
602	    Ok(read_string_file(&base, rel))
603	}
604
605	/// Whether `<adapter>:<profile>`'s LOCAL overlay file itself supplies `key_path`
606	/// as a file pointer — the provenance test that routes a pointer to the
607	/// user-owned profiles dir instead of the adapter-shipped `strings/` dir
608	/// (REQ-MANIFEST-5 update-safety). A shipped profile (no local file) is `false`.
609	fn local_profile_supplies_pointer(
610	    adapters_dir: &Path,
611	    adapter: &str,
612	    profile: &str,

/// `spt adapter get-string <adapter-option> <key.path>` resolver. Resolves the
/// option (bare = parent, else shipped/local overlay) exactly as every other
/// consumer, then dot-path-traverses the merged `[strings]` tree. `Ok(None)` =
/// the key is absent (a clean "not set", distinct from an unresolvable option).
// [impl->REQ-MANIFEST-3]
pub fn get_string(
    adapters_dir: &Path,
    option: &str,
    key_path: &str,
) -> Result<Option<Value>, RegistryError> {
    let (record, manifest) = resolve_option(adapters_dir, option)?;
    let Some(value) = profile::string_at(&manifest.strings, key_path) else {
        return Ok(None);
    };
    // A literal value reads as-is. A single-key `{file=…}` pointer resolves to
    // the file's contents (REQ-MANIFEST-5), lazily — so live edits reflect.
    let Some(rel) = profile::as_file_pointer(value) else {
        return Ok(Some(value.clone()));
    };
    // Provenance (the update-safety guard): if this key is supplied as a file
    // pointer by a LOCAL profile, resolve against its user-owned dir (survives
    // adapter updates); otherwise against the adapter-shipped `strings/` dir.
    let (adapter, profile_seg) = profile::split_option(option);
    let base = match profile_seg {
        Some(p) if local_profile_supplies_pointer(adapters_dir, adapter, p, key_path) => {
            local_profiles_dir(adapters_dir, adapter)
        }
        _ => shipped_strings_dir(adapters_dir, &record),
    };
    Ok(read_string_file(&base, rel))
}

/// Whether `<adapter>:<profile>`'s LOCAL overlay file itself supplies `key_path`
/// as a file pointer — the provenance test that routes a pointer to the
/// user-owned profiles dir instead of the adapter-shipped `strings/` dir
/// (REQ-MANIFEST-5 update-safety). A shipped profile (no local file) is `false`.
fn local_profile_supplies_pointer(
    adapters_dir: &Path,
    adapter: &str,
    profile: &str,


`get_string` resolves **only the `[strings]` tree** (not `[adapter].version`). So `spt adapter version <name>` surfaces an adapter-author-maintained **`[strings].version`** (human-facing) — distinct from `[adapter].version` (the manifest/update-comparison version, spt-core-facing). That's a clean alias + a docs convention to encourage. Find the integration checklist doc:


echo "=== integration/harness-contract checklist doc ==="; ls docs-site/src/ 2>/dev/null; find docs-site docs -iname "*checklist*" -o -iname "*harness-contract*" 2>/dev/null | head; echo "=== get-string CLI surface (AdapterCmd variants) ==="; sed -n '570,585p' crates/spt/src/cli.rs

Find checklist doc + AdapterCmd GetString surface


=== integration/harness-contract checklist doc ===
SUMMARY.md
cli
concepts
harness-contract
index.md
instances
lifecycle
messaging
networking
quickstart
reference
self-update
shells
terminal
docs-site/book/harness-contract
docs-site/src/harness-contract
docs-site/src/harness-contract/integration-checklist.md
=== get-string CLI surface (AdapterCmd variants) ===
    },
    /// Read a `[strings]` dot-path from an adapter option's merged view
    /// (`<adapter>[:profile] <key.path>`). Resolves through the profile overlay
    /// like every other consumer; prints the value (strings raw, else JSON).
    /// Exit 1 if the key is unset. Strings are **data** — never executed.
    GetString {
        /// `<adapter>` or `<adapter>:<profile>`.
        option: String,
        /// Dot-separated key path into `[strings]` (e.g. `hook.additionalContext`).
        key: String,
    },
    /// Prove an adapter's `[digest]` extractor against a real log sample
    /// (ADR-0019). Runs the declared extractor over `--sample <log>` (or the
    /// declared source) and prints the parsed contract records, the rendered
    /// digest, and **every dropped line with its reason** — the author-time
    /// answer to "`spt endpoint digest` returns nothing" (no silent empty).


Found both anchors: checklist = `docs-site/src/harness-contract/integration-checklist.md`; `GetString {option,key}` (cli.rs:575, "Exit 1 if unset"). Clean design — `spt adapter version <name>` = thin alias for `get_string(name,"version")` surfacing an adapter-maintained `[strings].version` (human-facing, distinct from `[adapter].version` = manifest/update-comparison). Add as **W6** (small, additive; sequenced after W5 — shared `cli.rs` AdapterCmd region). Task + dispatch:


TaskCreate

W6 — spt adapter version alias + docs semi-mandate

Operator-requested for v0.13.2. (1) `spt adapter version <name>` = thin alias for `spt adapter get-string <name> version` (registry::get_string → [strings].version). New AdapterCmd::Version{option}; print value strings-raw like GetString; exit 1 if unset (mirror GetString cli.rs:575). The `version` key is a [strings] convention (human-facing display version), DISTINCT from [adapter].version (manifest/update-comparison, spt-core-facing). (2) Docs semi-mandate: integration-checklist.md (docs-site/src/harness-contract/) strongly encourages adapter devs to populate [strings].version + keep it current (surfaced by `spt adapter version`); + MANIFEST/strings doc documents the convention + the two-version distinction. Mint REQ (rule 3); doc+impl+unit (no int — CLI alias, unit-covered). xtask gen (new CLI command). Sequenced after W5.


Task #9 created successfully: W6 — spt adapter version alias + docs semi-mandate


oh, if there's already `[adapter].version`, then it should just surface that.


Bash

"$OWL" send todlando doyle <<'EOF'
QUEUE W6 (operator-added to v0.13.2) — build AFTER W5 (shared cli.rs AdapterCmd region; finish W4+W5 first, gate, then this). Small + additive.

W6 — `spt adapter version <name>` alias + docs semi-mandate:
1. New AdapterCmd::Version{option} = a THIN alias for get-string <name> version: call spt_runtime::registry::get_string(&adapters, &option, "version"), print the value strings-raw (mirror GetString's print + EXIT 1 if unset, cli.rs:5974/575). Clap help concise (no REQ/M#/W# — it's public --help). It resolves [strings].version ONLY (get_string is [strings]-tree only — confirmed registry.rs:584), NOT [adapter].version.
2. KEY distinction to document: [adapter].version = the manifest/UPDATE-comparison version (spt-core-facing, mandatory, drives ripple-update); [strings].version = an adapter-author-maintained HUMAN-facing display version surfaced by `spt adapter version`. Two different things; the docs must make that explicit so devs don't conflate them.
3. DOCS (the operator's "semi-mandate"):
   - docs-site/src/harness-contract/integration-checklist.md — add a STRONG-ENCOURAGEMENT checklist item: adapter devs SHOULD populate [strings].version and keep it current; it's what `spt adapter version <name>` surfaces (a uniform "what version is this adapter" answer). Frame as a near-requirement, not optional boilerplate.
   - the MANIFEST / [strings] doc — document the `version` strings-key convention + the [adapter].version-vs-[strings].version distinction.
4. Mint REQ (rule 3) — propose REQ-ADAPTER-VERSION-STRING (the [strings].version convention + the `adapter version` alias surfacing it); stages [doc,impl,unit] (doc=checklist+manifest, no int — CLI alias is unit-covered). Tag on real evidence.
GATE: unit (alias resolves [strings].version; unset → exit 1 like get-string; profile-overlay path works like get-string). xtask gen (new CLI command → regen cli/reference.md). clippy --workspace --all-targets -D warnings; traceable standalone EXIT 0; scope-audit; co-author trailer; push. Ping me; I gate. Then deployah cuts v0.13.2 (W4+W5+W6 in).
EOF
echo "---sent $?---"

Queue W6 to todlando after W5


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


Prompt: oh, if there's already `[adapter].version`, then it should just surface that.


User correction: if `[adapter].version` already exists (it does — mandatory, manifest.rs:834 rejects empty), `spt adapter version` should just surface **that**, not a separate `[strings].version`. Cleaner — no second version field, no conflation. Correcting the W6 dispatch (supersedes what I just sent):


"$OWL" send todlando doyle <<'EOF'
W6 CORRECTION — SUPERSEDES my prior W6 message. Operator clarified: surface the EXISTING [adapter].version, NOT a [strings].version. Simpler, no second version field.

W6 (corrected):
1. `spt adapter version <name>` = new AdapterCmd::Version{option}; resolve_option(&adapters, &option) → print manifest.adapter.version (the EXISTING mandatory [adapter] manifest field — manifest.rs rejects empty at validate, ~834, so it's always set). NOT get_string, NOT [strings]. Drop the alias-for-get-string framing entirely. Handle the unresolvable-option error like the other adapter subcommands (the only failure mode; version itself is never empty). Clap help concise (public --help, no REQ/M#/W#).
2. NO new [strings].version convention. There is ONE version: [adapter].version. It is both the update-comparison version AND what `spt adapter version` surfaces.
3. DOCS (the operator's semi-mandate): integration-checklist.md — strong-encouragement item: keep [adapter].version CURRENT (bump it every release). It's already mandatory-non-empty, but staleness misleads BOTH `spt adapter version` AND the gh_release ripple-update comparison (which keys on it), so accuracy is near-required, not cosmetic. Frame it that way. (No manifest [strings] doc change needed now — just the checklist emphasis on keeping [adapter].version accurate.)
4. REQ (rule 3): REQ-ADAPTER-VERSION-CMD (`spt adapter version` surfaces [adapter].version); stages [doc,impl,unit] (doc=checklist, no int). Tag on real evidence.
GATE: unit (resolves + prints [adapter].version for a registered adapter; unresolvable option errors). xtask gen (new CLI command). clippy --workspace; traceable standalone EXIT 0; scope-audit; co-author trailer; push. Still AFTER W5. Ping me; I gate.
EOF
echo "---sent $?---"

Send corrected W6 (surface [adapter].version) to todlando


TaskUpdate

Operator-requested for v0.13.2 (corrected). `spt adapter version <name>` = new AdapterCmd::Version{option}; resolve_option(&adapters,&option) → print manifest.adapter.version (the EXISTING mandatory [adapter] field, manifest.rs:834 rejects empty). NOT get_string/[strings] — there is ONE version ([adapter].version), both update-comparison + human-surfaced. Handle unresolvable-option error (only failure mode). Docs semi-mandate: integration-checklist.md (docs-site/src/harness-contract/) — strong-encouragement to keep [adapter].version CURRENT (bump each release); staleness misleads BOTH `spt adapter version` AND the gh_release ripple-update comparison that keys on it → accuracy near-required not cosmetic. REQ-ADAPTER-VERSION-CMD (rule 3), doc+impl+unit (no int). xtask gen (new CLI command). Sequenced after W5.


Updated task #9 description


Corrected. W6 = `spt adapter version <name>` surfaces the existing `[adapter].version` (one version, no `[strings]` duplicate); checklist semi-mandate = keep it current (staleness misleads both the command *and* the ripple-update comparison that keys on it). Queued after W5, task #9 updated.

**v0.13.2 remaining:** W4 (building) → W5 → W6 → deployah cuts. W3 keystone complete + gated. Grill design captured on `design/endpoint-creation-flow`. Waiting on todlando's gate-packets. Reachable on `doyle`.


<task-notification>
<task-id>b3ok5tzh4</task-id>
<summary>Monitor event: "« spt event »"</summary>
<event><EVENT-PART seq="1/3" id="b31cec40" type="msg" from="todlando">ACK W6 — queued AFTER W5, using your CORRECTED version (supersedes the first): `spt adapter version &lt;name&gt;` = AdapterCmd::Version{option} -&gt; resolve_option -&gt; print manifest.adapter.version (the EXISTING mandatory [adapter] field; no [strings].version, no get-string alias). Docs = integration-checklist.</EVENT-PART>
<EVENT-PART seq="2/3" id="b31cec40">md strong-encouragement to keep [adapter].version current (drives both the cmd AND the gh_release ripple compare). Mint REQ-ADAPTER-VERSION-CMD [doc,impl,unit] (doc=checklist, no int). xtask gen for the new CLI command. Will build it after W5 (shared cli.rs AdapterCmd region — finish W4+W5+gate first). Order now: W4 -&gt; W5 -&gt; W6 -&gt; d</EVENT-PART>
<EVENT-PART seq="3/3" id="b31cec40">eployah cuts v0.13.2. Currently building W4 (update-message). Will ping at each gate.</EVENT-PART></event>
</task-notification>


todlando confirmed W6 (corrected) + locked the order: **W4 → W5 → W6 → deployah cuts v0.13.2**. Building W4 now, pings at each gate.

Nothing actionable until W4 lands. Waiting on the gate-packets. Reachable on `doyle`.


please write a JIT plan for v0.14.0 (the v0.13.2 followup we grilled)


Writing the v0.14.0 JIT for the endpoint-creation-flow milestone (ADR-0026/0027). It belongs with the design artifacts — switching to the design branch:


Bash

git checkout design/endpoint-creation-flow 2>&1 | tail -2

Switch to design branch to write the JIT


Your branch is up to date with 'origin/design/endpoint-creation-flow'.
Switched to branch 'design/endpoint-creation-flow'


Write

# v0.14.0 — Endpoint creation flow (multi-subnet home + Unbound state): JIT build plan

**Design (operator grill, 2026-06-22):** ADR-0026 (multi-subnet home at `endpoint run` creation) + ADR-0027 (Unbound endpoint state + attach-on-session-exists). CONTEXT.md term: **Unbound endpoint**. Two REQs registered (doc-active, this branch): `REQ-RUN-MULTISUBNET-HOME`, `REQ-ENDPOINT-UNBOUND-ATTACH`. MINOR bump (new endpoint-creation UX + a new on-disk liveness status).

**PREREQUISITE (binding):** start v0.14.0 from `main` **after v0.13.2 has merged** — this milestone builds on v0.13.2's broker `sessions` map carrying the per-endpoint `endpoint`/`adapter`/`install_dir` fields (ADR-0025 W3a), the source of truth for "is there a live session for this id." Rebase/merge this design branch (ADR-0026/0027 + the two REQs + this JIT) onto post-v0.13.2 main before activating.

**Motivating real-world case:** perri's multi-subnet bringup gap (operator-hit, 2026-06-22) — a **latent** gap (not a regression; `HOME_REFUSED` is the established ≥0.11.0 no-guess policy), exposed when the node crossed 1→2 subnets (BIGNET added). The spt-hosted bringup never supported a multi-subnet home. W2 closes it; perri's repro is the W6 regression test (**MUST run on a multi-subnet node** — a single-subnet node auto-homes and hides the gap).

**The two "resident" facts that shape the waves** (verified at code, the grill):
- Home is **immutable** (ADR-0010). `establish_perch → stamp_creation_fields` (home.rs:122) **inherits** a prior perch's `home_subnet` (prior-branch) and only `assign_home`s on the fresh (None) branch. So the gap is **only first-creation** on multi-subnet; pre-creating a skeleton perch WITH the home makes bind inherit it — no hook/env change.
- The broker (`spt daemon run`) and the brain (`spt daemon brain`) are **separate processes** (v0.13.2 W3c finding); the broker `sessions` map is the IPC-reachable, brain-restart-surviving source of "live session." Attach + the Unbound display both read it.

---

## W1 — `STATUS_UNBOUND` status primitive + lifecycle  (REQ-ENDPOINT-UNBOUND-ATTACH, ADR-0027)
Foundational — the skeleton (W2), the attach (W4), and the display (W5) all key on it.
- **Scope:** add `STATUS_UNBOUND` (`liveness.rs:37`, beside `STATUS_ONLINE`/`STATUS_OFFLINE`). Lifecycle transitions: spawn / skeleton-create → `unbound`; `bind` (cmd_bind establish-online) → `online`; broker session death → `offline` via the EXISTING exit-waiter/reconcile (the B2/B5 dead-but-latched path — reuse, no new teardown). Liveness predicates: an `unbound` perch is **attachable** (a live broker session exists) but **NOT message-addressable** (no bound `session_id`) — `is_perch_alive` / deliver resolution stay gated on `online`/bound; a new "has a live session" predicate (reads the broker sessions map) backs attach + display.
- **Files:** `crates/spt-store/src/liveness.rs` (the constant + predicates); the reconcile/exit-waiter that stamps offline (livehost.rs / broker session-exit path); `info.json` status writer.
- **Gate:** unit (the transitions; `unbound` is attachable-not-addressable; reconcile resolves a dead-session `unbound` → `offline`). doc = ADR-0027 + CONTEXT (active). No int yet (composed in W6).

## W2 — Home resolution + skeleton-with-home  (REQ-RUN-MULTISUBNET-HOME, ADR-0026) — the multi-subnet UNBLOCK
- **Scope:** at `endpoint run`'s skeleton-create step, resolve the home and **pre-create the skeleton perch** (status `unbound` from W1, `home_subnet` set) BEFORE spawning the harness; the hook's `bind` then inherits home via `establish_perch`'s prior-branch (UNBOUND→ONLINE). Resolution policy:
  - sole subnet → auto (`assign_home`, unchanged);
  - multi-subnet + no `--subnet` + **non-interactive** (`std::io::IsTerminal` on stdin, the cli.rs:1006/3956 precedent) → **refuse early** (`MULTI_SUBNET_HOME:` clear message + the subnet list) — never the silent 25s timeout;
  - multi-subnet + no `--subnet` + **interactive** → print the proposed config (id / project / adapter[:profile] / home = the default) + `Ok to proceed? Y/n`; `n` → print the `--subnet` guidance;
  - `--subnet <name>` overrides + validates membership (`assign_home` `NotMember`).
  - The default home (for the confirm) = a **node-global last-used** value to start (W3 upgrades it to the two-level lists).
- **Files:** `cmd_endpoint_run` (cli.rs:~1384, where it spawns straight to `launch_harness_brokered_in` today — add the resolve + skeleton-write before it); a `--subnet` arg on `endpoint run`; `assign_home` (home.rs:71) reused; the skeleton write via `info::write_info` (status `unbound`, `home_subnet`, `state=live_agent`, `adapter`, `cwd`). Conflict-safe: a skeleton has an empty `session_id`, so `establish_perch`'s Conflict check (owner-alive ∧ session-mismatch) never trips.
- **Gate:** unit (resolution matrix: sole-auto / multi-noninteractive-refuse / multi-interactive-confirm / --subnet override+NotMember; the skeleton carries the resolved home; bind inherits it via prior-branch). doc = ADR-0026 + CONTEXT. (int in W6.)

## W3 — Two-level MRU home lists  (REQ-RUN-MULTISUBNET-HOME)
Upgrades W2's single-value default to the full UX.
- **Scope:** persisted **ordered move-to-front LISTs** of recently-chosen home subnets at **two levels**: per-project (keyed by project_id) + an always-updated node-global list (the fallback for a project with no list yet). The interactive default home = project-list head ?? node-global head; the interactive confirm + the `--subnet` refusal guidance list subnets **MRU-ordered**. Both lists update (move-to-front) on every home choice (run-confirm or explicit `--subnet`).
- **Files:** a small MRU store under `$SPT_HOME` (node-global) + per-project (project store / `p-<project>` adjacency or a sibling file); the `endpoint run` resolve path reads/writes it; project_id from the run cwd (`project_id_for_dir`).
- **Gate:** unit (move-to-front; project-then-node-global precedence; node-global fallback for an MRU-less project; the guidance/confirm ordering). doc = CONTEXT (the MRU note on the home-subnet term).

## W4 — Attach-on-session-exists  (REQ-ENDPOINT-UNBOUND-ATTACH, ADR-0027)
- **Scope:** gate the attach on the broker **session being attachable**, not on perch `STATUS_ONLINE`. Replace `await_endpoint_online` (cli.rs:1434) with an await-session-attachable poll (reads the broker sessions map / a "has live session" query, the W1 predicate). `cmd_endpoint_run` AND `spt rc <id>` attach to a live session **regardless of perch status** (so an `unbound` endpoint is attachable — headless bringups too, and the operator can clear a bind-gating prompt). Preserve REQ-HAZARD-RC-ATTACH-ONLINE-RACE's "no attach before a session" intent at the earlier session-exists point; bounded deadline; local-only (a remote row can't see another node's broker).
- **Files:** `cmd_endpoint_run` (cli.rs:1426-1447, the await+attach block); `run_attach` / `spt rc` status-gate (rc.rs) — attach when a session exists, not only when online; the broker "has live session for id" query (over the W3a sessions map).
- **Gate:** unit (the gate keys on session-exists not perch-online; the offline-but-session-live case attaches; no-session still awaits/fails). int folded into W6.

## W5 — `Unbound` display  (REQ-ENDPOINT-UNBOUND-ATTACH)
- **Scope:** `EpDisplay` gains `Unbound` (picker/model.rs:55; today Offline/Online/HarnessOnly/Controlled). Render **hollow** (the existing filled/hollow-square precedent) + a **hollow-controlled** variant (an `unbound` endpoint a node is driving) — paralleling filled Online vs Controlled. **NOT amber** — amber is `HarnessOnly` (online-but-not-broker-controllable), the semantic opposite of `Unbound` (which IS broker-attachable). Derivation: `STATUS_UNBOUND` (+ `driven_by` for the controlled variant). Surface in the picker, `endpoint list`, and `whoami`. Remote rows render plain (the existing controllability limitation, picker/data.rs:254).
- **Files:** picker/model.rs (`EpDisplay::Unbound` + label/legend + the type-gated derivation), picker/data.rs (derive from status), `endpoint list`/`whoami` renderers.
- **Gate:** unit (status `unbound` → `EpDisplay::Unbound`; + driven_by → the controlled variant; offline/online/harness-only unchanged; remote renders plain). doc = the picker legend.

## W6 — int keystone + the multi-subnet regression  (both REQs)
- **Scope:** real-daemon e2e (reuse the v0.12.1 dummy-harness fixture, NO mocks):
  - **perri's multi-subnet repro (the regression test, MUST run on a multi-subnet node):** join a 2nd subnet → `spt endpoint run --adapter X --id Y` (no `--subnet`, non-interactive) → REFUSES clearly (not a 25s timeout); with `--subnet S` → the skeleton homes to S, the harness binds (UNBOUND→ONLINE), no `HOME_REFUSED`. A single-subnet control proves auto-home still works.
  - **attach-before-bind:** `endpoint run` (or `spt rc`) attaches while the endpoint is `unbound` (session live, not yet bound), the operator drives it, then bind flips it ONLINE.
  - **interactive confirm:** (where a TTY can be simulated) the proposed-config + Y/n path; non-interactive refuse.
- **Activate:** `REQ-RUN-MULTISUBNET-HOME` → `[doc,impl,unit,int]`; `REQ-ENDPOINT-UNBOUND-ATTACH` → `[doc,impl,unit,int]`. Flip ADR-0026/0027 status → **accepted** (with any build amendments, the ADR-0025 precedent).
- **Gate:** doyle gates the int hard against perri's repro; **perri validates on real claude-spt at publish** (her queued v0.13.3/v0.14.0 validation).

---

## Build order, gating, ship
**Order:** W1 (status primitive) → W2 (home resolve + skeleton — the unblock) → W3 (MRU lists) → W4 (attach-on-session) → W5 (Unbound display) → W6 (int + regression). W2/W4/W5 share the broker sessions map + the perch status seam — sequential, doyle gates each (CI green BOTH runners per wave; the contended Windows host can't run the lifecycle/daemon suite — CI is authority). Activate each REQ's real stages at its wave (rule 5).

**Scope note (operator decision):** shipped as ONE milestone (not split into a minimal-unblock fast-follow) — the operator accepted staying multi-subnet-blocked until the cohesive landing. The whole thing is v0.14.0.

**Downstream:** perri drops her temp `--subnet` hook stopgap (she declined it — never applied; she's been multi-subnet-blocked by choice). She validates her exact repro on real claude-spt at publish. Coordinate the v0.14.0 publish ping to perri.

**Numbering note:** ADR-0024/0025 ship with v0.13.2; ADR-0026/0027 are this milestone (stable).


File created successfully at: C:\Users\decid\Documents\projects\spt-core\V0.14.0-ENDPOINT-CREATION-FLOW-JIT.md (file state is current in your context — no need to Read it back)

# v0.14.0 — Endpoint creation flow (multi-subnet home + Unbound state): JIT build plan

**Design (operator grill, 2026-06-22):** ADR-0026 (multi-subnet home at `endpoint run` creation) + ADR-0027 (Unbound endpoint state + attach-on-session-exists). CONTEXT.md term: **Unbound endpoint**. Two REQs registered (doc-active, this branch): `REQ-RUN-MULTISUBNET-HOME`, `REQ-ENDPOINT-UNBOUND-ATTACH`. MINOR bump (new endpoint-creation UX + a new on-disk liveness status).

**PREREQUISITE (binding):** start v0.14.0 from `main` **after v0.13.2 has merged** — this milestone builds on v0.13.2's broker `sessions` map carrying the per-endpoint `endpoint`/`adapter`/`install_dir` fields (ADR-0025 W3a), the source of truth for "is there a live session for this id." Rebase/merge this design branch (ADR-0026/0027 + the two REQs + this JIT) onto post-v0.13.2 main before activating.

**Motivating real-world case:** perri's multi-subnet bringup gap (operator-hit, 2026-06-22) — a **latent** gap (not a regression; `HOME_REFUSED` is the established ≥0.11.0 no-guess policy), exposed when the node crossed 1→2 subnets (BIGNET added). The spt-hosted bringup never supported a multi-subnet home. W2 closes it; perri's repro is the W6 regression test (**MUST run on a multi-subnet node** — a single-subnet node auto-homes and hides the gap).

**The two "resident" facts that shape the waves** (verified at code, the grill):
- Home is **immutable** (ADR-0010). `establish_perch → stamp_creation_fields` (home.rs:122) **inherits** a prior perch's `home_subnet` (prior-branch) and only `assign_home`s on the fresh (None) branch. So the gap is **only first-creation** on multi-subnet; pre-creating a skeleton perch WITH the home makes bind inherit it — no hook/env change.
- The broker (`spt daemon run`) and the brain (`spt daemon brain`) are **separate processes** (v0.13.2 W3c finding); the broker `sessions` map is the IPC-reachable, brain-restart-surviving source of "live session." Attach + the Unbound display both read it.

---

## W1 — `STATUS_UNBOUND` status primitive + lifecycle  (REQ-ENDPOINT-UNBOUND-ATTACH, ADR-0027)
Foundational — the skeleton (W2), the attach (W4), and the display (W5) all key on it.
- **Scope:** add `STATUS_UNBOUND` (`liveness.rs:37`, beside `STATUS_ONLINE`/`STATUS_OFFLINE`). Lifecycle transitions: spawn / skeleton-create → `unbound`; `bind` (cmd_bind establish-online) → `online`; broker session death → `offline` via the EXISTING exit-waiter/reconcile (the B2/B5 dead-but-latched path — reuse, no new teardown). Liveness predicates: an `unbound` perch is **attachable** (a live broker session exists) but **NOT message-addressable** (no bound `session_id`) — `is_perch_alive` / deliver resolution stay gated on `online`/bound; a new "has a live session" predicate (reads the broker sessions map) backs attach + display.
- **Files:** `crates/spt-store/src/liveness.rs` (the constant + predicates); the reconcile/exit-waiter that stamps offline (livehost.rs / broker session-exit path); `info.json` status writer.
- **Gate:** unit (the transitions; `unbound` is attachable-not-addressable; reconcile resolves a dead-session `unbound` → `offline`). doc = ADR-0027 + CONTEXT (active). No int yet (composed in W6).

## W2 — Home resolution + skeleton-with-home  (REQ-RUN-MULTISUBNET-HOME, ADR-0026) — the multi-subnet UNBLOCK
- **Scope:** at `endpoint run`'s skeleton-create step, resolve the home and **pre-create the skeleton perch** (status `unbound` from W1, `home_subnet` set) BEFORE spawning the harness; the hook's `bind` then inherits home via `establish_perch`'s prior-branch (UNBOUND→ONLINE). Resolution policy:
  - sole subnet → auto (`assign_home`, unchanged);
  - multi-subnet + no `--subnet` + **non-interactive** (`std::io::IsTerminal` on stdin, the cli.rs:1006/3956 precedent) → **refuse early** (`MULTI_SUBNET_HOME:` clear message + the subnet list) — never the silent 25s timeout;
  - multi-subnet + no `--subnet` + **interactive** → print the proposed config (id / project / adapter[:profile] / home = the default) + `Ok to proceed? Y/n`; `n` → print the `--subnet` guidance;
  - `--subnet <name>` overrides + validates membership (`assign_home` `NotMember`).
  - The default home (for the confirm) = a **node-global last-used** value to start (W3 upgrades it to the two-level lists).
- **Files:** `cmd_endpoint_run` (cli.rs:~1384, where it spawns straight to `launch_harness_brokered_in` today — add the resolve + skeleton-write before it); a `--subnet` arg on `endpoint run`; `assign_home` (home.rs:71) reused; the skeleton write via `info::write_info` (status `unbound`, `home_subnet`, `state=live_agent`, `adapter`, `cwd`). Conflict-safe: a skeleton has an empty `session_id`, so `establish_perch`'s Conflict check (owner-alive ∧ session-mismatch) never trips.
- **Gate:** unit (resolution matrix: sole-auto / multi-noninteractive-refuse / multi-interactive-confirm / --subnet override+NotMember; the skeleton carries the resolved home; bind inherits it via prior-branch). doc = ADR-0026 + CONTEXT. (int in W6.)

## W3 — Two-level MRU home lists  (REQ-RUN-MULTISUBNET-HOME)
Upgrades W2's single-value default to the full UX.
- **Scope:** persisted **ordered move-to-front LISTs** of recently-chosen home subnets at **two levels**: per-project (keyed by project_id) + an always-updated node-global list (the fallback for a project with no list yet). The interactive default home = project-list head ?? node-global head; the interactive confirm + the `--subnet` refusal guidance list subnets **MRU-ordered**. Both lists update (move-to-front) on every home choice (run-confirm or explicit `--subnet`).
- **Files:** a small MRU store under `$SPT_HOME` (node-global) + per-project (project store / `p-<project>` adjacency or a sibling file); the `endpoint run` resolve path reads/writes it; project_id from the run cwd (`project_id_for_dir`).
- **Gate:** unit (move-to-front; project-then-node-global precedence; node-global fallback for an MRU-less project; the guidance/confirm ordering). doc = CONTEXT (the MRU note on the home-subnet term).

## W4 — Attach-on-session-exists  (REQ-ENDPOINT-UNBOUND-ATTACH, ADR-0027)
- **Scope:** gate the attach on the broker **session being attachable**, not on perch `STATUS_ONLINE`. Replace `await_endpoint_online` (cli.rs:1434) with an await-session-attachable poll (reads the broker sessions map / a "has live session" query, the W1 predicate). `cmd_endpoint_run` AND `spt rc <id>` attach to a live session **regardless of perch status** (so an `unbound` endpoint is attachable — headless bringups too, and the operator can clear a bind-gating prompt). Preserve REQ-HAZARD-RC-ATTACH-ONLINE-RACE's "no attach before a session" intent at the earlier session-exists point; bounded deadline; local-only (a remote row can't see another node's broker).
- **Files:** `cmd_endpoint_run` (cli.rs:1426-1447, the await+attach block); `run_attach` / `spt rc` status-gate (rc.rs) — attach when a session exists, not only when online; the broker "has live session for id" query (over the W3a sessions map).
- **Gate:** unit (the gate keys on session-exists not perch-online; the offline-but-session-live case attaches; no-session still awaits/fails). int folded into W6.

## W5 — `Unbound` display  (REQ-ENDPOINT-UNBOUND-ATTACH)
- **Scope:** `EpDisplay` gains `Unbound` (picker/model.rs:55; today Offline/Online/HarnessOnly/Controlled). Render **hollow** (the existing filled/hollow-square precedent) + a **hollow-controlled** variant (an `unbound` endpoint a node is driving) — paralleling filled Online vs Controlled. **NOT amber** — amber is `HarnessOnly` (online-but-not-broker-controllable), the semantic opposite of `Unbound` (which IS broker-attachable). Derivation: `STATUS_UNBOUND` (+ `driven_by` for the controlled variant). Surface in the picker, `endpoint list`, and `whoami`. Remote rows render plain (the existing controllability limitation, picker/data.rs:254).
- **Files:** picker/model.rs (`EpDisplay::Unbound` + label/legend + the type-gated derivation), picker/data.rs (derive from status), `endpoint list`/`whoami` renderers.
- **Gate:** unit (status `unbound` → `EpDisplay::Unbound`; + driven_by → the controlled variant; offline/online/harness-only unchanged; remote renders plain). doc = the picker legend.

## W6 — int keystone + the multi-subnet regression  (both REQs)
- **Scope:** real-daemon e2e (reuse the v0.12.1 dummy-harness fixture, NO mocks):
  - **perri's multi-subnet repro (the regression test, MUST run on a multi-subnet node):** join a 2nd subnet → `spt endpoint run --adapter X --id Y` (no `--subnet`, non-interactive) → REFUSES clearly (not a 25s timeout); with `--subnet S` → the skeleton homes to S, the harness binds (UNBOUND→ONLINE), no `HOME_REFUSED`. A single-subnet control proves auto-home still works.
  - **attach-before-bind:** `endpoint run` (or `spt rc`) attaches while the endpoint is `unbound` (session live, not yet bound), the operator drives it, then bind flips it ONLINE.
  - **interactive confirm:** (where a TTY can be simulated) the proposed-config + Y/n path; non-interactive refuse.
- **Activate:** `REQ-RUN-MULTISUBNET-HOME` → `[doc,impl,unit,int]`; `REQ-ENDPOINT-UNBOUND-ATTACH` → `[doc,impl,unit,int]`. Flip ADR-0026/0027 status → **accepted** (with any build amendments, the ADR-0025 precedent).
- **Gate:** doyle gates the int hard against perri's repro; **perri validates on real claude-spt at publish** (her queued v0.13.3/v0.14.0 validation).

---

## Build order, gating, ship
**Order:** W1 (status primitive) → W2 (home resolve + skeleton — the unblock) → W3 (MRU lists) → W4 (attach-on-session) → W5 (Unbound display) → W6 (int + regression). W2/W4/W5 share the broker sessions map + the perch status seam — sequential, doyle gates each (CI green BOTH runners per wave; the contended Windows host can't run the lifecycle/daemon suite — CI is authority). Activate each REQ's real stages at its wave (rule 5).

**Scope note (operator decision):** shipped as ONE milestone (not split into a minimal-unblock fast-follow) — the operator accepted staying multi-subnet-blocked until the cohesive landing. The whole thing is v0.14.0.

**Downstream:** perri drops her temp `--subnet` hook stopgap (she declined it — never applied; she's been multi-subnet-blocked by choice). She validates her exact repro on real claude-spt at publish. Coordinate the v0.14.0 publish ping to perri.

**Numbering note:** ADR-0024/0025 ship with v0.13.2; ADR-0026/0027 are this milestone (stable).


Bash

git add V0.14.0-ENDPOINT-CREATION-FLOW-JIT.md && git commit -q -m "$(cat <<'EOF'
docs: v0.14.0 endpoint-creation-flow JIT build plan

Wave-by-wave plan for the milestone grilled with the operator (ADR-0026/
0027): W1 STATUS_UNBOUND primitive → W2 home-resolve + skeleton-with-home
(the multi-subnet unblock) → W3 two-level MRU lists → W4 attach-on-session
→ W5 Unbound display → W6 int + perri's multi-subnet regression. Prereq:
start from main after v0.13.2 merges (builds on the broker sessions map).
One milestone (operator chose all-together); MINOR bump.

Co-authored by: doyle
EOF
)" && git push 2>&1 | tail -2; echo "=== back to v0.13.2 ==="; git checkout v0.13.2-adapter-packaging 2>&1 | tail -1

Commit + push v0.14.0 JIT, return to v0.13.2 branch


warning: in the working copy of 'V0.14.0-ENDPOINT-CREATION-FLOW-JIT.md', LF will be replaced by CRLF the next time Git touches it
To https://github.com/SaberMage/spt-core.git
   5fe1f0e..7aab0fb  design/endpoint-creation-flow -> design/endpoint-creation-flow
=== back to v0.13.2 ===
Switched to branch 'v0.13.2-adapter-packaging'


v0.14.0 JIT written + pushed to `design/endpoint-creation-flow` @7aab0fb (with ADR-0026/0027 + the 2 REQs). Updating task #8 + summary:


TaskUpdate

v0.14.0 endpoint-creation-flow milestone (multi-subnet home + Unbound) — DESIGNED + JIT'd


Updated task #8 subject


**v0.14.0 JIT done** — on `design/endpoint-creation-flow` @7aab0fb alongside ADR-0026/0027 + the 2 REQs. Six waves:

| Wave | Delivers |
|------|----------|
| **W1** | `STATUS_UNBOUND` primitive + lifecycle (spawn→unbound, bind→online, death→offline; attachable-not-addressable; reuses exit-waiter/reconcile) — foundation |
| **W2** | Home resolve + skeleton-with-home → bind inherits via prior-branch (**the multi-subnet unblock**): sole-auto / multi-noninteractive-refuse / multi-interactive-confirm / `--subnet` |
| **W3** | Two-level MRU lists (per-project + node-global fallback) |
| **W4** | Attach-on-session-exists (replaces `await_endpoint_online`; `rc` + `endpoint run` attach an unbound endpoint) |
| **W5** | `EpDisplay::Unbound` (hollow + hollow-controlled) |
| **W6** | int + perri's multi-subnet regression (MUST run multi-subnet) + attach-before-bind |

**Key framing baked in:** MINOR bump; **prerequisite** = start from main *after v0.13.2 merges* (builds on the v0.13.2 broker sessions map); one milestone (your all-together call); perri's repro = the W6 regression test; ADR-0026/0027 flip to accepted at W6.

Not dispatched — it activates after v0.13.2 ships (then mint impl/unit/int stages + dispatch todlando). v0.13.2 stays the focus: W4 building → W5 → W6 → cut. Reachable on `doyle`.


<command-message>spt:commune</command-message>
<command-name>/spt:commune</command-name>
<command-args>with immediate next steps and broad summary of the project's status + end goal. we'll resume in the next session</command-args>


Base directory for this skill: C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.25\skills\commune

# /spt:commune

All commands use `$LIVE` env var, auto-injected by the plugin's SessionStart hook. If commands fail with "command not found", restart the Claude Code session so the SessionStart hook re-runs.

> **Identity auto-detection:** Your identity is auto-detected from your session. Pass your ID explicitly only if auto-detection fails.

## Flow

1. **Download Psyche's *current* context** — it shifts during the session as background echo communes fire. Re-download right before composing; the gen-N snapshot you read at session start is already stale:
   ```bash
   $LIVE psyche-download <your-id>
   ```

   > **Output truncation.** If the Bash tool reports `Output too large (N KB)` and saves the full result to a `tool-results/<id>.txt` file, use the `Read` tool on that saved path to access the full content. Do NOT re-run `psyche-download` to grep/awk out sections — repeated invocations re-download the same context, waste tokens, and may duplicate `## Pending Commune` sections in `live_context.md`.

2. **Diff your knowledge against this fresh download** — only what's missing from *it* is the real delta (new work, decisions, intentions). Anything already absorbed is not. If the download includes a `<memformat>` section, use it as a guide for what topics to cover. A `## Pending Commune (uncommitted)` section means a previous commune is still queued — do NOT re-send it.

3. **Commune only the delta** -- use the **Write tool** to create
   `.claude/{your-id}-commune.md` with the two-slice body as its contents
   (see `## Two-slice body shape (Phase 25 D-10/D-11)` below for the required
   envelope wrapping). Do not use Bash/heredoc -- Write is the canonical path:
   no shell escaping, no `EOF` collision risk, and Write auto-creates the
   `.claude/` parent directory.

   Your Self listener detects the file on its next poll iteration, notifies
   your Psyche wrapper, and the wrapper ingests the content via the existing
   Psyche session. On success (subprocess exit code 0) the wrapper deletes the
   file. On error (token rate limit, etc) the file is retained and retried on
   the next consume cycle. If you write again before the wrapper consumes, the
   latest write wins (last-write-wins overwrite semantics).

If Psyche's context is already current, no commune is needed. Say so and move on.

Read `commune.md` (in this skill directory) for the full commune protocol -- when to send, what to include, and diligence triggers.

**Agent-type guard:** Only works on live agents. If target is not live, refuses with: "Communes require a live agent with Psyche."

## Two-slice body shape (Phase 25 D-10/D-11)

The body you write in `.claude/{your-id}-commune.md` MUST itself be wrapped in two-slice envelopes per Phase 25 D-10/D-11. The taxonomy — what belongs in `<live-context>` vs `<project-context>` — is defined canonically in `psyche.md` §`<output_envelope>` (lines 282–323). Read that section as the source of truth; this skill doc teaches only when and how to apply it.

**In-project detection rule (D-25.1-04).** Before composing your commune body, run `$LIVE psyche-download <your-id>` (Step 1 of `## Flow` above) and inspect the `<current ... />` tag at the top of the output. The `<current/>` tag carries a `project="..."` attribute whenever any payload is produced — its presence means you are inside a tracked project (inside a git repo, `project` is the repo name; outside a git repo per D-11, `project` is the cwd-basename fallback, which still counts as a project context for routing purposes). A `psyche-download` invocation that returns no payload at all (NO-CONTEXT exit) means no project resolved. So: `project="..."` populated on `<current/>` → emit BOTH envelopes (apply rule 1 or 2 below); no payload at all → emit ONLY `<live-context>` (rule 3). Secondary signal: the `<project-context-resolved name="..."/>` sentinel further down the output indicates the project file already has prior content — useful for spotting first-time-in-project pairings, but NOT the routing rule.

**Three rules (D-25.1-01..03):**

- **In-project, both slices populated** (marker present, you have project-specific content this cycle): emit `<live-context>...</live-context>` AND `<project-context>...</project-context>`, both with non-empty bodies.
- **In-project, no project-specific update this cycle** (marker present, but nothing project-bound to report): emit `<live-context>...</live-context>` AND `<project-context></project-context>` (empty body — the deliberate "in-project but quiet" signal per D-25.1-02). Distinguishable from "no project resolved".
- **Outside any tracked project** (marker absent): emit ONLY `<live-context>...</live-context>`. Do NOT emit an empty `<project-context>` envelope (missing tag means "no project resolved" per D-25.1-03).

**Nested envelopes.** The Phase 23 `<EVENT type="commune">` envelope (see `## Envelope shape (Phase 23 v1.8)` below) is composed by `$LIVE commune` automatically — you NEVER write `<EVENT>` tags. You write ONLY the inner two-slice body. The two envelope layers nest: outer EVENT wraps inner two-slice body.

(See `psyche.md` §`<output_envelope>` for the canonical taxonomy and per-slice content rules.)

## Envelope shape (Phase 23 v1.8)

As of v1.8 Phase 23, commune deliveries are wrapped in a typed EVENT envelope carrying a 5-field stamp (`machine`, `project`, `branch`, `head_sha`, `head_subject`); the prose `COMMUNE (timestamp): body` form is gone.

```xml
<EVENT type="commune" timestamp="2026-05-20T06:00:00-07:00" machine="hostname" project="claude_skill_owl" branch="main" head_sha="abc123..." head_subject="subject line capped at 72 chars">body</EVENT>
```

Outside a git repo (D-11), `branch`, `head_sha`, and `head_subject` are omitted entirely (not empty-string-set):

```xml
<EVENT type="commune" timestamp="2026-05-20T06:00:00-07:00" machine="hostname" project="cwd-basename">body</EVENT>
```

`$LIVE commune` composes this envelope automatically — Self does NOT manually construct EVENT XML. The `body` placeholder in the example above is the two-slice body (see `## Two-slice body shape (Phase 25 D-10/D-11)` above); the two envelope layers nest. The wrapper-side parser ignores unknown attrs, so the stamp fields are forward-compatible with older receivers. For drift detection on resume, see the `live` skill.

ARGUMENTS: with immediate next steps and broad summary of the project's status + end goal. we'll resume in the next session


Following commune flow — download my current Psyche context first (it shifts mid-session):


Bash

"$LIVE" psyche-download doyle 2>&1 | head -60

Download current doyle Psyche context


/usr/bin/bash: line 6: C:/Users/decid/.claude/plugins/cache/cplugs/spt/1.11.25/owl.exe live: No such file or directory


"$OWL" live psyche-download doyle 2>&1 | head -80

Download doyle Psyche context (corrected invocation)


<memformat>
<?xml version="1.0"?>
<memformat version="1">
<!-- Changelog: v1 - initial seed -->
<topics>
  <topic name="events">Notable or unusual events and requests</topic>
  <topic name="interactions">Agent interactions -- who, roles, contributions</topic>
  <topic name="reflections">Thoughts on recent work -- what went well, what was tricky</topic>
  <topic name="ideas">Ideas to raise with the user</topic>
  <topic name="improvements">Further improvement ideas for recent problem spaces</topic>
  <topic name="timeline">What happened when, timing, sequencing</topic>
  <topic name="blockers">Current blockers or concerns</topic>
  <topic name="focus">Current focus and immediate next steps</topic>
</topics>
</memformat>
</memformat>

<current machine="HFENDULEAM" project="spt-core" branch="v0.13.2-adapter-packaging" head_sha="92ee7884b0477ab0d38c604b4b421baf3ca772e8" head_subject="test(daemon): W3e no-mocks keystone int + ADR-0025 accepted (live update…" commits_since="0" commits_unpulled="0"/>
**W3d-i = GATE-PASS @b62476e** (clippy 0, `apply_release_crc_swap_only_swaps_changed` pass, traceable 0). Update now non-destructive even pre-W3d-ii.

**W3d-ii status:** design-approved with binding constraints; todlando building (careful gate on diff).

**v0.13.2 keystone progress:**

| Step | State |
|------|-------|
| W1/W2/W3a+W3b/W3c | ✅ GATE-PASS |
| W3d-i CLI-side CRC swap | ✅ GATE-PASS @b62476e |
| W3d-ii broker live-apply | 🔨 design-approved, building |
| W3e no-mocks keystone int | queued (perri 4-step repro + Windows-lock regression = ship-blocker) |
| W4/W5/release | queued |

**Grill (v0.13.3 multi-subnet fix):** paused, captured in task #8. Design crystallized: skeleton→live (reuse existing pattern); `endpoint run` creates skeleton perch with resolved home before spawn; home resolution = single-auto / multi-subnet non-interactive-refuse / interactive-confirm-MRU / `--subnet` override. Remaining: skeleton lifecycle+cleanup, MRU storage, attach-ASAP, ADR.

Perri psyche-lock fix landed @d6d7816.<project-context-resolved name="spt-core"/>
v0.13.2 now at W3d-i gate-pass, W3d-ii in build. W3e blocker identified (Windows-lock regression + perri repro). Grill design solid but paused for v0.13.2 ship.


/spt:commune with immediate next steps and broad summary of the project's status + end goal. we'll resume in the next session


In-project (`project="spt-core"`). Snapshot is stale at W3d-i/ii — the delta since: W3d-ii + W3e gate-passed (W3 complete, ship-blocker cleared), grill completed→v0.14.0 designed+JIT'd, W6 added. Writing the delta commune (both slices):


Write

<live-context>
Session-close commune (doyle, 2026-06-22). Resuming next session. Delta since the gen snapshot (which had W3d-i/ii + the grill-paused).

MY ROLE this session: drove v0.13.2 build→release as gater/orchestrator (operator: "run the build, todlando executes, you gate, deployah publishes, see it through"). Dispatch live agents via `$OWL send <target> doyle` (legacy owl — todlando/perri/deployah live THERE; NOT subagents, NOT spt-core endpoints — I mis-started with a subagent early and the operator corrected me). Gated every wave by reading the REAL diff + re-running gates myself (green-units ≠ green-real held all session).

IMMEDIATE NEXT STEPS (resume here):
1. Gate W4 (update-message) when todlando pings — markdown [update].message, only on an actual apply, not a no-op.
2. Gate W5 (`*-proof --dir/--manifest`, F-011) — unit + int.
3. Gate W6 (`spt adapter version <name>` — surfaces the EXISTING [adapter].version, NOT a [strings] dup; + integration-checklist semi-mandate to keep [adapter].version current since it also drives the gh_release ripple compare).
4. Verify FINAL CI green on BOTH runners on the v0.13.2 branch (PR #29) — the daemon/lifecycle suite can't run on this contended Windows box; CI is the authority.
5. deployah cuts v0.13.2: VERIFY the release counter vs spt-releases metadata (NOT memory — the v0.13.1 lesson); author the bump (workspace.package.version 0.13.1→0.13.2 + CHANGELOG + Cargo.lock first-party); push bump→CI-green-both→tag→sign+publish; ping perri with version+hashes.
6. AFTER v0.13.2 ships + merges to main: start v0.14.0 — rebase `design/endpoint-creation-flow` onto post-v0.13.2 main, mint impl/unit/int stages on the 2 REQs, dispatch todlando W1 (STATUS_UNBOUND).

LESSONS (worth keeping):
- The design-gate + incremental gating paid off TWICE: caught ADR-0025 step-3 unimplementable (BrainLifecycle is thread-local, IPC-unreachable) AND the broker≠brain PROCESS boundary (an Arc can't span processes — I'd SEEN the distinct PIDs clearing the build lock and missed it; todlando STOP-AND-FLAGged → reload-on-dirty). Always read the real diff.
- File-lock: a stale DEV daemon (target/debug/spt) locked the build artifact → scoped PID-tree kill of the DEV daemon only. Installed live daemon = AppData\Local\spt-core\bin, hosts wall-* — NEVER machine-wide kill.
- Windows: std::fs::rename FAILS over an existing file (todlando's move-aside-to-.old was the right Windows-safe refinement of my naive partial-failure brief). Test bins must be live_adapt_* not live_update_* (os-740 UAC name-elevation). broker_socket_name is home_tag-namespaced → real-daemon ints isolate from the live daemon (safe to run; I ran both W3e ints myself, green).

INTERACTIONS: todlando (executor — disciplined; STOP-AND-FLAGged the process boundary; clean scope-audits after the W3c subagent-overstep). perri (found F-010×F-015 psyche-lock + the multi-subnet latent gap; validator-on-publish for both). deployah (cuts v0.13.2 next). BLOCKERS: none, clean state, W4 building.
</live-context>
<project-context>
spt-core DELTA since the snapshot (which had W3d-i gate-pass + W3d-ii building):

**W3 COMPLETE end-to-end — the v0.13.2 keystone (live daemon-coordinated adapter update, ADR-0025) is done + gated.** Chain: W3a+W3b @b49d4a0 · W3c @97ff869 · (a)+(c) @d6d7816 · W3d-i @b62476e · W3d-ii @322ca55 · W3e @92ee788.
- W3d-ii @322ca55 (broker cross-process apply, GATE-PASS): lock discipline (sessions Mutex only for snapshot/input-fetch/re-insert, never across terminate/swap/build_translation); partial-failure re-spawn-OLD (never translation-less); build_translation = pure verbatim extract from dispatch_spawn (zero semantic change); manifest re-read post-swap (flag-5); (c) simplification = translation-only cycle, NO psyche reap (psyche on .live-bin own-copy never locks install_dir).
- W3e @92ee788 (the NO-MOCKS keystone int, the SHIP-BLOCKER, GATE-PASS): I ran BOTH real-daemon ints myself — (A) perri's exact 4-step psyche-lock (own-copy → lock-free install-dir overwrite while psyche runs → kill brain-parent orphans → endpoint-stop reaps the brain-less orphan), (B) the translation-swap keystone (#[cfg(windows)] lock-regression pre-assert = vacuity guard; swap LANDS new bytes on the previously-LOCKED binary; brain-parity harness_alive+still_online). NO mocks (mock-session = fixture name), isolated tempdir SPT_HOME.
- ADR-0025 → ACCEPTED with a 3-correction amendment: (1) psyche is a SEPARATE install-dir binary, not in-daemon; (2) broker≠brain are separate PROCESSES so "re-clone into running BrainLifecycle" is impossible → reload-on-dirty (corrects my own W3 verdict caution-ii); (3) third axis = ephemeral-but-install-dir-locking → per-endpoint own-copy. REQ-ADAPTER-LIVE-UPDATE + REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP both [doc,impl,unit,int].

**v0.13.2 remaining:** W4 update-message (building) → W5 proof --dir → W6 `spt adapter version` (NEW operator ask: surface [adapter].version, one version not two; integration-checklist semi-mandate to keep it current) → deployah cuts. PR #29 (draft) drives CI both runners; final pre-release run = authoritative gate.

**v0.14.0 milestone DESIGNED + JIT'd** (the grill — operator scoped it as ONE milestone, v0.14.0, NOT a v0.13.3 fast-follow). Branch `design/endpoint-creation-flow` @7aab0fb: ADR-0026 (multi-subnet home @ endpoint-run creation, REQ-RUN-MULTISUBNET-HOME) + ADR-0027 (Unbound endpoint state + attach-on-session-exists, REQ-ENDPOINT-UNBOUND-ATTACH) + CONTEXT "Unbound endpoint" term + V0.14.0-ENDPOINT-CREATION-FLOW-JIT.md (6 waves: W1 STATUS_UNBOUND primitive → W2 home-resolve+skeleton-with-home [the multi-subnet unblock] → W3 two-level MRU lists → W4 attach-on-session → W5 Unbound display [hollow] → W6 int+perri regression). PREREQ: start from main AFTER v0.13.2 merges (builds on the broker sessions map W3a). KEY: the multi-subnet bug is a LATENT gap, NOT a regression (HOME_REFUSED established ≥0.11.0; exposed by the node crossing 1→2 subnets, BIGNET added). Home stays IMMUTABLE (ADR-0010); reuse the existing skeleton→live model (no hook/env change — bind inherits home via establish_perch prior-branch).

**perri = validator on publish** for both: v0.13.2 psyche-lock (real claude-spt re-run: update no longer bricks on a locked binary + stale orphan reaps on stop) AND v0.14.0 multi-subnet (her repro = the W6 regression test, MUST run on a multi-subnet node — single-subnet auto-homes + hides it). She declined the temp --subnet stopgap (multi-subnet-blocked by choice).

END GOAL: spt-core = harness-independent agent-ecosystem core (messaging, live-agent lifecycle, terminal hosting, P2P, runtime-manifest harness contract), clean-room rebuild of legacy owl. Near arc: v0.13.2 (adapter packaging + live update) → v0.14.0 (endpoint creation flow). Adapters (claude-spt via perri) consume published spt + docs.
</project-context>


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

<live-context>
Session-close commune (doyle, 2026-06-22). Resuming next session. Delta since the gen snapshot (which had W3d-i/ii + the grill-paused).

MY ROLE this session: drove v0.13.2 build→release as gater/orchestrator (operator: "run the build, todlando executes, you gate, deployah publishes, see it through"). Dispatch live agents via `$OWL send <target> doyle` (legacy owl — todlando/perri/deployah live THERE; NOT subagents, NOT spt-core endpoints — I mis-started with a subagent early and the operator corrected me). Gated every wave by reading the REAL diff + re-running gates myself (green-units ≠ green-real held all session).

IMMEDIATE NEXT STEPS (resume here):
1. Gate W4 (update-message) when todlando pings — markdown [update].message, only on an actual apply, not a no-op.
2. Gate W5 (`*-proof --dir/--manifest`, F-011) — unit + int.
3. Gate W6 (`spt adapter version <name>` — surfaces the EXISTING [adapter].version, NOT a [strings] dup; + integration-checklist semi-mandate to keep [adapter].version current since it also drives the gh_release ripple compare).
4. Verify FINAL CI green on BOTH runners on the v0.13.2 branch (PR #29) — the daemon/lifecycle suite can't run on this contended Windows box; CI is the authority.
5. deployah cuts v0.13.2: VERIFY the release counter vs spt-releases metadata (NOT memory — the v0.13.1 lesson); author the bump (workspace.package.version 0.13.1→0.13.2 + CHANGELOG + Cargo.lock first-party); push bump→CI-green-both→tag→sign+publish; ping perri with version+hashes.
6. AFTER v0.13.2 ships + merges to main: start v0.14.0 — rebase `design/endpoint-creation-flow` onto post-v0.13.2 main, mint impl/unit/int stages on the 2 REQs, dispatch todlando W1 (STATUS_UNBOUND).

LESSONS (worth keeping):
- The design-gate + incremental gating paid off TWICE: caught ADR-0025 step-3 unimplementable (BrainLifecycle is thread-local, IPC-unreachable) AND the broker≠brain PROCESS boundary (an Arc can't span processes — I'd SEEN the distinct PIDs clearing the build lock and missed it; todlando STOP-AND-FLAGged → reload-on-dirty). Always read the real diff.
- File-lock: a stale DEV daemon (target/debug/spt) locked the build artifact → scoped PID-tree kill of the DEV daemon only. Installed live daemon = AppData\Local\spt-core\bin, hosts wall-* — NEVER machine-wide kill.
- Windows: std::fs::rename FAILS over an existing file (todlando's move-aside-to-.old was the right Windows-safe refinement of my naive partial-failure brief). Test bins must be live_adapt_* not live_update_* (os-740 UAC name-elevation). broker_socket_name is home_tag-namespaced → real-daemon ints isolate from the live daemon (safe to run; I ran both W3e ints myself, green).

INTERACTIONS: todlando (executor — disciplined; STOP-AND-FLAGged the process boundary; clean scope-audits after the W3c subagent-overstep). perri (found F-010×F-015 psyche-lock + the multi-subnet latent gap; validator-on-publish for both). deployah (cuts v0.13.2 next). BLOCKERS: none, clean state, W4 building.
</live-context>
<project-context>
spt-core DELTA since the snapshot (which had W3d-i gate-pass + W3d-ii building):

**W3 COMPLETE end-to-end — the v0.13.2 keystone (live daemon-coordinated adapter update, ADR-0025) is done + gated.** Chain: W3a+W3b @b49d4a0 · W3c @97ff869 · (a)+(c) @d6d7816 · W3d-i @b62476e · W3d-ii @322ca55 · W3e @92ee788.
- W3d-ii @322ca55 (broker cross-process apply, GATE-PASS): lock discipline (sessions Mutex only for snapshot/input-fetch/re-insert, never across terminate/swap/build_translation); partial-failure re-spawn-OLD (never translation-less); build_translation = pure verbatim extract from dispatch_spawn (zero semantic change); manifest re-read post-swap (flag-5); (c) simplification = translation-only cycle, NO psyche reap (psyche on .live-bin own-copy never locks install_dir).
- W3e @92ee788 (the NO-MOCKS keystone int, the SHIP-BLOCKER, GATE-PASS): I ran BOTH real-daemon ints myself — (A) perri's exact 4-step psyche-lock (own-copy → lock-free install-dir overwrite while psyche runs → kill brain-parent orphans → endpoint-stop reaps the brain-less orphan), (B) the translation-swap keystone (#[cfg(windows)] lock-regression pre-assert = vacuity guard; swap LANDS new bytes on the previously-LOCKED binary; brain-parity harness_alive+still_online). NO mocks (mock-session = fixture name), isolated tempdir SPT_HOME.
- ADR-0025 → ACCEPTED with a 3-correction amendment: (1) psyche is a SEPARATE install-dir binary, not in-daemon; (2) broker≠brain are separate PROCESSES so "re-clone into running BrainLifecycle" is impossible → reload-on-dirty (corrects my own W3 verdict caution-ii); (3) third axis = ephemeral-but-install-dir-locking → per-endpoint own-copy. REQ-ADAPTER-LIVE-UPDATE + REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP both [doc,impl,unit,int].

**v0.13.2 remaining:** W4 update-message (building) → W5 proof --dir → W6 `spt adapter version` (NEW operator ask: surface [adapter].version, one version not two; integration-checklist semi-mandate to keep it current) → deployah cuts. PR #29 (draft) drives CI both runners; final pre-release run = authoritative gate.

**v0.14.0 milestone DESIGNED + JIT'd** (the grill — operator scoped it as ONE milestone, v0.14.0, NOT a v0.13.3 fast-follow). Branch `design/endpoint-creation-flow` @7aab0fb: ADR-0026 (multi-subnet home @ endpoint-run creation, REQ-RUN-MULTISUBNET-HOME) + ADR-0027 (Unbound endpoint state + attach-on-session-exists, REQ-ENDPOINT-UNBOUND-ATTACH) + CONTEXT "Unbound endpoint" term + V0.14.0-ENDPOINT-CREATION-FLOW-JIT.md (6 waves: W1 STATUS_UNBOUND primitive → W2 home-resolve+skeleton-with-home [the multi-subnet unblock] → W3 two-level MRU lists → W4 attach-on-session → W5 Unbound display [hollow] → W6 int+perri regression). PREREQ: start from main AFTER v0.13.2 merges (builds on the broker sessions map W3a). KEY: the multi-subnet bug is a LATENT gap, NOT a regression (HOME_REFUSED established ≥0.11.0; exposed by the node crossing 1→2 subnets, BIGNET added). Home stays IMMUTABLE (ADR-0010); reuse the existing skeleton→live model (no hook/env change — bind inherits home via establish_perch prior-branch).

**perri = validator on publish** for both: v0.13.2 psyche-lock (real claude-spt re-run: update no longer bricks on a locked binary + stale orphan reaps on stop) AND v0.14.0 multi-subnet (her repro = the W6 regression test, MUST run on a multi-subnet node — single-subnet auto-homes + hides it). She declined the temp --subnet stopgap (multi-subnet-blocked by choice).

END GOAL: spt-core = harness-independent agent-ecosystem core (messaging, live-agent lifecycle, terminal hosting, P2P, runtime-manifest harness contract), clean-room rebuild of legacy owl. Near arc: v0.13.2 (adapter packaging + live update) → v0.14.0 (endpoint creation flow). Adapters (claude-spt via perri) consume published spt + docs.
</project-context>


Commune queued — `.claude/doyle-commune.md` written (two-slice, both envelopes since in-project). Self listener ingests it next poll; wrapper deletes on success.

**Session close state:**
- **v0.13.2 keystone (W3) COMPLETE + gated** — ship-blocker cleared (both no-mocks ints pass, ADR-0025 accepted+amended). Remaining: W4 (building) → W5 → W6 → deployah cut.
- **v0.14.0 designed + JIT'd** on `design/endpoint-creation-flow` (ADR-0026/0027 + 2 REQs + 6-wave JIT), starts after v0.13.2 merges.
- **Resume next session at:** gate W4/W5/W6 → verify CI green both runners → deployah cuts v0.13.2 (verify counter vs spt-releases metadata) → ping perri → then kick off v0.14.0.

Clean state, no blockers. Reachable on `doyle`.