1	//! Runtime-manifest schema — the typed form of [`docs/MANIFEST.md`].
2	//!
3	//! A manifest declares only what *varies per harness*; command templates are
4	//! **opaque strings** spt-core never parses (MANIFEST §Principle). This module
5	//! is the parse+validate front door: [`Manifest::from_toml_str`] deserializes
6	//! the TOML and runs [`Manifest::validate`], returning a [`ManifestError`] with
7	//! a human-readable message instead of panicking on bad adapter input.
8	//!
9	//! Closed vocabularies (adapter kind, history strategy, env direction, inject
10	//! method, identity source, update avenue, shell broadcast/receipt) are modeled
11	//! as enums so an unknown variant is a parse error pointing at the bad value —
12	//! the "helpful errors for adapter authors" open sub-item (MANIFEST §Open).
13	//!
14	//! Scope (M2a/T1): every section is *declared and parsed*. Executing the
15	//! lifecycle sections (`[session.psyche_*]`, commune/signoff dirs, `[history]`
16	//! source, `[update]` conduct) is M2b/M3 — here they only round-trip.
17	//
18	// [impl->REQ-MANIFEST-1]
19
20	use schemars::JsonSchema;
21	use serde::{Deserialize, Serialize};
22	use std::collections::BTreeMap;
23	use std::fmt;
24
25	/// A fully-parsed runtime manifest. Top-level sections map 1:1 to the
26	/// `docs/MANIFEST.md` tables; all but `[adapter]` are optional so a minimal
27	/// adapter declares only its header.
28	#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
29	pub struct Manifest {
30	    pub adapter: Adapter,
31	    /// `[hooks.<event>]` — inbound hook table, keyed by harness event name.
32	    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
33	    pub hooks: BTreeMap<String, Hook>,
34	    /// `[session]` — watched-dir keys plus the `[session.<role>]` templates.
35	    #[serde(default, skip_serializing_if = "Session::is_empty")]
36	    pub session: Session,
37	    /// `[env.<VAR>]` — env-var inject/read table.
38	    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
39	    pub env: BTreeMap<String, EnvVar>,
40	    #[serde(default, skip_serializing_if = "Option::is_none")]
41	    pub history: Option<History>,
42	    /// `[digest]` — the adapter-declared session-digest extractor seam (ADR-0019).
43	    #[serde(default, skip_serializing_if = "Option::is_none")]
44	    pub digest: Option<Digest>,
45	    #[serde(default, skip_serializing_if = "Option::is_none")]
46	    pub inject: Option<Inject>,
47	    #[serde(default, skip_serializing_if = "Option::is_none")]
48	    pub identity: Option<Identity>,
49	    #[serde(default, skip_serializing_if = "Option::is_none")]
50	    pub update: Option<Update>,
51	    /// `[shell]` body — present iff `adapter.kind = "shell"` (validated).
52	    #[serde(default, skip_serializing_if = "Option::is_none")]
53	    pub shell: Option<Shell>,
54	    /// `[profiles.<name>]` — **shipped** profile overlays: sparse leaf-replace
55	    /// overlays declared by the adapter dev inside the parent manifest, updating
56	    /// as one unit with it. Stored raw ([`toml::Value`]); [`crate::profile::resolve`]
57	    /// merges one onto the base and re-validates the complete manifest. A bare
58	    /// `adapter_name` ignores these (parent unmodified); the composite
59	    /// `<adapter>:<profile>` selects one. **Local** (node-local, user-authored)
60	    /// profiles live beside the adapter in the registry, never here.
61	    /// (CONTEXT.md §adapter profile.)
62	    // [impl->REQ-MANIFEST-2]
63	    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
64	    #[schemars(with = "BTreeMap<String, serde_json::Value>")]
65	    pub profiles: BTreeMap<String, toml::Value>,
66	    /// `[strings]` — an adapter-authored KV tree of **opaque data** (spt-core
67	    /// never executes a string; command templates live in their own sections
68	    /// behind registration). Dot-path-readable via `spt adapter get-string`, and
69	    /// it rides the same leaf-replace profile overlay as the rest of the manifest
70	    /// (a shipped or local profile may override base strings). Node-local; no
71	    /// cross-node sync. (CONTEXT.md §adapter strings.)
72	    // [impl->REQ-MANIFEST-3]
73	    #[serde(default, skip_serializing_if = "toml::Table::is_empty")]
74	    #[schemars(with = "BTreeMap<String, serde_json::Value>")]
75	    pub strings: toml::Table,
76	    /// `[[hints]]` — once-per-session keyword hints (CONTEXT.md §keyword hints).
77	    /// **Order is significant** (first match wins). A profile overlays this by
78	    /// leaf-replace like any section — the array is replaced wholesale, never
79	    /// spliced (override/extend = re-declare).
80	    // [impl->REQ-MANIFEST-4]
81	    #[serde(default, skip_serializing_if = "Vec::is_empty")]
82	    pub hints: Vec<Hint>,
83	}
84
85	/// `[adapter]` — the manifest header, readable before any update (compat gate).
86	#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
87	pub struct Adapter {
88	    pub name: String,
89	    #[serde(default)]
90	    pub kind: AdapterKind,
91	    pub version: String,
92	    /// Lowest spt-core version this adapter tolerates (compat gate).
93	    pub min_spt_core_version: String,
94	    /// Endpoint types this adapter can host (`LiveAgent`, `Worker`, …).
95	    #[serde(default, skip_serializing_if = "Vec::is_empty")]
96	    pub hostable_types: Vec<String>,
97	    /// Optional basename for the `spt endpoint run` picker's `<basename>-<id>`
98	    /// launcher shortcut (REQ-MANIFEST-7). Absent ⇒ the harness-agnostic default
99	    /// `spt` (→ `spt-<id>`); an adapter sets this to brand its shortcuts
100	    /// (spt-claude-code → `cc`, giving `cc-<id>`). Additive + N-1-safe (omitted
101	    /// from serialization when absent). The picker reads it from the RESOLVED
102	    /// manifest of the selected adapter.
103	    #[serde(default, skip_serializing_if = "Option::is_none")]
104	    pub shortcut_basename: Option<String>,
105	}
106
107	/// The two adapter kinds. A `harness` hosts agents; a `shell` provides a driven
108	/// surface (MANIFEST §Shell adapters).
109	#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
110	#[serde(rename_all = "snake_case")]
111	pub enum AdapterKind {
112	    #[default]
113	    Harness,
114	    Shell,
115	}
116
117	/// `[hooks.<event>]` — one harness event → the `api` command it fires, the
118	/// stdin fields it maps, and whether it can surface context to the agent.
119	#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
120	pub struct Hook {
121	    /// Opaque `api …` command line the harness invokes for this event.
122	    pub fires: String,
123	    #[serde(default, skip_serializing_if = "Vec::is_empty")]
124	    pub reads: Vec<String>,
125	    /// Whether this hook can inject context (false ⇒ sentinel/relay fallback).
126	    #[serde(default)]
127	    pub can_inject: bool,
128	}
129
130	/// `[[hints]]` — one once-per-session keyword hint (CONTEXT.md §keyword hints).
131	/// The adapter's user-prompt hook pipes the full user message to `spt api hint`;
132	/// a matching keyword surfaces `text` to the agent's context channel, at most
133	/// once per session and once per message.
134	#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
135	pub struct Hint {
136	    /// Keywords that fire the hint — literal **case-insensitive substrings** by
137	    /// default; compiled as **regex** patterns when `regex = true`.
138	    #[serde(default, skip_serializing_if = "Vec::is_empty")]
139	    pub keywords: Vec<String>,
140	    /// The hint text surfaced when a keyword matches.
141	    pub text: String,
142	    /// Treat `keywords` as regex patterns instead of literal substrings.
143	    #[serde(default)]
144	    pub regex: bool,
145	}
146
147	impl Hint {
148	    /// The first keyword that fires against `message` (literal case-insensitive
149	    /// substring, or regex when `regex = true`); `None` if none match. An invalid
150	    /// regex never matches — a bad pattern silences its own hint, never panics
151	    /// (best-effort, the opaque-template stance).
152	    // [impl->REQ-MANIFEST-4]

... [1281 lines truncated] ...


1	//! The brain's live-agent Psyche host (M11-W0.1, REQ-DAEMON-1) — re-host the
2	//! daemon-hosted LiveAgent lifecycle from disk and reconcile the host-set.
3	//!
4	//! This is the production caller `run_pulse_loop` lacked: the brain instantiates
5	//! a [`BrainLifecycle`] per hosted **live** endpoint and drives its config-paced
6	//! pulse loop on a supervised thread. It is the **harness-endpoint mirror** of
7	//! [`crate::shellwake::reconcile_once`] (which re-hosts offline shells from
8	//! disk): same shape — enumerate Self perches, resolve each manifest from its
9	//! `info.json` `adapter` option, hold a stop flag per supervised thread, and run
10	//! one bounded reconcile sweep at boot and every tick. Where the shell waker
11	//! hosts **offline** shells, this hosts **online** live endpoints — `status` is
12	//! daemon-authoritative liveness (CONTEXT:194 / KH 2.5), so re-hosting the
13	//! online ones re-hosts exactly what was live before a brain (re)start, and
14	//! never resurrects a gracefully-departed (offline) agent.
15	//!
16	//! **Online-only is staged behind the interim.** The interim `spt api listen`
17	//! process still first-hosts its live agents today; those perches are
18	//! `status`-ABSENT (not "online"), so this scan ignores them — no double-host in
19	//! the W0.1→W0.2 window. W0.2 retires the interim and makes the brain the
20	//! first-host (it sets `status=online` on a live `listen`/`bind`); from then this
21	//! reconcile reacts within one tick, the same way the shell waker reacts to a
22	//! CLI-process status flip. The full bind→online→brain-restart→re-host arc is
23	//! proven through the real brain process by the W0.4 production int.
24	// [impl->REQ-DAEMON-1]
25
26	use std::collections::HashMap;
27	use std::path::Path;
28	use std::sync::atomic::{AtomicBool, Ordering};
29	use std::sync::{Arc, Mutex};
30	use std::thread::JoinHandle;
31	use std::time::Duration;
32
33	use spt_runtime::registry::AdapterRecord;
34	use spt_runtime::Manifest;
35	use spt_store::liveness::STATUS_ONLINE;
36	use spt_store::perch::{self, ParentHint};
37
38	use crate::brainproc::StartReason;
39	use crate::config::DaemonConfig;
40	use crate::lifecycle::BrainLifecycle;
41
42	/// The `InfoJson::state` tag of a live agent — only these get a daemon-hosted
43	/// Psyche; a `ready_agent` (messaging-only) never does.
44	const LIVE_AGENT_STATE: &str = "live_agent";
45
46	/// The reconcile cadence (ms): a lifecycle flip lands in a CLI process, so the
47	/// daemon loop is the invariant holder — a flip is at most one tick stale
48	/// (mirrors [`crate::shellwake::RECONCILE_INTERVAL_MS`]).
49	pub const LIVE_RECONCILE_INTERVAL_MS: u64 = 5_000;
50
51	/// One brain-hosted live lifecycle: its config-paced pulse-driver thread plus the
52	/// held stop flag (tripped to un-host on an offline-transition).
53	struct HostedLife {
54	    stop: Arc<AtomicBool>,
55	    thread: JoinHandle<()>,
56	}
57
58	/// The brain's hosted live-endpoint table, keyed by endpoint id. Shared between
59	/// the reconcile loop and anything that must stop a host. Mirror of
60	/// [`crate::shellwake::WakeSet`].
61	#[derive(Default)]
62	pub struct LiveSet {
63	    inner: Mutex<HashMap<String, HostedLife>>,
64	}
65
66	impl LiveSet {
67	    pub fn new() -> Self {
68	        Self::default()
69	    }
70
71	    /// How many lifecycles are hosted (finished driver threads pruned first).
72	    pub fn len(&self) -> usize {
73	        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
74	        map.retain(|_, h| !h.thread.is_finished());
75	        map.len()
76	    }
77
78	    pub fn is_empty(&self) -> bool {
79	        self.len() == 0
80	    }
81
82	    /// Is `id` currently hosted (driver thread alive)? Prunes a finished entry so
83	    /// a crashed driver is re-hosted next sweep.
84	    fn contains(&self, id: &str) -> bool {
85	        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
86	        match map.get(id) {
87	            Some(h) if !h.thread.is_finished() => true,
88	            Some(_) => {
89	                map.remove(id);
90	                false
91	            }
92	            None => false,
93	        }
94	    }
95
96	    fn insert(&self, id: &str, handle: HostedLife) {
97	        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
98	        map.insert(id.to_string(), handle);
99	    }
100
101	    /// The currently-tabled ids (finished pruned) — the stop-side reconcile input.
102	    fn ids(&self) -> Vec<String> {
103	        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
104	        map.retain(|_, h| !h.thread.is_finished());
105	        map.keys().cloned().collect()
106	    }
107
108	    /// Un-host one lifecycle: trip its stop flag, join the driver, de-table. The
109	    /// pulse loop checks the flag each sleep-slice, so the join is prompt. We do
110	    /// NOT re-stamp `offline` — the perch already left the online set (graceful
111	    /// signoff cleared it, or the perch is gone); a brain *crash* deliberately
112	    /// leaves `status=online` so the next brain rehydrates it (CONTEXT:194).
113	    fn stop_host(&self, id: &str) {
114	        let handle = {
115	            let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
116	            map.remove(id)
117	        };
118	        if let Some(h) = handle {
119	            h.stop.store(true, Ordering::SeqCst);
120	            let _ = h.thread.join();
121	        }
122	    }
123	}
124
125	/// One reconcile sweep — the brain's live host-set invariant holder, run at boot
126	/// and every [`LIVE_RECONCILE_INTERVAL_MS`]. The complete set-diff against the
127	/// on-disk **online** live endpoints (mirror of [`crate::shellwake::reconcile_once`]):
128	///
129	/// - **un-host** (tabled ∧ ¬online) — stop the driver + de-table (exit guard: a
130	///   signed-off / removed endpoint must not keep pulsing — thread + dead-agent leak).
131	/// - **host** (online ∧ ¬tabled) — spawn the Psyche + start the driver.
132	/// - **leave-alone** (online ∧ tabled).
133	///
134	/// `status` is daemon-authoritative (CONTEXT:194); interim `listen` perches are
135	/// `status`-ABSENT so they are never hosted here (no double-host) until W0.2.
136	// [impl->REQ-DAEMON-1]
137	pub fn reconcile_once(
138	    owlery: &Path,
139	    registered: &[(AdapterRecord, Manifest)],
140	    adapters_dir: &Path,
141	    set: &Arc<LiveSet>,
142	    cfg: &DaemonConfig,
143	    reason: StartReason,
144	) {
145	    // Stop side first: hosted endpoints that DEFINITIVELY left the online set —
146	    // trip the flag, join, de-table. Un-host on a definitive departure ONLY, never
147	    // on a TRANSIENT read failure (M11-W0.4 (ii)): a momentarily-unreadable
148	    // info.json must not stop+respawn the driver (Psyche churn on a disk blip). The
149	    // discriminator is the perch DIR:
150	    //   - dir gone        → torn down (teardown removed the perch) → un-host;
151	    //   - dir present, read_info Some(status≠online) → signed-off/offline → un-host;
152	    //   - dir present, read_info None (transient/torn read) → LEAVE the driver, retry.
153	    // [impl->REQ-DAEMON-1]
154	    for id in set.ids() {
155	        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);
156	        let unhost = if !perch.exists() {
157	            true // the perch dir is gone — torn down
158	        } else {
159	            match spt_store::info::read_info(&perch) {
160	                Some(info) => info.status.as_deref() != Some(STATUS_ONLINE),
161	                None => false, // dir present but unreadable: transient → keep hosting
162	            }
163	        };
164	        if unhost {
165	            set.stop_host(&id);
166	        }
167	    }
168
169	    // Start side: every online live endpoint not yet hosted.
170	    for id in perch::list_self_perch_ids(owlery) {
171	        if set.contains(&id) {
172	            continue; // already hosted (leave-alone)
173	        }
174	        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);
175	        let Some(info) = spt_store::info::read_info(&perch) else {
176	            continue;
177	        };

... [376 lines truncated] ...


Both files confirmed intact post-linter. **PR #16 = MERGED** (GitHub recognized the merge). Now ping the gater (doyle) with the reconciled tree + gate results, then update memory:


Bash

cat <<'EOF' | "$OWL" send doyle
todlando — v0.8.0 UNIFIED MERGE DONE + PUSHED. origin/main = c3bdccf (c433793..c3bdccf). PR #16 auto-closed MERGED. Reconciled deliberately, not a naive merge.

RECONCILIATION (per your GO):
- PSYCHE MODEL: daemon-host WINS. Dropped main's interim in-process spawn_psyche from api/startup.rs → PSYCHE_SPAWNED grep-ZERO. Dropped cmd_listen's now-unused install_dir param + mod.rs call-site arg.
- FEATURE B REAPPLIED at the daemon site (the silent-drop fix): livehost.rs builds BrainLifecycle::with_config_in(&manifest,&id,cfg, Some(source_dir)) — source_dir = the matching AdapterRecord in the `registered` slice (parent-matched). Psyche role program now resolves vs the install dir, not PATH-only. [impl->REQ-INSTALL-11] re-tagged at livehost; digest-site INSTALL-11 tags kept.
- FEATURE E kept (resolve_ctx_manifest present) → HOW_TO_LIVE "(v0.8.0+) --adapter resolves manifest" line stays true.
- manifest.rs test conflict: kept BOTH update_avenue_gh_release_fields (UPD-9) + shell_tunnel_opt_in (SHELL-4).
- CONTEXT.md psyche phrasing fixed: "api/psyche seam" → daemon-host source_dir resolution.

GATE (a-h) GREEN LOCALLY:
a. traceable EXIT0
b. clippy --workspace --all-targets -D warnings clean
c. live_firsthost_e2e + live_bind_firsthost_e2e GREEN (daemon-hosted psyche spawns + reconciles on the merged tree)
d. PSYCHE_SPAWNED grep-zero; marker = LIVEHOST_PSYCHE
e. Feature B digest resolution intact (digest.rs/spt-live/digest.rs tags present, merged clean — M11 didn't touch)
f. Feature E present (resolve_ctx_manifest in mod.rs)
g. xtask check clean
h. no NEW doc leaks from the merge (reference.md REQ-refs + doc-tags are pre-existing main state)
i. → CI now running on c3bdccf both runners (your call to confirm green).

Over to you: run your gate (a-i), confirm CI green both runners → deployah v0.8.0 (counter 16, operator-gated at publish). Holler if the gate finds anything.
EOF
echo "EXIT=$?"

Ping doyle with reconciled tree + gate results


➜ SENT:doyle
EXIT=0


Sent (`SENT:doyle`). Updating memory to mark the reconciliation done:


Edit

**BLOCKED — cannot execute yet:** main's Feature B IMPL is on doyle's **LOCAL UNPUSHED main**. origin/main HEAD e6722b8 = `V074-PLAN.md` only (plan commit) + the OLD baseline `spawn_psyche`/`PSYCHE_SPAWNED` (no install-dir resolution, no `with_config_in`). Nothing to merge against until doyle pushes Feature B. Then: unified merge + reconcile per above + gate + deployah bumps 0.8.0/CHANGELOG/tag/sign/publish (operator-gated). Wait for doyle merge-ordering ping ("(next msg)" go-signal). Do NOT merge solo.

**DONE @c3bdccf — unified merge PUSHED to main 2026-06-16.** doyle pushed Feature B/A/E/docs (origin/main c433793) + gave merge-ordering GO. Merged PR #16 (m11 672b928) INTO main as v080-merge (first-parent=main), reconciled per the recipe above (all 6 steps), pushed `git push origin HEAD:main` (c433793..c3bdccf). PR #16 auto-closed MERGED. Executed exactly as planned: dropped interim startup.rs spawn (PSYCHE_SPAWNED grep-zero) + its install_dir param + mod.rs call-site arg; reapplied Feature B at livehost.rs via `with_config_in(..., Some(source_dir))` matching the AdapterRecord by parent name in the `registered` slice + `[impl->REQ-INSTALL-11]` re-tag; kept both manifest.rs tests; fixed CONTEXT.md psyche phrasing. **GATE a-h GREEN locally** (traceable EXIT0, clippy --workspace -D warnings clean, live_firsthost_e2e + live_bind_firsthost_e2e green, PSYCHE_SPAWNED zero, digest intact, Feature E present, xtask check clean, no new doc leaks). Pinged doyle. NEXT: doyle runs gate a-i + confirms CI green BOTH runners on c3bdccf → deployah cuts v0.8.0 (counter 16, operator-gated at publish). HOLDING for doyle gate result / any merge bug.

LESSON: the silent-drop was REAL and surfaced exactly where predicted — the M11-only file (livehost.rs) did NOT conflict, so a textual merge kept `with_config` (install_dir=None) and would have shipped a daemon-hosted Psyche that resolves only via PATH. Also the `cmd_listen` signature silently kept main's `install_dir` param (git auto-resolved delete-vs-keep to keep) → would've been an unused-param clippy fail. Both caught by reading the diff + grounding in the actual code, not trusting auto-merge.


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

**DONE @c3bdccf — unified merge PUSHED to main 2026-06-16.** doyle pushed Feature B/A/E/docs (origin/main c433793) + gave merge-ordering GO. Merged PR #16 (m11 672b928) INTO main as v080-merge (first-parent=main), reconciled per the recipe above (all 6 steps), pushed `git push origin HEAD:main` (c433793..c3bdccf). PR #16 auto-closed MERGED. Executed exactly as planned: dropped interim startup.rs spawn (PSYCHE_SPAWNED grep-zero) + its install_dir param + mod.rs call-site arg; reapplied Feature B at livehost.rs via `with_config_in(..., Some(source_dir))` matching the AdapterRecord by parent name in the `registered` slice + `[impl->REQ-INSTALL-11]` re-tag; kept both manifest.rs tests; fixed CONTEXT.md psyche phrasing. **GATE a-h GREEN locally** (traceable EXIT0, clippy --workspace -D warnings clean, live_firsthost_e2e + live_bind_firsthost_e2e green, PSYCHE_SPAWNED zero, digest intact, Feature E present, xtask check clean, no new doc leaks). Pinged doyle. NEXT: doyle runs gate a-i + confirms CI green BOTH runners on c3bdccf → deployah cuts v0.8.0 (counter 16, operator-gated at publish). HOLDING for doyle gate result / any merge bug.

LESSON: the silent-drop was REAL and surfaced exactly where predicted — the M11-only file (livehost.rs) did NOT conflict, so a textual merge kept `with_config` (install_dir=None) and would have shipped a daemon-hosted Psyche that resolves only via PATH. Also the `cmd_listen` signature silently kept main's `install_dir` param (git auto-resolved delete-vs-keep to keep) → would've been an unused-param clippy fail. Both caught by reading the diff + grounding in the actual code, not trusting auto-merge.

---
name: v080-merge-reconciliation
description: "v0.8.0 unified merge is NON-clean — live psyche model diverged (M11 daemon-host vs main Feature B in-process install-dir resolution); naive merge silently DROPS Feature B. The deliberate reconciliation recipe + what's verified + what blocks."
metadata:
  node_type: memory
  type: project
  originSessionId: 8350d43e-fe2d-4e24-9999-eecbcc6d3a50
---

**v0.8.0 = unified merge** (counter 16, minor): M11 (PR #16, daemon-host psyche) + main v0.7.4 (Feature B REQ-INSTALL-11 adapter-binary install-dir resolution + gh_release avenue + Feature E REQ-API-4 `resolve_ctx_manifest`) + how-to-live @672b928 + api ergonomics fix. See [[f007-live-relay-acceptance]], [[m11-complete]].

**⚠ NON-CLEAN MERGE — naive textual merge silently DROPS Feature B** (todlando-psyche flagged + I verified the divergence). The live-agent PSYCHE MODEL diverged between M11 and main:
- **M11 (my branch, WINS):** psyche is DAEMON-HOSTED. `api/startup.rs` just marks perch online; `livehost.rs:191` builds `BrainLifecycle::with_config(&manifest,&id,cfg)`, `host_one:203` → `spawn_psyche:211` (`LIVEHOST_PSYCHE:{id}`). `PSYCHE_SPAWNED` is GONE.
- **main (Feature B, REQ-INSTALL-11):** INTERIM in-process spawn at `api/startup.rs:288-294` (`PSYCHE_SPAWNED`) resolves the psyche program vs the install dir via `BrainLifecycle::with_config_in(manifest,id,cfg,install_dir)` + `ManifestRuntime::with_install_dir` (lifecycle.rs).

**RECONCILIATION (daemon-host model WINS; REAPPLY Feature B onto it):**
1. Take M11's daemon-host psyche; DROP main's interim in-process `spawn_psyche` from `api/startup.rs` (superseded). Confirm `PSYCHE_SPAWNED` stays GONE in merged tree.
2. **REAPPLY Feature B at the daemon site:** `livehost.rs:191` → `BrainLifecycle::with_config_in(&manifest,&id,cfg, Some(<source_dir>))` not `with_config(...)`. `<source_dir>` = the registry record's `source_dir` — grab the matching `AdapterRecord` in the `registered: &[(AdapterRecord,Manifest)]` slice on `adapter` (`resolve_option_in` gives the manifest; the record carries `source_dir`). PRECISE (source_dir, not a --manifest-parent) — fully closes the copy-mode psyche edge.
3. Bring main's `lifecycle.rs` Feature B seam (`with_config_in` + the `install_dir` field + `ManifestRuntime::with_install_dir`) into the merged `lifecycle.rs` alongside the daemon-host structure.
4. Feature B `digest.rs` / `spt-live/digest.rs` / `cli.rs` digest-proof resolution → should merge clean (confirm M11 didn't touch digest extraction).
5. Feature E (REQ-API-4, `mod.rs run()` → `resolve_ctx_manifest`) → main wins (additive; M11 loads only from `--manifest`). Reconcile `mod.rs`. **This is the "api ergonomics fix" → makes HOW_TO_LIVE's "(v0.8.0+) `--adapter` resolves the registered manifest" TRUE.** If Feature E does NOT land, that HOW_TO_LIVE claim is wrong — fix the doc.
6. CONTEXT.md/MANIFEST install-dir doc note references the psyche resolution against the "api/psyche seam" (main-interim phrasing) — update to the daemon-host `source_dir` resolution. (todlando commits main docs as-is; I adjust the psyche phrasing in the merge.)

**TRACEABLE caveat:** REQ-INSTALL-11 psyche evidence MOVES from `api/startup.rs` (deleted interim) to `livehost`. Re-tag `[impl/int->REQ-INSTALL-11]` at the daemon site or traceable goes RED. GATE = traceable EXIT0 (REQ-INSTALL-11 @ livehost, REQ-API-4, REQ-…**TRUNCATED at part 8/9 — get full gate from coordinator**) + full suite + workspace-clippy.

**VERIFIED (branch side, 2026-06-16):** livehost.rs:191 = `with_config`; `AdapterRecord.source_dir` exists (registry.rs:57) in the `registered` slice → the reapply is feasible & precise. `PSYCHE_SPAWNED` zero occurrences in M11 tree.

**BLOCKED — cannot execute yet:** main's Feature B IMPL is on doyle's **LOCAL UNPUSHED main**. origin/main HEAD e6722b8 = `V074-PLAN.md` only (plan commit) + the OLD baseline `spawn_psyche`/`PSYCHE_SPAWNED` (no install-dir resolution, no `with_config_in`). Nothing to merge against until doyle pushes Feature B. Then: unified merge + reconcile per above + gate + deployah bumps 0.8.0/CHANGELOG/tag/sign/publish (operator-gated). Wait for doyle merge-ordering ping ("(next msg)" go-signal). Do NOT merge solo.

-**BLOCKED — cannot execute yet:** main's Feature B IMPL is on doyle's **LOCAL UNPUSHED main**. origin/main HEAD e6722b8 = `V074-PLAN.md` only (plan commit) + the OLD baseline `spawn_psyche`/`PSYCHE_SPAWNED` (no install-dir resolution, no `with_config_in`). Nothing to merge against until doyle pushes Feature B. Then: unified merge + reconcile per above + gate + deployah bumps 0.8.0/CHANGELOG/tag/sign/publish (operator-gated). Wait for doyle merge-ordering ping ("(next msg)" go-signal). Do NOT merge solo.

+**DONE @c3bdccf — unified merge PUSHED to main 2026-06-16.** doyle pushed Feature B/A/E/docs (origin/main c433793) + gave merge-ordering GO. Merged PR #16 (m11 672b928) INTO main as v080-merge (first-parent=main), reconciled per the recipe above (all 6 steps), pushed `git push origin HEAD:main` (c433793..c3bdccf). PR #16 auto-closed MERGED. Executed exactly as planned: dropped interim startup.rs spawn (PSYCHE_SPAWNED grep-zero) + its install_dir param + mod.rs call-site arg; reapplied Feature B at livehost.rs via `with_config_in(..., Some(source_dir))` matching the AdapterRecord by parent name in the `registered` slice + `[impl->REQ-INSTALL-11]` re-tag; kept both manifest.rs tests; fixed CONTEXT.md psyche phrasing. **GATE a-h GREEN locally** (traceable EXIT0, clippy --workspace -D warnings clean, live_firsthost_e2e + live_bind_firsthost_e2e green, PSYCHE_SPAWNED zero, digest intact, Feature E present, xtask check clean, no new doc leaks). Pinged doyle. NEXT: doyle runs gate a-i + confirms CI green BOTH runners on c3bdccf → deployah cuts v0.8.0 (counter 16, operator-gated at publish). HOLDING for doyle gate result / any merge bug.

+LESSON: the silent-drop was REAL and surfaced exactly where predicted — the M11-only file (livehost.rs) did NOT conflict, so a textual merge kept `with_config` (install_dir=None) and would have shipped a daemon-hosted Psyche that resolves only via PATH. Also the `cmd_listen` signature silently kept main's `install_dir` param (git auto-resolved delete-vs-keep to keep) → would've been an unused-param clippy fail. Both caught by reading the diff + grounding in the actual code, not trusting auto-merge.


**v0.8.0 unified merge DONE + pushed to main.** `c433793..c3bdccf`. PR #16 auto-closed MERGED.

Reconciled deliberately (not naive — the silent-drop was real, surfaced exactly where predicted):

**Reconciliation:**
- **Psyche model:** daemon-host wins. Dropped main's interim in-process `spawn_psyche` (`PSYCHE_SPAWNED` grep-zero) + `cmd_listen`'s now-unused `install_dir` param + the mod.rs call-site arg.
- **Feature B reapplied at the silent-drop site** — `livehost.rs` now builds `with_config_in(..., Some(source_dir))` (matched `AdapterRecord` in the `registered` slice). The M11-only file didn't conflict, so a textual merge would've kept `install_dir=None` → daemon Psyche resolving via PATH only. Re-tagged `[impl->REQ-INSTALL-11]` there.
- **Feature E kept** → HOW_TO_LIVE's "(v0.8.0+) `--adapter` resolves manifest" stays true.
- `manifest.rs`: kept both tests (UPD-9 + SHELL-4). `CONTEXT.md` psyche phrasing fixed.

**Gates a–h green locally:** traceable EXIT0 · clippy `--workspace --all-targets -D warnings` clean · `live_firsthost_e2e` + `live_bind_firsthost_e2e` green · PSYCHE_SPAWNED zero · digest intact · Feature E present · xtask check clean · no new doc leaks. Gate i (full suite, both runners) now running on CI.

Pinged doyle with the tree + results. **HOLDING** for doyle's gate run + CI-green-both-runners confirmation → deployah cuts v0.8.0 (counter 16, operator-gated at publish).


<task-notification>
<task-id>be1did0ug</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/3" id="968a7e80" type="msg" from="todlando">GATE a-h PASS — independently verified on the merged tree c3bdccf (pulled it, ran them myself):
a traceable EXIT0 · b clippy --workspace --all-targets -D warnings clean · c live_firsthost_e2e + live_bind_firsthost_e2e BOTH green (daemon-host psyche spawns on the merged model) · d PSYCHE_SPAWNED grep-zero (mar</EVENT-PART>
<EVENT-PART seq="2/3" id="968a7e80">ker=LIVEHOST_PSYCHE) · e Feature B digest tags intact · f Feature E resolve_ctx_manifest present · g xtask check OK · h no leaks. Reconciliation is correct — I read livehost.rs:191-203: with_config_in + precise source_dir from the matched record, [impl-&gt;REQ-INSTALL-11] re-tagged. Excellent deliberate merge.

(i) CI: the FEATURES</EVENT-PART>
<EVENT-PART seq="3/3" id="968a7e80">-on-main (c433793) already completed CI SUCCESS both runners (27612622143) — features are solid. The MERGED tree c3bdccf (27613218507) is QUEUED. PING me when 27613218507 finishes both runners (you're watching) — on green, deployah cuts v0.8.0. If it flakes, rerun --failed + tell me.</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.


doyle independently verified **gate a–h PASS** on c3bdccf. Reconciliation confirmed correct. Features-on-main CI already green both runners; the merged-tree run **27613218507** is QUEUED. My job: watch it, ping doyle on green (or rerun --failed on flake).

Checking status + launching a background watch:


PowerShell

gh run view 27613218507 --json status,conclusion,displayTitle,headSha -q '.displayTitle + " | sha=" + .headSha[0:7] + " | status=" + .status + " conclusion=" + (.conclusion // "pending")' 2>&1 | Select-Object -First 3

Check merged-tree CI run status


merge: M11 shell-substrate (PR #16) into v0.7.4 main — unified v0.8.0 | sha=c3bdccf | status=in_progress conclusion=


Run 27613218507 in_progress on c3bdccf. Launching a background watch — it'll re-invoke me when CI completes:


gh run watch 27613218507 --exit-status > $env:TEMP/ci-27613218507.log 2>&1; Write-Output "WATCH_DONE exit=$LASTEXITCODE"; gh run view 27613218507 --json status,conclusion -q '.status + " " + (.conclusion // "none")'

Watch merged-tree CI to completion (background)


Command running in background with ID: bipqvex29. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\30df62c3-0b22-4b14-88de-fef422a5554f\tasks\bipqvex29.output. You will be notified when it completes. To check interim output, use Read on that file path.


Background watch launched (`bipqvex29`, `--exit-status`) — I'll be notified when CI completes.

State: gate a–h independently verified PASS by doyle. Features-on-main already CI-green both runners. Merged tree c3bdccf (run 27613218507) in_progress — watching. On green → ping doyle → deployah cuts v0.8.0. On flake → `gh run rerun --failed` + tell doyle.

Holding for CI.


<task-notification>
<task-id>bipqvex29</task-id>
<tool-use-id>toolu_013qjr5xr2mpTXuWytzy9DL5</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\30df62c3-0b22-4b14-88de-fef422a5554f\tasks\bipqvex29.output</output-file>
<status>completed</status>
<summary>Background command "Watch merged-tree CI to completion (background)" completed (exit code 0)</summary>
</task-notification>


Watch exited 0 (success). Verifying both runners concluded green before pinging:


Write-Output "=== run conclusion ==="; gh run view 27613218507 --json status,conclusion -q '.status + " / " + (.conclusion // "none")'; Write-Output "=== per-job (both runners) ==="; gh run view 27613218507 --json jobs -q '.jobs[] | .name + " => " + .conclusion'

Confirm both-runner CI conclusion


=== run conclusion ===
completed / success
=== per-job (both runners) ===
test (self-hosted, Linux, kitsubito) => success
n1-gate (self-hosted, Windows, hfenduleam) => success
traceability => success
n1-gate (self-hosted, Linux, kitsubito) => success
test (self-hosted, Windows, hfenduleam) => success
twohost-b => skipped
twohost-a => skipped


CI **SUCCESS** on c3bdccf, both runners green (kitsubito Linux + hfenduleam Windows: test + n1-gate + traceability all success; twohost skipped — expected, merge commit isn't `[twohost]`-tagged; rig already proven on W5). Pinging doyle:


Bash

cat <<'EOF' | "$OWL" send doyle
todlando — CI GREEN on the merged tree. Run 27613218507 (c3bdccf) = completed/SUCCESS, BOTH runners:
- test (Linux kitsubito) ✓ · test (Windows hfenduleam) ✓
- n1-gate both runners ✓ · traceability ✓
- twohost-a/b SKIPPED (expected: merge commit isn't [twohost]-tagged; the rig was already proven green on W5 27608026651).

Gate i CLEARED. Full gate a-i PASS on c3bdccf. No flake, no rerun needed.

GO for deployah: cut v0.8.0 (counter 16, minor bump) — workspace.package.version 0.7.x→0.8.0 + CHANGELOG section (release-body) + Cargo.lock first-party lines + tag + sign + publish. Operator-gated at publish. Standing by if anything bounces.
EOF
echo "EXIT=$?"

Ping doyle: CI green both runners, go for v0.8.0


Sent (`SENT:doyle`). Updating memory — merge integration fully gated, handed to deployah:


Edit

Pinged doyle. NEXT: doyle runs gate a-i + confirms CI green BOTH runners on c3bdccf → deployah cuts v0.8.0 (counter 16, operator-gated at publish). HOLDING for doyle gate result / any merge bug.

Pinged doyle. **doyle independently re-ran gate a-h on c3bdccf = PASS (verified livehost.rs:191-203 with_config_in + source_dir himself). CI 27613218507 (c3bdccf) = SUCCESS BOTH runners** (test+n1-gate kitsubito/hfenduleam + traceability; twohost skipped = merge commit not [twohost]-tagged, rig already proven W5 27608026651). Gate i cleared → full gate a-i PASS. Handed GO to deployah to cut v0.8.0 (counter 16, minor bump, operator-gated at publish). My reconciliation leg COMPLETE. Remaining = deployah's publish leg + operator publish-gate.


---
name: v080-merge-reconciliation
description: "v0.8.0 unified merge is NON-clean — live psyche model diverged (M11 daemon-host vs main Feature B in-process install-dir resolution); naive merge silently DROPS Feature B. The deliberate reconciliation recipe + what's verified + what blocks."
metadata:
  node_type: memory
  type: project
  originSessionId: 8350d43e-fe2d-4e24-9999-eecbcc6d3a50
---

**v0.8.0 = unified merge** (counter 16, minor): M11 (PR #16, daemon-host psyche) + main v0.7.4 (Feature B REQ-INSTALL-11 adapter-binary install-dir resolution + gh_release avenue + Feature E REQ-API-4 `resolve_ctx_manifest`) + how-to-live @672b928 + api ergonomics fix. See [[f007-live-relay-acceptance]], [[m11-complete]].

**⚠ NON-CLEAN MERGE — naive textual merge silently DROPS Feature B** (todlando-psyche flagged + I verified the divergence). The live-agent PSYCHE MODEL diverged between M11 and main:
- **M11 (my branch, WINS):** psyche is DAEMON-HOSTED. `api/startup.rs` just marks perch online; `livehost.rs:191` builds `BrainLifecycle::with_config(&manifest,&id,cfg)`, `host_one:203` → `spawn_psyche:211` (`LIVEHOST_PSYCHE:{id}`). `PSYCHE_SPAWNED` is GONE.
- **main (Feature B, REQ-INSTALL-11):** INTERIM in-process spawn at `api/startup.rs:288-294` (`PSYCHE_SPAWNED`) resolves the psyche program vs the install dir via `BrainLifecycle::with_config_in(manifest,id,cfg,install_dir)` + `ManifestRuntime::with_install_dir` (lifecycle.rs).

**RECONCILIATION (daemon-host model WINS; REAPPLY Feature B onto it):**
1. Take M11's daemon-host psyche; DROP main's interim in-process `spawn_psyche` from `api/startup.rs` (superseded). Confirm `PSYCHE_SPAWNED` stays GONE in merged tree.
2. **REAPPLY Feature B at the daemon site:** `livehost.rs:191` → `BrainLifecycle::with_config_in(&manifest,&id,cfg, Some(<source_dir>))` not `with_config(...)`. `<source_dir>` = the registry record's `source_dir` — grab the matching `AdapterRecord` in the `registered: &[(AdapterRecord,Manifest)]` slice on `adapter` (`resolve_option_in` gives the manifest; the record carries `source_dir`). PRECISE (source_dir, not a --manifest-parent) — fully closes the copy-mode psyche edge.
3. Bring main's `lifecycle.rs` Feature B seam (`with_config_in` + the `install_dir` field + `ManifestRuntime::with_install_dir`) into the merged `lifecycle.rs` alongside the daemon-host structure.
4. Feature B `digest.rs` / `spt-live/digest.rs` / `cli.rs` digest-proof resolution → should merge clean (confirm M11 didn't touch digest extraction).
5. Feature E (REQ-API-4, `mod.rs run()` → `resolve_ctx_manifest`) → main wins (additive; M11 loads only from `--manifest`). Reconcile `mod.rs`. **This is the "api ergonomics fix" → makes HOW_TO_LIVE's "(v0.8.0+) `--adapter` resolves the registered manifest" TRUE.** If Feature E does NOT land, that HOW_TO_LIVE claim is wrong — fix the doc.
6. CONTEXT.md/MANIFEST install-dir doc note references the psyche resolution against the "api/psyche seam" (main-interim phrasing) — update to the daemon-host `source_dir` resolution. (todlando commits main docs as-is; I adjust the psyche phrasing in the merge.)

**TRACEABLE caveat:** REQ-INSTALL-11 psyche evidence MOVES from `api/startup.rs` (deleted interim) to `livehost`. Re-tag `[impl/int->REQ-INSTALL-11]` at the daemon site or traceable goes RED. GATE = traceable EXIT0 (REQ-INSTALL-11 @ livehost, REQ-API-4, REQ-…**TRUNCATED at part 8/9 — get full gate from coordinator**) + full suite + workspace-clippy.

**VERIFIED (branch side, 2026-06-16):** livehost.rs:191 = `with_config`; `AdapterRecord.source_dir` exists (registry.rs:57) in the `registered` slice → the reapply is feasible & precise. `PSYCHE_SPAWNED` zero occurrences in M11 tree.

**DONE @c3bdccf — unified merge PUSHED to main 2026-06-16.** doyle pushed Feature B/A/E/docs (origin/main c433793) + gave merge-ordering GO. Merged PR #16 (m11 672b928) INTO main as v080-merge (first-parent=main), reconciled per the recipe above (all 6 steps), pushed `git push origin HEAD:main` (c433793..c3bdccf). PR #16 auto-closed MERGED. Executed exactly as planned: dropped interim startup.rs spawn (PSYCHE_SPAWNED grep-zero) + its install_dir param + mod.rs call-site arg; reapplied Feature B at livehost.rs via `with_config_in(..., Some(source_dir))` matching the AdapterRecord by parent name in the `registered` slice + `[impl->REQ-INSTALL-11]` re-tag; kept both manifest.rs tests; fixed CONTEXT.md psyche phrasing. **GATE a-h GREEN locally** (traceable EXIT0, clippy --workspace -D warnings clean, live_firsthost_e2e + live_bind_firsthost_e2e green, PSYCHE_SPAWNED zero, digest intact, Feature E present, xtask check clean, no new doc leaks). Pinged doyle. NEXT: doyle runs gate a-i + confirms CI green BOTH runners on c3bdccf → deployah cuts v0.8.0 (counter 16, operator-gated at publish). HOLDING for doyle gate result / any merge bug.

LESSON: the silent-drop was REAL and surfaced exactly where predicted — the M11-only file (livehost.rs) did NOT conflict, so a textual merge kept `with_config` (install_dir=None) and would have shipped a daemon-hosted Psyche that resolves only via PATH. Also the `cmd_listen` signature silently kept main's `install_dir` param (git auto-resolved delete-vs-keep to keep) → would've been an unused-param clippy fail. Both caught by reading the diff + grounding in the actual code, not trusting auto-merge.

-**DONE @c3bdccf — unified merge PUSHED to main 2026-06-16.** doyle pushed Feature B/A/E/docs (origin/main c433793) + gave merge-ordering GO. Merged PR #16 (m11 672b928) INTO main as v080-merge (first-parent=main), reconciled per the recipe above (all 6 steps), pushed `git push origin HEAD:main` (c433793..c3bdccf). PR #16 auto-closed MERGED. Executed exactly as planned: dropped interim startup.rs spawn (PSYCHE_SPAWNED grep-zero) + its install_dir param + mod.rs call-site arg; reapplied Feature B at livehost.rs via `with_config_in(..., Some(source_dir))` matching the AdapterRecord by parent name in the `registered` slice + `[impl->REQ-INSTALL-11]` re-tag; kept both manifest.rs tests; fixed CONTEXT.md psyche phrasing. **GATE a-h GREEN locally** (traceable EXIT0, clippy --workspace -D warnings clean, live_firsthost_e2e + live_bind_firsthost_e2e green, PSYCHE_SPAWNED zero, digest intact, Feature E present, xtask check clean, no new doc leaks). Pinged doyle. NEXT: doyle runs gate a-i + confirms CI green BOTH runners on c3bdccf → deployah cuts v0.8.0 (counter 16, operator-gated at publish). HOLDING for doyle gate result / any merge bug.

+**DONE @c3bdccf — unified merge PUSHED to main 2026-06-16.** doyle pushed Feature B/A/E/docs (origin/main c433793) + gave merge-ordering GO. Merged PR #16 (m11 672b928) INTO main as v080-merge (first-parent=main), reconciled per the recipe above (all 6 steps), pushed `git push origin HEAD:main` (c433793..c3bdccf). PR #16 auto-closed MERGED. Executed exactly as planned: dropped interim startup.rs spawn (PSYCHE_SPAWNED grep-zero) + its install_dir param + mod.rs call-site arg; reapplied Feature B at livehost.rs via `with_config_in(..., Some(source_dir))` matching the AdapterRecord by parent name in the `registered` slice + `[impl->REQ-INSTALL-11]` re-tag; kept both manifest.rs tests; fixed CONTEXT.md psyche phrasing. **GATE a-h GREEN locally** (traceable EXIT0, clippy --workspace -D warnings clean, live_firsthost_e2e + live_bind_firsthost_e2e green, PSYCHE_SPAWNED zero, digest intact, Feature E present, xtask check clean, no new doc leaks). Pinged doyle. **doyle independently re-ran gate a-h on c3bdccf = PASS (verified livehost.rs:191-203 with_config_in + source_dir himself). CI 27613218507 (c3bdccf) = SUCCESS BOTH runners** (test+n1-gate kitsubito/hfenduleam + traceability; twohost skipped = merge commit not [twohost]-tagged, rig already proven W5 27608026651). Gate i cleared → full gate a-i PASS. Handed GO to deployah to cut v0.8.0 (counter 16, minor bump, operator-gated at publish). My reconciliation leg COMPLETE. Remaining = deployah's publish leg + operator publish-gate.


**v0.8.0 unified merge — fully gated, handed to deployah.**

- **CI green both runners** on c3bdccf (run 27613218507): test + n1-gate (kitsubito Linux + hfenduleam Windows) + traceability all success. twohost skipped (expected — non-tagged merge commit; rig proven on W5).
- doyle independently re-ran gate a–h on the merged tree = PASS (verified the livehost reapply himself). Gate i cleared → **full a–i PASS**.
- GO sent to deployah: cut v0.8.0 (counter 16, minor bump, operator-gated at publish).

My reconciliation leg is complete. Remaining = deployah's publish leg + the operator publish-gate. Standing by if anything bounces.


<task-notification>
<task-id>be1did0ug</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/11" id="81143310" type="msg" from="doyle">DELEGATION — build v0.8.1 (livehost boot-race). You own W/T planning + execution; I gate + drive release. Operator-directed.

BASE: branch v0.8.1-livehost-bootrace (off main = v0.8.0), commit 2aab895 already mints the REQ. `git fetch &amp;&amp; git checkout v0.8.1-livehost-bootrace`. Build the fix here; PR → p</EVENT-PART>
<EVENT-PART seq="2/11" id="81143310">ing me (doyle) to gate.

REQ: REQ-HAZARD-LIVEHOST-BOOT-RACE in traceable-reqs.toml — its TITLE is the full diagnostic, read it first. Minted required_stages=[]; YOU activate [impl,unit,int] per wave as evidence lands (same-commit tags).

DEFECT (perri's v0.8.0 dogfood, real spt-core bug): a harness-hosted live agent (`api seed`+`</EVENT-PART>
<EVENT-PART seq="3/11" id="81143310">api listen`) goes ONLINE but never gets a Psyche on a net-less/unpaired/peer-pump-STALLED node. Host-predicate FULLY passes (state=live_agent, status=online, adapter=claude-spt:live resolvable, psyche_init present) yet reconcile_once never emits LIVEHOST_PSYCHE. Correlate: &quot;peer pump: STALLED&quot; reappears after every restart = the net-</EVENT-PART>
<EVENT-PART seq="4/11" id="81143310">less boot-race; the live-host reconcile shares the brain boot lifecycle and is starved.

KEY FILES: crates/spt-daemon/src/livehost.rs (reconcile_once / host_one / spawn_live_host) · brainproc.rs (run_brain :159 → connect_retry → spawn_live_host UNCONDITIONAL @ :230, before heartbeat) · broker.rs (bind_in_with_net @575 binds the IPC</EVENT-PART>
<EVENT-PART seq="5/11" id="81143310"> listener INDEPENDENT of net, attach_net + REQ-DAEMON-9 self-heal).

HYPOTHESES (ground in source+runtime, do NOT presume — perri already refuted doyle's first guess cleanly): H1 brain reaches :230 but reconcile starved by an upstream net-attach gate; H2 spawn_psyche FAILS silently — check LIVEHOST_PSYCHE_FAIL log; may be REQ-INSTALL</EVENT-PART>
<EVENT-PART seq="6/11" id="81143310">-11 install-dir binary resolution (the psyche binary not found) — perri's blocked proof; H3 real detached `api listen` doesn't actually stamp status=online OR the brain child isn't spawned at all in daemon-run mode; H4 the real broker→brain-child boot path diverges from the in-process test harness.

MANDATE (3 things): (1) root-cause</EVENT-PART>
<EVENT-PART seq="7/11" id="81143310"> the net-less starvation AND the persistent peer-pump STALL on a single UNPAIRED Windows node (perri's REQ-DAEMON-9 self-heal isn't firing — may be same root). (2) FIX: live-host reconcile runs net-INDEPENDENTLY + survives the boot-race (mirror the dispatcher/peer-pump self-heal). (3) NON-NEGOTIABLE TEST SEAM: a REAL detached-daemon E2E — </EVENT-PART>
<EVENT-PART seq="8/11" id="81143310">api seed + listen a live agent on a net-less/unpaired node → assert LIVEHOST_PSYCHE. The existing establish→online ints use an IN-PROCESS daemon and the real-brain test hand-writes the perch, so the production broke
...(truncated)</event>
</task-notification>


1	- [pre-flight clippy --workspace](ci-clippy-preflight-workspace.md) — CI denies warnings workspace-wide (`clippy --workspace --all-targets -D warnings`); a `-p <crate>` local check misses sibling-crate lints → CI red round-trip (M11-W1: `-p spt` missed a `useless_vec` in spt-daemon's act-gate test). Pre-flight the exact CI invocation. CI only fires on PR (push branches=main/dev-freeform)
2	- [v0.8.0 merge reconciliation](v080-merge-reconciliation.md) — unified v0.8.0 merge NON-clean: live psyche model diverged (M11 daemon-host WINS vs main Feature B in-process install-dir spawn); naive merge silently DROPS Feature B. Reapply recipe (livehost.rs:191 → `with_config_in` w/ registry `source_dir`) + traceable re-tag caveat. BLOCKED: main Feature B on doyle's local unpushed main; wait for push + merge-ordering ping. how-to-live shipped @672b928
3	- [F-007 live-relay acceptance ruling](f007-live-relay-acceptance.md) — live bringup = persistent `api listen <id>` via Monitor (heir to legacy $LIVE start), NOT `--once` (no-Monitor degenerate) nor $OWL listen (ready); psyche-spawn gated on `--manifest` (declares psyche_init, fires regardless of --once) + the v0.7.4 install-dir binary fix (REQ-INSTALL-11) for bare psyche binaries; both F-007 items Bucket 2 docs gap. Lesson: ground in CONTEXT.md before ruling (I erred recommending --once)
4	- [delegate-build preflight hang](delegate-build-preflight-hang.md) — background build agents hang silently on the full spt-daemon/spt integration suite in preflight (no completion notification on hang); instruct targeted units only (`--lib` + named tests); check liveness via transcript mtime, TaskStop the stalled one, take ownership of the (usually good) partial work + verify
5	- [don't wait — proceed autonomously](dont-wait-proceed-autonomously.md) — operator 2026-06-14: don't stop to ask permission between steps; chain next action (ping/commit gated batch/next wave). After doyle W4 gate-PASS, commit myself
6	- [alarm port deferred — arrangement system](alarm-port-deferred-arrangement.md) — /spt:new-alarm durable scheduler = ACCEPTED parity gap, stays DEFERRED (DEFERRED.md:43, ADR-0018 Q4/V3). spt-core has alarm shape+relay only; real timer in legacy owl in-mem. Operator 2026-06-15: keep deferred, a superseding "arrangement" system is coming (not ready to discuss). Don't dispatch the port, don't build an adapter stopgap. perri logs it as known gap
7	- [broker is daemon state anchor](broker-is-daemon-state-anchor.md) — post-ADR-0018 the BROKER (not restartable brain child) is the always-up CLI-reachable daemon-lifetime anchor (serves all sockets, "brain restart loses nothing", holds DigestHub); place ephemeral in-mem daemon state there. My M11-W2 drive-slot gate said "brain" — wrong word, functional spec = broker; todlando caught it. Drive no-replay needs link-gen stamping (clear-on-link-break not crash-robust alone)
8	- [adapter add --release archive](adapter-add-release-archive.md) — `spt adapter add --release <user/repo> [--tag]` (REQ-INSTALL-9, @94bda1e, operator-directed doyle build): fetches a `.spt` tar from a GitHub release, extracts (system tar, no new dep), registers root. Closes perri's monorepo --github gap (--github is root-only). Trusts HTTPS+GitHub; does NOT change the [update] ripple route (acquisition only). Docs live; perri directed
9	- [adapter tips → published docs](adapter-tips-published-docs.md) — synthesized perri's SPT_HARNESS_ADAPTER_TIPS.md into a NEW harness-agnostic docs-site page harness-contract/patterns.md ("Adapter patterns & pitfalls"); generalized all [CC] specifics, zero leakage; published live to GH-Pages via `gh workflow run docs-publish.yml --ref main` (fires on tags/dispatch only, NOT main push). Pre-flight: xtask check + xtask site
10	- [how-to subnet → W5 scope](howto-subnet-w5-scope.md) — perri found `spt how-to subnet` dead-ends (NO_SUCH_TOPIC); HOW_TO_TOPICS locked to ready/send (v1, M7 decision 12). Genuine REQ-DOCS-6 content gap. doyle ACCEPTED → scheduled M11-W5 docs wave (HOW_TO_SUBNET text + registry + bump v1-lock test). Confirm landed at W5 gate
11	- [M11 COMPLETE](m11-complete.md) — shell-substrate milestone done end-to-end (W1 consent · W2 drive · W3 tunnel-same-node · W4 gateway capstone · W5 rig+docs), all doyle GATE PASS. PR #16 @c5887db UNMERGED until the operator-gated M11 release (merge+bump+tag, doyle lines up GO). Cross-node tunnel DEFERRED. HOLDING for release ping
12	- [M11-W4 progress](m11-w4-progress.md) — Gateway-owner shell capstone (REQ-SHELL-5, proof-not-change, ZERO prod diff): built (int gateway_owner_shell_e2e + linkhost unit, same-type-diff-id negative), doyle code-read PASS + tree-identity = PRE-CLEARED, only CI green left. CI saga: branch unmergeable vs advanced main → 0 pull_request runs → merged main in (HEAD 63a5d3e, v0.7.3). Supersedes W3
13	- [twohost rig runs notify REAL mode](twohost-rig-real-notify-mode.md) — the [twohost] rig sets SPT_TWO_HOST_NOTIFY_BIN → notify-shell resident → notify-0 ONLINE (fallback/local = noop offline). M11-W5 first-fire: the W2 drive-DROP rung asserted offline against notify-0 → it was ONLINE → drive DELIVERED (live slot) not dropped → RED. Use a deterministically-offline noop shell for drop proofs. A [twohost]-gated rung never fired = UNPROVEN regardless of green normal CI
14	- [unmergeable PR blocks CI](unmergeable-pr-blocks-ci.md) — push yields NO pull_request run + 0 checks + runners idle = UNMERGEABLE PR (GitHub can't compute merge ref), NOT a webhook drop. Diagnose: `gh pr view --json mergeable` + `git rev-list --count HEAD..origin/main`. Fix: merge main in (union append-docs, regen derived). Empty-commit/reopen nudges DON'T fix it. Check mergeability FIRST
15	- [M11-W3 progress](m11-w3-progress.md) — shell TUNNEL (REQ-SHELL-4): doyle design-gate GO; manifest opt-in + broker TunnelHub registry + close-hook LANDED on branch; NEXT = open-hook + the QUIC-stream↔`api tunnel` bridge (dispatch first-record demux + serve_tunnel relay loop), then T3.3 same-node mock E2E. Full next-step detail in the file
16	- [M11-W2 progress](m11-w2-progress.md) — drive channel (REQ-SHELL-3): ALL 4 tasks DONE + PUSHED to PR #16 (T2.1 b919dfd / T2.2 0557bc6 / T2.3 d200524 / T2.4 13fa53e + drift-fix 9d5fb63). REQ-SHELL-3 activated [impl,unit,int]. CI re-running post-drift-fix (run 27590716942); on green → submit W2 to doyle. Full detail in Psyche commune
17	- [CLI command → docs-drift gate](cli-command-docs-drift.md) — adding/changing a `spt` CLI command needs `cargo run -p xtask -- gen` (regen docs-site/src/cli/reference.md) AND no `M#-W#`/`REQ-*` in the clap `///` help (it IS the public --help). Docs-drift gate is CI-gated both runners, fires independent of traceable. M11-W2 first CI run RED on exactly this
18	- [gate against documented design](gate-against-documented-design.md) — gating lesson: verify a wave's premise vs the RECORDED design (CONTEXT.md/ADRs), not just current impl — an impl limitation can contradict a locked design (W2.5 single-subscriber vs locked controller/viewer model; operator caught it)
19	- [grep REQ tags to find impl](grep-req-tags-to-find-impl.md) — operator 2026-06-15: to reconcile is-REQ-X-wired, grep `REQ-X` FIRST — the [impl/int->REQ-X] tags point straight to the code; faster + more authoritative than an agent trace (I burned an agent trace + overstated "false-green" on REQ-DAEMON-1; tags showed it's built+E2E-tested, production-activation staged behind the live-agent adapter)
20	- [public docs: version not milestone](public-docs-version-not-milestone.md) — operator 2026-06-15: published docs-site uses VERSION numbers (since v0.7.0), never internal milestone/wave codes (M12-W3); internal design docs (CONTEXT/MANIFEST/ADRs) keep milestone refs. Grep docs-site edits for M#/W#/wave before commit

... [46 lines truncated] ...


New delegation from doyle: build **v0.8.1 livehost boot-race fix** (v0.8.0 shipped). I own planning + execution; doyle gates. Operator-directed — clear go. Message truncated at 8/11 (will request the tail), but the 3-part mandate is clear: root-cause net-less Psyche starvation, fix net-independent reconcile, add a real detached-daemon E2E.

Starting with investigation (safe + needed regardless of the truncated tail). Checkout + read the REQ diagnostic + the boot path:


PowerShell

git fetch origin --quiet 2>&1 | Select-Object -Last 2; git checkout v0.8.1-livehost-bootrace 2>&1 | Select-Object -Last 3; Write-Output "=== HEAD ==="; git log --oneline -1; Write-Output "=== REQ title ==="; Select-String -Path traceable-reqs.toml -Pattern "REQ-HAZARD-LIVEHOST-BOOT-RACE" -Context 0,3

Checkout v0.8.1 branch + read the REQ diagnostic


Your branch is up to date with 'origin/v0.8.1-livehost-bootrace'.
Already on 'v0.8.1-livehost-bootrace'
=== HEAD ===
2aab895 v0.8.1 base: mint REQ-HAZARD-LIVEHOST-BOOT-RACE (registry-first)
=== REQ title ===

> traceable-reqs.toml:950:id = "REQ-HAZARD-LIVEHOST-BOOT-RACE"
  traceable-reqs.toml:951:title = "The brain's live-host reconcile runs net-INDEPENDENTLY and survives the net-less boot-race:
a harness-hosted live agent that is state=live_agent + status=online with a resolvable adapter manifest (psyche_init present)
MUST get a daemon-hosted Psyche (LIVEHOST_PSYCHE) on a net-less / unpaired / peer-pump-STALLED node — the Psyche lifecycle
needs no net/pump, so whatever upstream net-attach starves the brain from reaching livehost::spawn_live_host
(brainproc.rs:230) must self-heal exactly as the dispatcher/peer-pump boot-race self-heal does (REQ-DAEMON-9). Silent-failure
was masked because state=live_agent + alive=true are stamped UNCONDITIONALLY, independent of the psyche-host online-stamp
(perri's diagnostic: host-predicate fully passes — state/status/adapter/psyche_init all resolve — yet reconcile_once never
produced a Psyche). The establish→online ints exercise an IN-PROCESS daemon and the real-brain test hand-writes the online
perch, so the production broker→brain-child→run_brain→spawn_live_host→reconcile_once boot path was NEVER E2E-exercised
end-to-end; closing the test seam (a REAL detached daemon: api seed + listen a live agent on a net-less/unpaired node → assert
LIVEHOST_PSYCHE) is part of this hazard."
  traceable-reqs.toml:952:required_stages = []  # rule 5: minted inactive 2026-06-16 (v0.8.1 work pending); todlando activates
[impl,unit,int] per wave as the net-independent reconcile fix + the real-detached-daemon E2E land
  traceable-reqs.toml:953:[[requirements]]


REQ diagnostic is comprehensive — confirms: live-host reconcile must run net-independently + self-heal the net-less boot-race (mirror REQ-DAEMON-9), and the production `broker→brain-child→run_brain→spawn_live_host→reconcile_once` boot path was never E2E-exercised. `spawn_live_host` @ brainproc.rs:230.

Delegating the boot-path root-cause investigation to a thorough Explore agent (conserves my context; grounds the 4 hypotheses in source), and asking doyle for the truncated tail (parts 9–11) in parallel:


Agent

Root-cause livehost boot-race

Prompt:
Root-cause a daemon boot-race in the spt-core Rust workspace (branch v0.8.1-livehost-bootrace, cwd C:\Users\decid\Documents\projects\spt-core). READ-ONLY: trace control flow + report; do NOT edit.

DEFECT: a harness-hosted live agent (`spt api seed` + `spt api listen <id>`) goes status=online but NEVER gets a daemon-hosted Psyche on a NET-LESS / unpaired / peer-pump-STALLED node. The host-predicate fully passes (state=live_agent, status=online, adapter resolvable, [session.psyche_init] present) yet `livehost::reconcile_once` never emits `LIVEHOST_PSYCHE`. Symptom correlates with "peer pump: STALLED" reappearing after every brain restart. Hypothesis: the live-host reconcile shares the brain boot lifecycle and is starved by an upstream net-attach step before it can run.

TRACE THE EXACT BOOT CONTROL FLOW and report with file:line citations:

1. crates/spt-daemon/src/brainproc.rs — `run_brain` (around :159). Walk the boot sequence line by line from entry through `connect_retry` to `spawn_live_host` (cited at :230, described as UNCONDITIONAL, before heartbeat). CRITICAL QUESTIONS:
   - Is there any BLOCKING / retry-loop / await between run_brain entry and :230 that, on a net-less node (no peer, net attach fails/retries forever), would PREVENT reaching :230? Quote the exact gating code.
   - Does connect_retry block indefinitely or bounded? What happens net-less?
   - Is spawn_live_host truly reached unconditionally net-less, or is it downstream of a net-dependent step?

2. crates/spt-daemon/src/livehost.rs — `reconcile_once`, `host_one`, `spawn_live_host`. Does spawn_live_host's reconcile loop run on its own thread/cadence independent of net? Does anything inside reconcile_once / host_one depend on net being attached? Where would LIVEHOST_PSYCHE_FAIL be logged (host_one ~:213)? Could spawn_psyche fail silently net-less?

3. crates/spt-daemon/src/broker.rs — `bind_in_with_net` (around :575): how the IPC listener binds INDEPENDENT of net, `attach_net`, and the REQ-DAEMON-9 self-heal. DESCRIBE the REQ-DAEMON-9 dispatcher/peer-pump boot-race self-heal pattern in detail (how it survives a net-less boot and retries attach without blocking the rest) — this is the pattern the fix must mirror for the live-host reconcile.

4. Compare: how does the broker→brain-child boot path (daemon-run mode) differ from the in-process test harness used by existing establish→online integration tests (e.g. crates/spt/tests/live_firsthost_e2e.rs calls livehost::reconcile_once directly in-process)? Identify why the production boot path was never exercised E2E.

EVALUATE these 4 hypotheses against the source, say which is supported/refuted with evidence:
- H1: brain reaches :230 but reconcile is starved by an upstream net-attach gate.
- H2: spawn_psyche FAILS silently (check LIVEHOST_PSYCHE_FAIL); possibly REQ-INSTALL-11 install-dir psyche-binary resolution (binary not found).
- H3: real detached `api listen` doesn't actually stamp status=online, OR the brain child isn't spawned at all in daemon-run mode.
- H4: the real broker→brain-child boot path diverges from the in-process test harness.

Also locate where "peer pump: STALLED" is logged and what condition produces it, and whether REQ-DAEMON-9 self-heal actually fires on a single UNPAIRED node (or whether it too is starved).

Deliverable: a concise root-cause report — the precise line(s) where a net-less node diverges from getting LIVEHOST_PSYCHE, which hypothesis holds, the REQ-DAEMON-9 self-heal pattern to mirror, and the minimal fix location(s). Cite file:line throughout.


<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: afe7959fd70c6cb94 (internal ID - do not mention to user. Use SendMessage with to: 'afe7959fd70c6cb94' 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\30df62c3-0b22-4b14-88de-fef422a5554f\tasks\afe7959fd70c6cb94.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.

Root-cause livehost boot-race

Prompt:
Root-cause a daemon boot-race in the spt-core Rust workspace (branch v0.8.1-livehost-bootrace, cwd C:\Users\decid\Documents\projects\spt-core). READ-ONLY: trace control flow + report; do NOT edit.

DEFECT: a harness-hosted live agent (`spt api seed` + `spt api listen <id>`) goes status=online but NEVER gets a daemon-hosted Psyche on a NET-LESS / unpaired / peer-pump-STALLED node. The host-predicate fully passes (state=live_agent, status=online, adapter resolvable, [session.psyche_init] present) yet `livehost::reconcile_once` never emits `LIVEHOST_PSYCHE`. Symptom correlates with "peer pump: STALLED" reappearing after every brain restart. Hypothesis: the live-host reconcile shares the brain boot lifecycle and is starved by an upstream net-attach step before it can run.

TRACE THE EXACT BOOT CONTROL FLOW and report with file:line citations:

1. crates/spt-daemon/src/brainproc.rs — `run_brain` (around :159). Walk the boot sequence line by line from entry through `connect_retry` to `spawn_live_host` (cited at :230, described as UNCONDITIONAL, before heartbeat). CRITICAL QUESTIONS:
   - Is there any BLOCKING / retry-loop / await between run_brain entry and :230 that, on a net-less node (no peer, net attach fails/retries forever), would PREVENT reaching :230? Quote the exact gating code.
   - Does connect_retry block indefinitely or bounded? What happens net-less?
   - Is spawn_live_host truly reached unconditionally net-less, or is it downstream of a net-dependent step?

2. crates/spt-daemon/src/livehost.rs — `reconcile_once`, `host_one`, `spawn_live_host`. Does spawn_live_host's reconcile loop run on its own thread/cadence independent of net? Does anything inside reconcile_once / host_one depend on net being attached? Where would LIVEHOST_PSYCHE_FAIL be logged (host_one ~:213)? Could spawn_psyche fail silently net-less?

3. crates/spt-daemon/src/broker.rs — `bind_in_with_net` (around :575): how the IPC listener binds INDEPENDENT of net, `attach_net`, and the REQ-DAEMON-9 self-heal. DESCRIBE the REQ-DAEMON-9 dispatcher/peer-pump boot-race self-heal pattern in detail (how it survives a net-less boot and retries attach without blocking the rest) — this is the pattern the fix must mirror for the live-host reconcile.

4. Compare: how does the broker→brain-child boot path (daemon-run mode) differ from the in-process test harness used by existing establish→online integration tests (e.g. crates/spt/tests/live_firsthost_e2e.rs calls livehost::reconcile_once directly in-process)? Identify why the production boot path was never exercised E2E.

EVALUATE these 4 hypotheses against the source, say which is supported/refuted with evidence:
- H1: brain reaches :230 but reconcile is starved by an upstream net-attach gate.
- H2: spawn_psyche FAILS silently (check LIVEHOST_PSYCHE_FAIL); possibly REQ-INSTALL-11 install-dir psyche-binary resolution (binary not found).
- H3: real detached `api listen` doesn't actually stamp status=online, OR the brain child isn't spawned at all in daemon-run mode.
- H4: the real broker→brain-child boot path diverges from the in-process test harness.

Also locate where "peer pump: STALLED" is logged and what condition produces it, and whether REQ-DAEMON-9 self-heal actually fires on a single UNPAIRED node (or whether it too is starved).

Deliverable: a concise root-cause report — the precise line(s) where a net-less node diverges from getting LIVEHOST_PSYCHE, which hypothesis holds, the REQ-DAEMON-9 self-heal pattern to mirror, and the minimal fix location(s). Cite file:line throughout.


Explore root-cause agent launched (background). Now requesting the truncated tail from doyle while it works:


Bash

cat <<'EOF' | "$OWL" send doyle
todlando — v0.8.1 livehost boot-race delegation ACCEPTED. On branch v0.8.1-livehost-bootrace @2aab895; read the REQ-HAZARD-LIVEHOST-BOOT-RACE diagnostic. Root-cause investigation underway (tracing the broker→brain-child→run_brain→spawn_live_host→reconcile_once boot path + the REQ-DAEMON-9 self-heal pattern to mirror).

Your delegation msg truncated at part 8/11 — resend parts 9-11 (cut off mid-sentence at "...the existing establish→online ints use an IN-PROCESS daemon and the real-brain test hand-writes the perch, so the production broke..."). Likely the rest of the test-seam spec + PR/gate process + any constraints. Proceeding on the clear 3-part mandate meanwhile; will fold in the tail.
EOF
echo "EXIT=$?"

Request truncated parts 9-11 from doyle


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

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


➜ SENT:doyle
EXIT=0


Both dispatched. Two things in flight:
1. **Root-cause investigation** (Explore agent `todlando-w159`, background) — tracing the production boot path + the REQ-DAEMON-9 self-heal pattern to mirror, grounding doyle's 4 hypotheses in source.
2. **Requested parts 9–11** from doyle (delegation msg truncated at 8/11).

v0.8.0 is shipped; v0.8.1 livehost boot-race delegation accepted, on branch @2aab895. REQ diagnostic read: live-host reconcile must run net-independently + self-heal the net-less boot-race, plus a real detached-daemon E2E (the production boot path was never E2E-tested).

Holding for the investigation report + doyle's tail, then I'll plan the waves and build.


<task-notification>
<task-id>be1did0ug</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/4" id="a11880c4" type="msg" from="doyle">TAIL (parts 9-11 of the delegation, truncated by owl part-cap):

...so the production broker→brain-child→run_brain→spawn_live_host→reconcile path was NEVER exercised. The real-daemon E2E MUST spawn the real brain child (not in-process) or it doesn't catch the bug. That E2E is the non-negotiable gate artifa</EVENT-PART>
<EVENT-PART seq="2/4" id="a11880c4">ct.

WHY IT MATTERS: unblocks perri's last two parity proofs — REQ-INSTALL-11 install-dir binary resolution (the psyche must actually SPAWN to exercise it) + the F-006 interim PATH-copy. Confirm install-dir resolution is exercised once the Psyche spawns (H2 candidate: spawn_psyche may fail because the psyche binary isn't found — chec</EVENT-PART>
<EVENT-PART seq="3/4" id="a11880c4">k LIVEHOST_PSYCHE_FAIL).

GATE CRITERIA (pre-flight before pinging me): clippy --workspace --all-targets -D warnings (exact CI invocation, NOT -p) · traceable-reqs check EXIT=0 · docs-drift gen if any CLI touched · the real-daemon E2E green. Then I code-read + verify the E2E spawns the brain child + gate → deployah ships v0.8.1 (cou</EVENT-PART>
<EVENT-PART seq="4/4" id="a11880c4">nter 17) → I ping perri.

Plan your own waves/tasks, self-drive, ping doyle at PR-ready. That's the whole brief.</EVENT-PART></event>
</task-notification>


Full brief received (parts 9–11):
- **Real-daemon E2E must spawn the real brain child** (not in-process) — the non-negotiable gate artifact; in-process won't catch the bug.
- **Why it matters:** unblocks perri's last two parity proofs — REQ-INSTALL-11 install-dir binary resolution (psyche must actually spawn to exercise it) + F-006 PATH-copy. Confirm install-dir resolution once Psyche spawns (H2: `spawn_psyche` may fail on binary-not-found → check `LIVEHOST_PSYCHE_FAIL`).
- **Gate (pre-flight before pinging doyle):** `clippy --workspace --all-targets -D warnings` (exact CI, not `-p`) · `traceable-reqs check` EXIT=0 · docs-drift gen if CLI touched · real-daemon E2E green. Then doyle code-reads + gates → deployah ships v0.8.1 (counter 17) → doyle pings perri.
- Self-drive; ping doyle at PR-ready only.

Complete picture now. Holding for the root-cause report (`todlando-w159`) before planning waves — won't duplicate its work. No ack needed (doyle self-drives; I ping at PR-ready).


<task-notification>
<task-id>afe7959fd70c6cb94</task-id>
<tool-use-id>toolu_016USAdoUjfND9V5ZzrcfSjM</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\30df62c3-0b22-4b14-88de-fef422a5554f\tasks\afe7959fd70c6cb94.output</output-file>
<status>completed</status>
<summary>Agent "Root-cause livehost boot-race" completed</summary>
<result>Perfect! Now I have all the pieces. Let me create the comprehensive root-cause report:

## ROOT-CAUSE ANALYSIS REPORT: Daemon Boot-Race Live-Host Starve

**Branch:** v0.8.1-livehost-bootrace
**Defect:** Live agent (`spt api seed` + `spt api listen <id>`) goes status=online but daemon never hosts the Psyche despite all predicates passing. Symptom: "peer pump: STALLED" reappears after brain restart.

---

### CRITICAL FINDING: REGISTRY LOAD ORDER RACE (H2 CONFIRMED)

The root cause is a **registry-load-order race** that causes `spawn_psyche` to fail silently with `LIVEHOST_PSYCHE_FAIL` — not a net-attach gate or brain starvation.

---

### BOOT CONTROL FLOW ANALYSIS

#### 1. **brainproc.rs:159–255 — `run_brain` boot sequence**

**Line-by-line flow:**
- **:161** `connect_retry()` — **NOT blocking indefinitely** on net-less nodes
  - Retries **300 times** × 10ms = **3 seconds max** (BRAIN_CONNECT_TRIES=300 at line 61)
  - Returns `Err` on failure, does NOT block forever
  - **Net-less nodes DO reach :230 unconditionally**

- **:186–195** `brain.resume_sessions()` — near-noop, non-fatal on error

- **:205** `write_ready()` — synchronous, fires before spawn_live_host

- **:219** `spawn_wake_host()` — spawned on its own thread, non-blocking

- **:230** `spawn_live_host()` — **UNCONDITIONALLY spawned, NO net-blocking gate**
  - Fires immediately, no await on net status
  - Comment line 221 explicitly states: "Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1)"
  - No `if net_up` guard — pure unconditional spawn

- **:232–254** Main heartbeat loop — polls `net_status()` to gate CONSUMERS (dispatcher/pump), NOT livehost
  - `consumer_gate()` :245 gates net-consumers ONLY, lines 245-251
  - `spawn_live_host` already running independently on :230

**VERDICT on H1 (net-blocking gate):** **REFUTED** — livehost spawn is unrestricted, reaches :230 unconditionally, fires even on net-less nodes. No net-attach dependency.

---

#### 2. **livehost.rs:248–266 — `spawn_live_host` thread (independent, runs unconditionally)**

**Line-by-line:**
- **:248–249** Spawned as a new thread (not awaited, non-blocking return)
- **:250–256** Infinite loop:
  - **:252–255** Load `owlery`, `adapters_dir`, **`registered` registry**, and config
  - **:256** Call `reconcile_once()` immediately
  - **:258–263** Sleep in 100ms slices for 5000ms (LIVE_RECONCILE_INTERVAL_MS)

**Key insight:** The thread runs **IMMEDIATELY** at boot, no sleep before first reconcile. The registry is **loaded fresh EVERY tick** (:254).

---

#### 3. **livehost.rs:137–209 — `reconcile_once` logic**

**The fatal sequence (lines 170–207):**

```rust
for id in perch::list_self_perch_ids(owlery) {     // :170 enumerate online perches
    if set.contains(&id) { continue; }             // :171–172
    let perch = perch::resolve_perch_path(&id, ParentHint::Infer); // :174
    let Some(info) = spt_store::info::read_info(&perch) else { continue; }; // :175–176
    if info.state != LIVE_AGENT_STATE { continue; }   // :178–179
    if info.status.as_deref() != Some(STATUS_ONLINE) { continue; } // :181–182

    let Some(adapter) = info.adapter.as_deref() else { continue; }; // :184–185

    // CRITICAL: resolve adapter from the PRELOADED registry (:254)
    let Ok(manifest) = spt_runtime::registry::resolve_option_in(
        registered,    // <- PASSED FROM LINE 254, snapshot at spawn time
        adapters_dir,
        adapter
    ) else { continue; };  // :187–189

    // REQ-INSTALL-11: resolve install dir from adapter parent record
    let parent = adapter.split(':').next().unwrap_or(adapter); // :197
    let install_dir = registered  // <- SAME REGISTRY SNAPSHOT
        .iter()
        .find(|(rec, _)| rec.name == parent)
        .map(|(rec, _)| std::path::PathBuf::from(&rec.source_dir)); // :198–201

    let Some(lifecycle) =
        BrainLifecycle::with_config_in(&manifest, &id, cfg.clone(), install_dir.as_deref())
    else { continue; };  // :202–205

    host_one(set, &id, lifecycle, info.session_id, reason); // :207
}
```

---

#### 4. **livehost.rs:216–239 — `host_one` — where LIVEHOST_PSYCHE_FAIL fires**

```rust
fn host_one(...) {
    lifecycle.sweep_on_start();                              // :223
    match lifecycle.spawn_psyche(Some(&session_id), None) { // :224
        Ok(h) => eprintln!("LIVEHOST_PSYCHE:{id} pid={}", h.pid), // :225 — SUCCESS
        Err(e) => {
            eprintln!("LIVEHOST_PSYCHE_FAIL:{id}: {e}");     // :227 — **FAILURE LOGGED HERE**
            return; // no Psyche → no driver; retried next reconcile // :228
        }
    }
    // Start pulse driver thread if spawn succeeded
    let stop = Arc::new(AtomicBool::new(false)); // :231–238
    // ... insert into set ...
}
```

**VERDICT:** If `LIVEHOST_PSYCHE_FAIL` appears in logs, `spawn_psyche()` failed. The driver is NEVER started (:235).

---

### ROOT CAUSE: REGISTRY LOAD-ORDER RACE (H2 CONFIRMED)

**The precise failure mechanism:**

1. **daemon.rs:184–194** `net_retry_attach()` spawned on background thread (line 194) when net bind fails
   - Retries `try_start_net()` every 1–30 seconds with capped backoff
   - On success, calls `broker.attach_net(host)` (:355)

2. **livehost.rs:254** `registered = spt_runtime::registry::registered(&adapters_dir)` loads registry snapshot
   - Called ONCE PER TICK (every 5000ms)
   - Reads from disk: `~SPT_HOME/adapters/registry.json` (or equivalent)

3. **REQ-INSTALL-11 (livehost.rs:191–201):** Install-dir resolution via registry record
   - Requires adapter parent record in `registered` vec (:198–200)
   - If adapter is registered AFTER the brain spawns but BEFORE the first reconcile tick:
     - Registry snapshot at line 254 may be stale or empty
     - `resolve_option_in()` (:187) may succeed if adapter was pre-registered
     - **BUT** install-dir lookup (:198–200) will find no matching parent record
     - `install_dir` becomes `None`
     - `BrainLifecycle::with_config_in(&manifest, &id, cfg, None)` (:203) is called with `install_dir=None`

4. **The actual failure:** If `spawn_psyche()` needs the install dir to resolve the psyche binary via REQ-INSTALL-11 logic:
   - Psyche binary cannot be resolved from the install dir (not present)
   - Falls back to PATH-only resolution (if configured)
   - **If copy-mode adapter** (psyche binary only in install dir, not in PATH):
     - Binary not found
     - `spawn_psyche()` returns `Err` (:226–228)
     - **`LIVEHOST_PSYCHE_FAIL` is logged** — the agent is never hosted
     - Next reconcile tick (5000ms later) retries with fresh registry snapshot

**Why "peer pump: STALLED"?**
The brain heartbeat (:238–244) polls `net-status`, which fails when the broker is net-less and hasn't yet attached net via the background retry. The peer pump isn't running (consumers not spawned at :247) because `net_enabled` is still `false`. But if the live host also fails to spawn (LIVEHOST_PSYCHE_FAIL), the brain never stamps a successful boot condition, and the CLI observes a stalled pump.

---

### EVIDENCE FROM CODE

**Evidence the registry is loaded too early and NOT re-read between brain spawn and first `api listen` online:**

1. **livehost.rs:248–256**: `spawn_live_host()` thread reads registry at `:254` **ONCE PER TICK** (5000ms)
2. **livehost.rs:187**: `resolve_option_in(registered, ...)` searches the snapshot taken at `:254`
3. **livehost.rs:198–200**: Install-dir lookup uses SAME snapshot: `registered.iter().find(...)`
4. **daemon.rs:350–363**: `net_retry_attach()` background thread retries net; no signal to refresh brain's registry snapshot

**The race window:**
```
t=0:     brain spawns, spawn_live_host() thread starts
t=0ms:   first reconcile_once() fires, loads registry (possibly empty or adapter not yet registered)
t=0–50ms: spt api listen <id> starts, calls `spt api seed`, then api listen which marks status=online
t=50ms:  adapter may be registered at this point (after brain's first reconcile)
t=5000ms: brain's SECOND reconcile tick loads fresh registry, now has the adapter
         (if registered between t=0 and t=50), re-tries host_one()
```

**If registry was empty or adapter missing at t=0ms first reconcile → install_dir=None → spawn fails.**

---

### TEST HARNESS vs. PRODUCTION (H4: DIVERGENCE CONFIRMED)

**crates/spt/tests/live_firsthost_e2e.rs:65–142** — the integration test:

```rust
start_inproc_daemon();  // :69 — in-process daemon (broker + brain in threads, no process isolation)
// seed + register adapter (:74–89)
// Call spt api seed (:100–103)
// Call spt api listen (:108–112) — marks status=online
// THEN call reconcile_once() in-process (:135–142)  <-- AFTER api listen marks online
```

**Key difference:**
- **Test:** In-process. Registry operations + `reconcile_once()` share the same Rust process.
- **Test timing:** The brain reconcile is called AFTER `api listen` has completed (marks online AND likely registered adapter).
- **Test registry:** Adapter is registered BEFORE `reconcile_once()` is called.

**Production daemon (daemon.rs:274 + brainproc.rs:230):**
- Brain spawned as **separate child process** — runs independently
- **Timing:** No guarantee when `spt api listen` registers the adapter relative to the brain's first reconcile tick
- **Registry load:** Snapshot at :254 happens ~t=0, but adapter registration may happen at t=50ms

**The harness NEVER exercised the production case** because:
1. The harness doesn't spawn a real child process
2. The harness doesn't measure time between broker spawn and `api listen` registration
3. The unit tests mock the registry (zynga_adapter is pre-registered for the test)
4. The integration test calls `reconcile_once()` AFTER marking the perch online AND registering the adapter

---

### THE FIX: Registry-Aware Reconcile Gate

The minimal fix mirrors the REQ-DAEMON-9 self-heal pattern (net-attach retry):

**Option A: Lazy Registry Load (Recommended)**
Cache the registry snapshot per perch in `reconcile_once()` so misses on first tick are retried:

```rust
// livehost.rs:137–209, approximate patch pseudocode
pub fn reconcile_once(...) {
    // ... stop-side logic ...

    // Start side: LAZY load registry per perch, retry on failure
    for id in perch::list_self_perch_ids(owlery) {
        // ... checks (state, status, adapter) ...

        // Load fresh registry for THIS perch ONLY if needed
        let mut manifest = None;
        let mut install_dir = None;

        // First, try cached/preloaded registry (passed param)
        if let Ok(m) = spt_runtime::registry::resolve_option_in(registered, adapters_dir, adapter) {
            manifest = Some(m);
            let parent = adapter.split(':').next().unwrap_or(adapter);
            install_dir = registered
                .iter()
                .find(|(rec, _)| rec.name == parent)
                .map(|(rec, _)| std::path::PathBuf::from(&rec.source_dir));
        }

        // If install_dir is still None (adapter missing from preloaded registry),
        // RELOAD the registry fresh and retry (mirrors REQ-DAEMON-9 self-heal)
        if install_dir.is_none() && manifest.is_some() {
            let fresh_registered = spt_runtime::registry::registered(&adapters_dir);
            let parent = adapter.split(':').next().unwrap_or(adapter);
            install_dir = fresh_registered
                .iter()
                .find(|(rec, _)| rec.name == parent)
                .map(|(rec, _)| std::path::PathBuf::from(&rec.source_dir));
        }

        // ... rest of host_one() with resolved install_dir ...
    }
}
```

**Option B: Refresh Registry on Empty Install-Dir (Minimal)**
In `host_one()`, refresh the registry ONLY if `install_dir` is None and re-resolve:

```rust
// livehost.rs:216–239, patch:
fn host_one(..., install_dir: Option<&Path>, ...) {
    lifecycle.sweep_on_start();

    // If install_dir is None (possibly stale registry), refresh and re-resolve
    let install_dir = if install_dir.is_none() {
        let fresh_registered = spt_runtime::registry::registered(&adapters_dir);
        let parent = info.adapter.split(':').next().unwrap_or(&info.adapter);
        fresh_registered
            .iter()
            .find(|(rec, _)| rec.name == parent)
            .map(|(rec, _)| std::path::PathBuf::from(&rec.source_dir))
    } else {
        install_dir.map(|p| p.to_path_buf())
    };

    match lifecycle.spawn_psyche(Some(&session_id), None) {
        Ok(h) => eprintln!("LIVEHOST_PSYCHE:{id} pid={}", h.pid),
        Err(e) => {
            eprintln!("LIVEHOST_PSYCHE_FAIL:{id}: {e}");
            return;
        }
    }
    // ...
}
```

---

### SUMMARY TABLE: HYPOTHESIS EVALUATION

| Hypothesis | Status | Evidence |
|-----------|--------|----------|
| **H1: brain blocked before :230 by net-attach gate** | **REFUTED** | `connect_retry()` :161 has 3s max timeout; `spawn_live_host()` :230 unconditional; no net-gate upstream |
| **H2: spawn_psyche fails silently on install-dir resolution** | **CONFIRMED** | REQ-INSTALL-11 :191–201 requires install_dir from registry; race between brain spawn (:254 load) and adapter registration by `api listen` causes None; `LIVEHOST_PSYCHE_FAIL` :227 logs the failure |
| **H3: api listen doesn't mark status=online or brain not spawned** | **REFUTED** | Integration test :65–142 proves api listen marks online; brainproc.rs:274 unconditionally spawns supervisor |
| **H4: production boot path diverges from test harness** | **CONFIRMED** | Harness: in-process, calls reconcile_once() AFTER api listen + adapter registration; Production: separate child process, first reconcile :250–256 may fire before api listen completes (registry snapshot race) |

---

### MINIMAL FIX LOCATION(S)

1. **Primary fix site: livehost.rs:187–201 (install-dir resolution)**
   - Implement lazy registry refresh when `install_dir` resolves to `None`
   - Mirror the REQ-DAEMON-9 self-heal pattern: background retry with capped backoff on failures

2. **Secondary logging site: livehost.rs:227 (LIVEHOST_PSYCHE_FAIL)**
   - Already logs failures — verify this appears in production logs when the defect reproduces

3. **Test harness gap: crates/spt/tests/live_firsthost_e2e.rs**
   - Add a variant that introduces a delay or races the brain's first tick BEFORE `api listen` completes
   - Prove the fresh-registry retry survives the race

---

### "PEER PUMP: STALLED" CORRELATION

**Why the symptom reappears after brain restart:**

1. Brain restarts (brain crash/respawn by supervisor)
2. Brain's `spawn_live_host()` (:230) spawns fresh
3. On **first tick** (:250–256), registry snapshot may still be stale/empty if net attach is also pending
4. Live endpoints fail to host (LIVEHOST_PSYCHE_FAIL)
5. Consumer start (:247–251) waits for `net_enabled=true`
6. Net attach still in background retry loop (daemon.rs:350–363)
7. **Combined:** No live hosts + no net consumers = no pulse loop activity visible to CLI → "peer pump: STALLED"

The "peer pump" itself is not the direct cause; it's the absence of OTHER evidence of liveness (live endpoint hosting + net consumers) that makes the pump appear stalled.

---

**END OF ANALYSIS**</result>
<usage><subagent_tokens>79214</subagent_tokens><tool_uses>15</tool_uses><duration_ms>88413</duration_ms></usage>
</task-notification>


Report is thorough but I must not take it at face value — doyle explicitly warned "do NOT presume" (perri already refuted his first guess). The report's mechanism has an **internal inconsistency**: it claims `resolve_option_in` succeeds but the install_dir `find` on the *same* `registered` slice returns None — impossible, they use the same snapshot/parent. And a first-tick registry miss would self-heal in 5s, contradicting the *persistent* "never gets a Psyche."

The REQ title (doyle's runtime-informed framing) says the brain is *starved from reaching* `spawn_live_host` net-less. The report claims :230 is reached unconditionally. That tension is the crux. Verifying `run_brain`'s actual handling of `connect_retry` net-less myself: