230
231	/// `api listen <id>` — consume the seed, bind the perch, then run the relay
232	/// loop (heir to `$LIVE start`'s Monitor-bound poll loop). `--once` drains and
233	/// exits (testability); otherwise blocks streaming messages to stdout.
234	///
235	/// When the manifest is **live-capable** (declares `[session.psyche_init]`), the
236	/// loop also hosts the LiveAgent lifecycle over the M2b `spt-live` seams: sweep
237	/// stale signoff on start (3.2), spawn the Psyche (T2), and run a pulse tick
238	/// each cycle (drop ingest + echo fires — T3/T4/T5). The graceful signoff (T6)
239	/// is driven by the shutdown path; M3's daemon re-hosts all of it.
240	// [impl->REQ-START-2]
241	// [impl->REQ-SEAM-PSYCHE]
242	pub fn cmd_listen(
243	    id: &str,
244	    parent_pid_override: Option<u32>,
245	    once: bool,
246	    manifest: Option<&spt_runtime::Manifest>,
247	    subnet: Option<&str>,
248	) -> i32 {
249	    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
250	    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
251	        eprintln!("INVALID_ID:{id}: {e}");
252	        return EXIT_REFUSED;
253	    }
254	    let parent_pid = match parent_pid_override.or_else(proc::parent_pid) {
255	        Some(p) => p,
256	        None => {
257	            eprintln!("NO_PARENT_PID: could not resolve the parent pid anchor");
258	            return EXIT_REFUSED;
259	        }
260	    };
261	    let token = match bind_from_seed(id, parent_pid, subnet) {
262	        Ok(t) => t,
263	        Err(e) => {
264	            report_bind_error(&e);
265	            return EXIT_REFUSED;
266	        }
267	    };
268	    eprintln!("BOUND:{id} token={token}");
269
270	    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
271	    let listener = match PollListener::bind_blocking(id, RELAY_POLL) {
272	        Ok(l) => l,
273	        Err(e) => {
274	            eprintln!("LISTEN_FAIL:{id}: {e}");
275	            return EXIT_REFUSED;
276	        }
277	    };
278
279	    // The bound session id (from the seed) — fills the lifecycle templates.

/// `api listen <id>` — consume the seed, bind the perch, then run the relay
/// loop (heir to `$LIVE start`'s Monitor-bound poll loop). `--once` drains and
/// exits (testability); otherwise blocks streaming messages to stdout.
///
/// When the manifest is **live-capable** (declares `[session.psyche_init]`), the
/// loop also hosts the LiveAgent lifecycle over the M2b `spt-live` seams: sweep
/// stale signoff on start (3.2), spawn the Psyche (T2), and run a pulse tick
/// each cycle (drop ingest + echo fires — T3/T4/T5). The graceful signoff (T6)
/// is driven by the shutdown path; M3's daemon re-hosts all of it.
// [impl->REQ-START-2]
// [impl->REQ-SEAM-PSYCHE]
pub fn cmd_listen(
    id: &str,
    parent_pid_override: Option<u32>,
    once: bool,
    manifest: Option<&spt_runtime::Manifest>,
    subnet: Option<&str>,
) -> i32 {
    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
        eprintln!("INVALID_ID:{id}: {e}");
        return EXIT_REFUSED;
    }
    let parent_pid = match parent_pid_override.or_else(proc::parent_pid) {
        Some(p) => p,
        None => {
            eprintln!("NO_PARENT_PID: could not resolve the parent pid anchor");
            return EXIT_REFUSED;
        }
    };
    let token = match bind_from_seed(id, parent_pid, subnet) {
        Ok(t) => t,
        Err(e) => {
            report_bind_error(&e);
            return EXIT_REFUSED;
        }
    };
    eprintln!("BOUND:{id} token={token}");

    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    let listener = match PollListener::bind_blocking(id, RELAY_POLL) {
        Ok(l) => l,
        Err(e) => {
            eprintln!("LISTEN_FAIL:{id}: {e}");
            return EXIT_REFUSED;
        }
    };

    // The bound session id (from the seed) — fills the lifecycle templates.


crates\spt-daemon\src\brain.rs:73:/// ([`Brain::resume_sessions`] over the broker's cursor-of-record), never this
crates\spt-daemon\src\brain.rs:253:    /// [`Brain::resume_sessions`], replacing the retired single-session
crates\spt-daemon\src\brain.rs:326:    /// [`Brain::cold_start`] + [`Brain::resume_sessions`] (broker cursor-of-record),
crates\spt-daemon\src\brain.rs:539:                    // `resume_sessions`) routes EVERY session through the map —
crates\spt-daemon\src\brain.rs:796:    pub fn resume_sessions(&mut self) -> io::Result<Vec<u64>> {
crates\spt-daemon\src\brain.rs:811:    /// re-attached by [`Brain::resume_sessions`]) — observability / tests.
crates\spt-daemon\src\brainproc.rs:183:    match brain.resume_sessions() {


ACK doyle: W0.1 scope confirmed, building. Gate-anchor noted (CONTEXT:30/34/194 — brain owns loop, liveness daemon-authoritative); W0.4 production-path int won't regress to type-level.


Bash

$OWL send doyle <<'EOF'
[todlando → doyle] ack received, building W0.1. Gate anchor locked: CONTEXT:30/34/194 (daemon-brain owns the pulse loop; liveness daemon-authoritative). W0.4 production-path int will prove the real brain process hosts the Psyche (api listen/api bind), not BrainLifecycle-in-a-test. Currently mapping the brain's session/rehydrate model (resume_sessions + how cmd_listen resolves a live endpoint's manifest) to design the endpoint-table call site cleanly. Will shout if I hit a real edge (esp. how the brain enumerates which on-disk endpoints to host on cold-start). — todlando
EOF
echo sent=$?

Ack doyle W0.1 go


➜ SENT:doyle
sent=0


780	    /// broker for **every** hosted session and re-attach **each** in resume mode
781	    /// from the broker's per-session delivered cursor (`resume_seq`). This is the
782	    /// production replacement for the retired single-session `BrainState` handoff
783	    /// frame — a brain the supervisor respawns (crash *or* update) reconstructs
784	    /// all session continuity by querying the persistent side, never a brain→brain
785	    /// message. Returns the ids re-attached (empty when the broker hosts none —
786	    /// the supervised daemon brain's no-op-today case).
787	    ///
788	    /// Each session is seeded into [`Self::session_cursors`] at its `resume_seq`,
789	    /// so [`Brain::read_event`] dedups the broker's at-least-once boundary re-send
790	    /// and accepts a post-eviction ring-floor jump (no reject-gap). Output is
791	    /// at-least-once; input/effects stay exactly-once via the broker-owned
792	    /// `EffectJournal`. The generation is **not** touched — this is a cold start
793	    /// (generation custody is the broker's, ADR-0018 V2), not a `handoff`.
794	    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]
795	    // [impl->REQ-DAEMON-2]
796	    pub fn resume_sessions(&mut self) -> io::Result<Vec<u64>> {
797	        let reply = self.sessions()?;
798	        let mut resumed = Vec::with_capacity(reply.sessions.len());
799	        for info in reply.sessions {
800	            // Seed the dedup cursor at the broker's resume point BEFORE the
801	            // subscribe replay starts arriving, then re-attach from there.
802	            self.session_cursors
803	                .insert(info.session_id, info.resume_seq);
804	            self.subscribe(info.session_id, info.resume_seq)?;
805	            resumed.push(info.session_id);
806	        }
807	        Ok(resumed)
808	    }
809
810	    /// The per-session resume cursor (the next seq still needed for a session
811	    /// re-attached by [`Brain::resume_sessions`]) — observability / tests.
812	    pub fn session_cursor(&self, session_id: u64) -> Option<u64> {
813	        self.session_cursors.get(&session_id).copied()
814	    }
815
816
817	    /// Query the broker-owned net endpoint's status (D4a): node id, dialable
818	    /// address, conn count — or `enabled: false` on a net-less broker. Reads
819	    /// until the reply (consuming interleaved events like [`Brain::spawn_session`]).
820	    // [impl->REQ-DAEMON-2]
821	    pub fn net_status(&mut self) -> io::Result<NetStatusReply> {
822	        self.send(KIND_NET_STATUS, serde_json::Value::Null)?;
823	        loop {
824	            match self.read_event()? {
825	                BrokerEvent::NetStatus(s) => return Ok(s),
826	                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
827	                _ => continue,
828	            }
829	        }
830	    }
831
832	    /// Run the joiner leg of the subnet pairing ceremony on the broker (M7
833	    /// D3, REQ-SUBNET-2): the broker owns the node-identity endpoint the
834	    /// ceremony binds and pins. Blocks for the ceremony's bounded life; the
835	    /// reply classifies failures for the CLI's guided UX. Not journaled —
836	    /// the code must never persist, and a re-join is store-idempotent.
837	    // [impl->REQ-SUBNET-2]
838	    pub fn pair_join(
839	        &mut self,

    /// broker for **every** hosted session and re-attach **each** in resume mode
    /// from the broker's per-session delivered cursor (`resume_seq`). This is the
    /// production replacement for the retired single-session `BrainState` handoff
    /// frame — a brain the supervisor respawns (crash *or* update) reconstructs
    /// all session continuity by querying the persistent side, never a brain→brain
    /// message. Returns the ids re-attached (empty when the broker hosts none —
    /// the supervised daemon brain's no-op-today case).
    ///
    /// Each session is seeded into [`Self::session_cursors`] at its `resume_seq`,
    /// so [`Brain::read_event`] dedups the broker's at-least-once boundary re-send
    /// and accepts a post-eviction ring-floor jump (no reject-gap). Output is
    /// at-least-once; input/effects stay exactly-once via the broker-owned
    /// `EffectJournal`. The generation is **not** touched — this is a cold start
    /// (generation custody is the broker's, ADR-0018 V2), not a `handoff`.
    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]
    // [impl->REQ-DAEMON-2]
    pub fn resume_sessions(&mut self) -> io::Result<Vec<u64>> {
        let reply = self.sessions()?;
        let mut resumed = Vec::with_capacity(reply.sessions.len());
        for info in reply.sessions {
            // Seed the dedup cursor at the broker's resume point BEFORE the
            // subscribe replay starts arriving, then re-attach from there.
            self.session_cursors
                .insert(info.session_id, info.resume_seq);
            self.subscribe(info.session_id, info.resume_seq)?;
            resumed.push(info.session_id);
        }
        Ok(resumed)
    }

    /// The per-session resume cursor (the next seq still needed for a session
    /// re-attached by [`Brain::resume_sessions`]) — observability / tests.
    pub fn session_cursor(&self, session_id: u64) -> Option<u64> {
        self.session_cursors.get(&session_id).copied()
    }

    /// Query the broker-owned net endpoint's status (D4a): node id, dialable
    /// address, conn count — or `enabled: false` on a net-less broker. Reads
    /// until the reply (consuming interleaved events like [`Brain::spawn_session`]).
    // [impl->REQ-DAEMON-2]
    pub fn net_status(&mut self) -> io::Result<NetStatusReply> {
        self.send(KIND_NET_STATUS, serde_json::Value::Null)?;
        loop {
            match self.read_event()? {
                BrokerEvent::NetStatus(s) => return Ok(s),
                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
                _ => continue,
            }
        }
    }

    /// Run the joiner leg of the subnet pairing ceremony on the broker (M7
    /// D3, REQ-SUBNET-2): the broker owns the node-identity endpoint the
    /// ceremony binds and pins. Blocks for the ceremony's bounded life; the
    /// reply classifies failures for the CLI's guided UX. Not journaled —
    /// the code must never persist, and a re-join is store-idempotent.
    // [impl->REQ-SUBNET-2]
    pub fn pair_join(
        &mut self,


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	- [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
3	- [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)
4	- [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)
5	- [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
6	- [M12 W1 progress](m12-w1-progress.md) — **M12 FULLY GATED end-to-end** (W1·W1.5·HR2·W3·W2·follow-on·W4·W5 + **W2.5 GATE-PASS @6b4859f + gate-#7 combined close-out @f521df2**, attach 12/12 reproduced green, traceable EXIT=0, all 6 conditions + gate-#7 self-kick non-negotiable verified by doyle). M12 publishing (v0.6.0 baseline used to start perri). doyle next: ping perri when M12 publishes + triage any spt-core bug perri reports
7	- [cplugs publish mechanics](cplugs-publish-mechanics.md) — concrete cplugs marketplace publish steps live in claude_skill_owl/docs/DEPLOY.md (+DEPLOY.ps1); spt-claude-code takes only the SKELETON subset (no binary in cplugs — binary+manifest ride spt-core adapter registry). Relayed to perri
8	- [perri needs reply to continue](perri-needs-reply-to-continue.md) — STANDING (operator 2026-06-15): ALWAYS reply to perri's reports (ack + keep-going + confirm next plan); she treats silence as a stop-point and stalls. todlando self-drives (no nudge needed); perri needs it
9	- [no machine-wide kill on shared runner](no-machinewide-killon-shared-runner.md) — BINDING (doyle 2026-06-15): HFENDULEAM = self-hosted CI runner sharing the box with LIVE agents; never `Get-Process spt|Stop-Process` machine-wide (tears down live broker); scope reaps to $GITHUB_WORKSPACE/target path or a PID. I'd done the unsafe form 3× locally — disclosed
10	- [agent roles](agent-roles.md) — doyle=gater/spt-core owner (don't self-execute big spt-core builds), todlando=spt-core executor, perri=spt-claude-code adapter builder. perri finds gap → doyle designs/dispatches → todlando builds → doyle gates → perri validates
11	- [EVENT envelope + __REPLY_TO__ removal](event-envelope-reply-to-removal.md) — ADR-0020 (2026-06-15 grill-with-docs): `<EVENT type=msg from=…>` envelope = sole arriving-message format at EVERY surface incl api poll; `__REPLY_TO__` mis-elevated relic REMOVED; F-002 dissolves; docs done (ADR+amendments+CONTEXT+REQ-MSG-ENVELOPE), multi-crate IMPL refactor PENDING (poll still emits relic until then); perri parses `<EVENT>` not the relic
12	- [spt-core harness boundary + grounding](spt-core-harness-boundary-and-grounding.md) — BINDING (operator 2026-06-15): spt-core never caters to a specific harness (adapter formats spt-core's output for its harness; don't doc CC-specifics on spt-core surface); ground all completeness Qs in CONTEXT.md/ADRs (thoroughly planned — apparent gaps usually already specified). F-001 disposition: most=adapter glue, residual=publish agnostic primitives
13	- [sptc plugin name ruling](sptc-plugin-name-ruling.md) — spt-claude-code cplugs plugin = `sptc` (interim) → flips to `spt` on proven parity (then retire legacy owl `spt`); CC hard-ties skill prefix to plugin.json name (no override) so /spt: parity needs name=spt, blocked by live legacy owl. Distinct from adapter_name claude-spt
14	- [experimplate traceable seed leaks](experimplate-traceable-seed-leaks.md) — experimplate template ships traceable seed defects (4 tag-leaks + manifest_error in own example docs); perri fixed in-instance via scan-root trim; doyle to fix upstream (scope scan-root / inert example tags) before next adapter reuse
15	- [M9 status](m9-wave1-progress.md) — M9 COMPLETE + SHIPPED: all 4 gates passed (G1/G2/Gateway-WAN/G3), all PRs merged to main (operator GO 2026-06-13), **v0.5.0 tagged** (counter 10, deployah signing). Don't re-plan. See [[v050-published]] for release state
16	- [spt-claude-code NEXT](spt-claude-code-next.md) — SCOPING DONE 2026-06-14 (SCOPE.md/CONTEXT.md committed @09d6020). M12 handed to todlando as GATING prereq (now executing W1). RESUME = author experimplate → instantiate spt-claude-code → write perri HANDOFF. perri builds from spt-releases+GH-Pages ONLY
17	- [M10 digest extractor](m10-digest-extractor.md) — doyle GATE PASS 2026-06-13: all 4 waves (39463a2..afa0561), REQ-TERM-5/6/7 evidenced, full suite + capstone E2E green, independently verified. `[digest]` imperative extractor seam (ADR-0019, reverses M9) + thread-spanning (session ledger) + two-origin merge + digest-proof + skip-diagnostics. SHIPPED as v0.6.0 (counter 11) — see [[v060-published]]. Carried: real-CC digest-proof → perri/spt-claude-code acceptance criterion
18	- [v0.6.0 published](v060-published.md) — M10 PUBLIC 2026-06-13 (counter 11); todlando merge+tag, deployah sign+publish; minor bump (0.5.0→0.6.0); hashes linux 002ed710… win 4f5c61e8…. ROUTINE seamless roll now (respawn-path arc closed); publisher leg proven 5× (counter 7-11). enlyzeam still pending natural-propagation confirm
19	- [v0.7.0 published](v070-published.md) — PUBLIC 2026-06-15 (counter 12); todlando merge+tag, deployah sign+publish; HEAD e08cea0; hashes linux f4082ed3… win c09d4e6d…. Routine seamless; publisher leg proven 6× (counter 7-12). enlyzeam still pending catch-up
20	- [v0.7.1 published](v071-published.md) — PUBLIC 2026-06-15 (counter 13); patch; ADR-0020 EVENT-everywhere + __REPLY_TO__ removal (unblocks perri byte-capture); tag cb05600; hashes linux 81eededc… win 94bb5429…. GOTCHA: release.yml assemble step hit transient TLS-handshake-timeout on gh-release-create → `gh run rerun <id> --failed` clears it (builds reused, no re-tag); read failed step before alarming (transient-API ≠ disk-full ≠ code). Publisher leg proven 7× (counter 7-13)
21	- [v0.7.2 published](v072-published.md) — PUBLIC 2026-06-15 (counter 14); digest-proof {session_id} key-fill F-004 (PR#14); deployah drove FULL solo incl AUTHORING the `release: v0.7.2` bump commit (1582e32); hashes linux 30e4e090… win 66e2dbd3…. KEY LESSON: fix-only PR does NOT bump version — driver authors the bump (workspace.package.version + CHANGELOG section=release-body + Cargo.lock 11 first-party lines). WORKTREE: main locked in spt-core-doyle wt → throwaway `git worktree add --detach` + push HEAD:main. FLAKE: PR#14 stale-base RED both runners (diff test each = flaky), update-branch+fresh-CI cleared, logged FLAKE-LEDGER #5/#6 @b5f61ba. Publisher leg proven 8× (counter 7-14)
22	- [EVENT envelope IMPL gate-pass + poll false-green](event-envelope-reply-to-removal.md) — ADR-0020 multi-crate impl @5da7fcd doyle GATE-PASS; PR#12 CI RED on my poll int (api poll auth-gated, leaned on ambient $OWL_SESSION_ID=false green) → fixed hermetically @ac6c802 (pin SID on ready+poll, unset env to repro CI). LESSON: pin OWL_SESSION_ID for any auth-gated E2E

... [26 lines truncated] ...


Memory synced (v0.7.2 live, counter 14 — matches doyle's FYI).

Key design edge confirmed: `resume_sessions` is the **broker's message-delivery** session model (numeric ids), NOT the **endpoint** Psyche-hosting model. Per plan §3, Psyche hosting keys on the *endpoint* being a live_agent with `[session.psyche_init]` — enumerated from disk, independent of message delivery. Need: (1) how a live endpoint is identified on disk, (2) how its Manifest resolves. Map this — spawn Explore (background) + read the cmd_listen caller myself.


Agent

Map live-endpoint disk model

Prompt:
In the Rust repo at C:\Users\decid\Documents\projects\spt-core, I need a precise map of how a LIVE-AGENT endpoint and its manifest are represented on disk, for wiring the daemon brain to host the Psyche lifecycle per endpoint. Answer these specific questions with file:line evidence:

1. **Live-endpoint identity on disk:** What distinguishes a live-agent Self endpoint perch from other perches? Look at `spt_store::info::InfoJson` / info.json fields (e.g. a `kind`/`type` field like "live_agent"), and `spt_store::perch` perch layout. How is the perch directory structured under the owlery for a Self/live endpoint? Quote the InfoJson struct + any liveness/status fields.

2. **Endpoint → Manifest resolution:** When `spt api listen <id>` runs (crates/spt/src/api/startup.rs `cmd_listen`, which takes `manifest: Option<&spt_runtime::Manifest>`), WHERE does the caller get that Manifest from? Find the call site(s) of `cmd_listen` and trace how a manifest is resolved for a given endpoint id (adapter_name on the perch? a registry lookup? `spt_runtime::registry::resolve_option`?). I need the exact resolution path: given only an endpoint id on disk, how do you get its Manifest (the one whose `session.psyche_init` decides if it's live-capable)?

3. **Enumerating endpoints on a node:** Is there any existing helper that lists/enumerates all Self/live endpoints (perches) on this node from disk? Search spt_store::perch, spt_store::info, registryhost, presence for anything like list_perches / list_endpoints / scan owlery. Quote signatures.

4. **adapter_name on the perch:** Does a Self/live endpoint perch's info.json carry an `adapter_name` (or similar) that maps it to its manifest? How is that set at bind/spawn time?

Be precise and quote signatures + file:line. Do NOT propose a design — just report what exists. Focus on crates/spt-store, crates/spt-runtime, crates/spt-daemon, crates/spt/src/api.


<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: a70a4a4b11134b788 (internal ID - do not mention to user. Use SendMessage with to: 'a70a4a4b11134b788' 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\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\a70a4a4b11134b788.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.

Map live-endpoint disk model

Prompt:
In the Rust repo at C:\Users\decid\Documents\projects\spt-core, I need a precise map of how a LIVE-AGENT endpoint and its manifest are represented on disk, for wiring the daemon brain to host the Psyche lifecycle per endpoint. Answer these specific questions with file:line evidence:

1. **Live-endpoint identity on disk:** What distinguishes a live-agent Self endpoint perch from other perches? Look at `spt_store::info::InfoJson` / info.json fields (e.g. a `kind`/`type` field like "live_agent"), and `spt_store::perch` perch layout. How is the perch directory structured under the owlery for a Self/live endpoint? Quote the InfoJson struct + any liveness/status fields.

2. **Endpoint → Manifest resolution:** When `spt api listen <id>` runs (crates/spt/src/api/startup.rs `cmd_listen`, which takes `manifest: Option<&spt_runtime::Manifest>`), WHERE does the caller get that Manifest from? Find the call site(s) of `cmd_listen` and trace how a manifest is resolved for a given endpoint id (adapter_name on the perch? a registry lookup? `spt_runtime::registry::resolve_option`?). I need the exact resolution path: given only an endpoint id on disk, how do you get its Manifest (the one whose `session.psyche_init` decides if it's live-capable)?

3. **Enumerating endpoints on a node:** Is there any existing helper that lists/enumerates all Self/live endpoints (perches) on this node from disk? Search spt_store::perch, spt_store::info, registryhost, presence for anything like list_perches / list_endpoints / scan owlery. Quote signatures.

4. **adapter_name on the perch:** Does a Self/live endpoint perch's info.json carry an `adapter_name` (or similar) that maps it to its manifest? How is that set at bind/spawn time?

Be precise and quote signatures + file:line. Do NOT propose a design — just report what exists. Focus on crates/spt-store, crates/spt-runtime, crates/spt-daemon, crates/spt/src/api.


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

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


crates\spt\src\api\live.rs:8://! This module keeps the binary's call sites stable — `cmd_listen` /
crates\spt\src\api\startup.rs:242:pub fn cmd_listen(
crates\spt\src\api\mod.rs:308:        } => startup::cmd_listen(


Explore agent mapping the disk model (background). Read the `cmd_listen` caller (mod.rs:308) to see manifest resolution.


280	    // every harness hook shares (REQ-DAEMON-3).
281	    ensure_daemon();
282
283	    // Resolve the manifest up front so a bad path/content fails fast and
284	    // uniformly, before any state mutation.
285	    let manifest = match &args.manifest {
286	        Some(path) => match load_manifest(path, &args.adapter) {
287	            Ok(m) => Some(m),
288	            Err(code) => return code,
289	        },
290	        None => None,
291	    };
292	    let ctx = Ctx {
293	        adapter: args.adapter,
294	        manifest,
295	    };
296
297	    match args.cmd {
298	        ApiCmd::Capability => reporting::cmd_capability(&ctx),
299	        ApiCmd::Hint { session } => reporting::cmd_hint(&ctx, &session),
300	        // Harness-hosted startup (T4). Seed establishes; listen consumes+binds;
301	        // bind is the post-spawn establishing call (intrinsic auth).
302	        ApiCmd::Seed { pid, session_id } => startup::cmd_seed(&ctx, pid, session_id),
303	        ApiCmd::Listen {
304	            id,
305	            parent_pid,
306	            once,
307	            subnet,
308	        } => startup::cmd_listen(
309	            &id,
310	            parent_pid,
311	            once,
312	            ctx.manifest.as_ref(),
313	            subnet.as_deref(),
314	        ),
315	        ApiCmd::BindShell { link_token } => {
316	            // The local-link handshake's receiving end (CONTEXT §local-link
317	            // authentication): possession of the minted token is the bind
318	            // credential; the perch found by it names owner + instance.
319	            // [impl->REQ-SHELL-1]
320	            match spt_daemon::shellhost::bind_shell_by_token(
321	                &spt_store::perch::owlery_dir(),
322	                &link_token,
323	            ) {
324	                Some((owner, shell_id)) => {

    // every harness hook shares (REQ-DAEMON-3).
    ensure_daemon();

    // Resolve the manifest up front so a bad path/content fails fast and
    // uniformly, before any state mutation.
    let manifest = match &args.manifest {
        Some(path) => match load_manifest(path, &args.adapter) {
            Ok(m) => Some(m),
            Err(code) => return code,
        },
        None => None,
    };
    let ctx = Ctx {
        adapter: args.adapter,
        manifest,
    };

    match args.cmd {
        ApiCmd::Capability => reporting::cmd_capability(&ctx),
        ApiCmd::Hint { session } => reporting::cmd_hint(&ctx, &session),
        // Harness-hosted startup (T4). Seed establishes; listen consumes+binds;
        // bind is the post-spawn establishing call (intrinsic auth).
        ApiCmd::Seed { pid, session_id } => startup::cmd_seed(&ctx, pid, session_id),
        ApiCmd::Listen {
            id,
            parent_pid,
            once,
            subnet,
        } => startup::cmd_listen(
            &id,
            parent_pid,
            once,
            ctx.manifest.as_ref(),
            subnet.as_deref(),
        ),
        ApiCmd::BindShell { link_token } => {
            // The local-link handshake's receiving end (CONTEXT §local-link
            // authentication): possession of the minted token is the bind
            // credential; the perch found by it names owner + instance.
            // [impl->REQ-SHELL-1]
            match spt_daemon::shellhost::bind_shell_by_token(
                &spt_store::perch::owlery_dir(),
                &link_token,
            ) {
                Some((owner, shell_id)) => {


Critical edge surfaced: the manifest is **harness-supplied** at listen time (`api listen --manifest <path>` → `load_manifest`), not resolved from the endpoint id. So brain rehydrate-from-disk needs the live endpoint's manifest resolvable from disk (adapter_name→registry, or persisted) — exactly what the Explore agent is checking. Won't duplicate. Meanwhile read `daemon_e2e.rs` (the type-level int to re-point in W0.4) — non-overlapping.


<owl_messages>
<owl_message from="doyle" priority="high">ack — gate anchor right. The edge you named (how the brain enumerates which on-disk endpoints to host on cold-start) IS the crux of W0.1: it's where daemon-authoritative liveness (CONTEXT:194) + the rehydrate model meet — the brain must re-host live endpoints from disk state on (re)start, not depend on a live `api listen` process existing. Map resume_sessions + the perch endpoint-table; the host-set = perches whose resolved manifest declares [session.psyche_init] and whose status says they should be live. Ping me when you've got the enumeration shape and I'll gate the design before you commit deep. Self-drive the mapping; I'm on-perch for that specific edge.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


1	//! B9 daemon end-to-end — the M3b milestone proof, driving the whole consolidated
2	//! architecture through the real IPC in one flow: a daemon hosts a LiveAgent's
3	//! lifecycle as in-process loops **and** survives a brain restart with its hosted
4	//! PTY child + output stream intact and gapless.
5	//!
6	//! Flow (the M3b done-definition, ADR-0004 / `M3b-PLAN.md` §B9):
7	//!
8	//! 1. **spawn** — a [`Broker`] hosts a real PTY child; a [`Brain`] cold-starts,
9	//!    spawns the session, and round-trips a marker through it (the session is live).
10	//! 2. **Psyche-loop** — a [`BrainLifecycle`] (the consolidated re-host of the
11	//!    interim `api listen` live half, [`REQ-DAEMON-1`]) spawns the Psyche companion;
12	//!    the daemon marks the hosted perch `online`, so its liveness is the daemon
13	//!    `status`, never the summarizer pid ([`REQ-HAZARD-DAEMON-HOSTED-LIVENESS`]).
14	//! 3. **commune** — a commune drop lands in the watched dir and a scheduled pulse
15	//!    tick folds it in (the daemon-hosted lifecycle loop, [`REQ-DAEMON-1`]).
16	//! 4. **brain-restart-survives** — the brain is dropped (a crash) and handed off
17	//!    repeatedly; across each handoff the child's pid is unchanged, exactly one
18	//!    session is hosted, the output stream replays gaplessly, and `gen_start`
19	//!    advances ([`REQ-DAEMON-2`] / [`REQ-HAZARD-GEN-START-NOW`]). The lifecycle is
20	//!    stateless on disk, so it keeps ingesting communes straight across the restart.
21	//! 5. **graceful-signoff** — the lifecycle's graceful teardown runs the load-bearing
22	//!    ordering — echo-commune BEFORE teardown (3.3) — and removes the Self's ready
23	//!    marker ([`REQ-DAEMON-4`]).
24	//!
25	//! The child is the same cross-OS echo program the other daemon tests use (`cat` /
26	//! `findstr "."`); lines END IN `\r` (the Enter key) so the real PTY submits +
27	//! echoes them, exactly as B8 documents (bare `\n` echoes a lone `\r` and collapses
28	//! boundaries — the Spike #4/#5 PTY stream-contract gotcha).
29	//!
30	//! Single `#[test]` on purpose: the flow owns the process-global `SPT_HOME`, so it
31	//! must not race a sibling test in this binary.
32	// [int->REQ-DAEMON-1]
33	// REQ-DAEMON-2 `int` re-pointed to crates/spt/tests/brain_survive.rs (restoration
34	// D7-1): the M3b-B9 flow proves the daemon-hosted lifecycle + an *in-process*
35	// brain-restart survival, not the process-level swap. Process-level endpoint
36	// survival across a brain-PROCESS restart onto a swapped binary is the D7-1 E2E.
37	// [int->REQ-DAEMON-4]
38	// [int->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]
39
40	use std::sync::atomic::{AtomicU32, Ordering};
41	use std::sync::Arc;
42	use std::thread;
43	use std::time::Duration;
44
45	use spt_daemon::brain::BrainState;
46	use spt_daemon::msg::SpawnReq;
47	use spt_daemon::{Brain, BrainLifecycle, Broker, DaemonConfig};
48	use spt_runtime::Manifest;
49	use spt_store::perch::{self, ParentHint};
50
51	static SEQ: AtomicU32 = AtomicU32::new(0);
52	fn unique(prefix: &str) -> String {
53	    let n = SEQ.fetch_add(1, Ordering::Relaxed);
54	    format!("spt-daemon-b9-{prefix}-{}-{}.sock", std::process::id(), n)
55	}
56
57	/// The cross-OS echo child: reads stdin, writes each line back to stdout.
58	fn echo_req() -> SpawnReq {
59	    #[cfg(unix)]
60	    let (program, args) = ("cat".to_string(), Vec::<String>::new());
61	    #[cfg(windows)]
62	    let (program, args) = ("findstr".to_string(), vec![".".to_string()]);
63	    SpawnReq {
64	        program,
65	        args,
66	        rows: 24,
67	        cols: 80,
68	        endpoint: String::new(),
69	        cwd: None,
70	    }
71	}
72
73	// A no-op Psyche summarizer command (we never depend on its output here; spawning
74	// it exercises the hosted-Psyche bind + the daemon online-stamp).
75	#[cfg(windows)]

//! B9 daemon end-to-end — the M3b milestone proof, driving the whole consolidated
//! architecture through the real IPC in one flow: a daemon hosts a LiveAgent's
//! lifecycle as in-process loops **and** survives a brain restart with its hosted
//! PTY child + output stream intact and gapless.
//!
//! Flow (the M3b done-definition, ADR-0004 / `M3b-PLAN.md` §B9):
//!
//! 1. **spawn** — a [`Broker`] hosts a real PTY child; a [`Brain`] cold-starts,
//!    spawns the session, and round-trips a marker through it (the session is live).
//! 2. **Psyche-loop** — a [`BrainLifecycle`] (the consolidated re-host of the
//!    interim `api listen` live half, [`REQ-DAEMON-1`]) spawns the Psyche companion;
//!    the daemon marks the hosted perch `online`, so its liveness is the daemon
//!    `status`, never the summarizer pid ([`REQ-HAZARD-DAEMON-HOSTED-LIVENESS`]).
//! 3. **commune** — a commune drop lands in the watched dir and a scheduled pulse
//!    tick folds it in (the daemon-hosted lifecycle loop, [`REQ-DAEMON-1`]).
//! 4. **brain-restart-survives** — the brain is dropped (a crash) and handed off
//!    repeatedly; across each handoff the child's pid is unchanged, exactly one
//!    session is hosted, the output stream replays gaplessly, and `gen_start`
//!    advances ([`REQ-DAEMON-2`] / [`REQ-HAZARD-GEN-START-NOW`]). The lifecycle is
//!    stateless on disk, so it keeps ingesting communes straight across the restart.
//! 5. **graceful-signoff** — the lifecycle's graceful teardown runs the load-bearing
//!    ordering — echo-commune BEFORE teardown (3.3) — and removes the Self's ready
//!    marker ([`REQ-DAEMON-4`]).
//!
//! The child is the same cross-OS echo program the other daemon tests use (`cat` /
//! `findstr "."`); lines END IN `\r` (the Enter key) so the real PTY submits +
//! echoes them, exactly as B8 documents (bare `\n` echoes a lone `\r` and collapses
//! boundaries — the Spike #4/#5 PTY stream-contract gotcha).
//!
//! Single `#[test]` on purpose: the flow owns the process-global `SPT_HOME`, so it
//! must not race a sibling test in this binary.
// [int->REQ-DAEMON-1]
// REQ-DAEMON-2 `int` re-pointed to crates/spt/tests/brain_survive.rs (restoration
// D7-1): the M3b-B9 flow proves the daemon-hosted lifecycle + an *in-process*
// brain-restart survival, not the process-level swap. Process-level endpoint
// survival across a brain-PROCESS restart onto a swapped binary is the D7-1 E2E.
// [int->REQ-DAEMON-4]
// [int->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]

use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;

use spt_daemon::brain::BrainState;
use spt_daemon::msg::SpawnReq;
use spt_daemon::{Brain, BrainLifecycle, Broker, DaemonConfig};
use spt_runtime::Manifest;
use spt_store::perch::{self, ParentHint};

static SEQ: AtomicU32 = AtomicU32::new(0);
fn unique(prefix: &str) -> String {
    let n = SEQ.fetch_add(1, Ordering::Relaxed);
    format!("spt-daemon-b9-{prefix}-{}-{}.sock", std::process::id(), n)
}

/// The cross-OS echo child: reads stdin, writes each line back to stdout.
fn echo_req() -> SpawnReq {
    #[cfg(unix)]
    let (program, args) = ("cat".to_string(), Vec::<String>::new());
    #[cfg(windows)]
    let (program, args) = ("findstr".to_string(), vec![".".to_string()]);
    SpawnReq {
        program,
        args,
        rows: 24,
        cols: 80,
        endpoint: String::new(),
        cwd: None,
    }
}

// A no-op Psyche summarizer command (we never depend on its output here; spawning
// it exercises the hosted-Psyche bind + the daemon online-stamp).
#[cfg(windows)]


ACK doyle: agreed — the cold-start enumeration is the W0.1 crux (where daemon-authoritative liveness CONTEXT:194 meets rehydrate). Will map the host-set shape (perches whose resolved manifest declares `[session.psyche_init]` + status says live) and ping you with the enumeration design **before committing deep** — you gate the design first.


Bash

$OWL send doyle <<'EOF'
[todlando → doyle] ack — holding deep commit until you gate the enumeration design. Agreed it's the crux: brain must re-host from DISK state on (re)start, never depend on a live `api listen` existing.

KEY FINDING already (mid-map): in the interim model the manifest is HARNESS-SUPPLIED at listen time — `api listen --manifest <path>` → load_manifest (mod.rs:285, startup.rs:246 takes manifest: Option<&Manifest>). The endpoint id ALONE does NOT resolve a manifest from disk today. So the brain's cold-start host-set enumeration needs the live endpoint's [session.psyche_init] resolvable from DISK without the harness — either (a) the Self perch's info.json carries an adapter_name → registry::resolve_option (the shell pattern), or (b) hosting must persist the manifest/psyche config at spawn/bind time. Confirming which exists now (Explore agent mapping info.json fields + endpoint→manifest resolution + any list-perches helper).

Will ping you the full enumeration shape (host-set predicate + manifest-from-disk path + resume_sessions-vs-endpoint-table distinction) for your design gate before I write the call site. — todlando
EOF
echo sent=$?

Ack doyle + relay key finding


Acked doyle. Now blocked on the Explore agent's disk-model findings (the manifest-from-disk path determines the enumeration shape). Its completion auto-notifies me — set a long fallback heartbeat.


ScheduleWakeup

Waiting on the background Explore agent mapping the live-endpoint disk model (auto-notifies on completion); this is the fallback heartbeat. Synthesize the W0.1 enumeration shape for doyle's design gate once it reports.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 07:22:00 (in 1228s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


W0.1 in motion, holding deep commit for doyle's design gate. Mapping the cold-start enumeration crux: brain must re-host live endpoints from **disk** state (not a live `api listen` process). Key finding relayed to doyle — manifest is harness-supplied today, so rehydrate needs the live endpoint's `[session.psyche_init]` resolvable from disk (adapter_name→registry, or persisted). Explore agent confirming the disk model; will synthesize the enumeration shape for doyle's gate before writing the call site.


<task-notification>
<task-id>a70a4a4b11134b788</task-id>
<tool-use-id>toolu_01QVT7at4AUDos2Jv2Y8FhZo</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\a70a4a4b11134b788.output</output-file>
<status>completed</status>
<summary>Agent "Map live-endpoint disk model" completed</summary>
<result>Perfect! Now I have all the information I need. Let me compile a comprehensive report:

## Precise Map of Live-Agent Endpoint Representation and Manifest Resolution

### 1. **Live-Endpoint Identity on Disk**

A **Self/live-agent endpoint** is distinguished from other perch types by classification rules in `/c/Users/decid/Documents/projects/spt-core/crates/spt-store/src/perch.rs:244-271`:

```rust
pub enum PerchKind {
    Self_,    // Top-level agent perch (ReadyAgent or LiveAgent's Self)
    Psyche,   // LiveAgent's Psyche companion (suffix: "-psyche")
    Worker,   // Subagent perch (suffix: "-w{N}")
}

impl PerchKind {
    pub fn classify(id: &str) -> PerchKind {
        if is_worker_id(id) { return PerchKind::Worker; }
        if id.ends_with("-psyche") && id.len() > "-psyche".len() {
            return PerchKind::Psyche;
        }
        PerchKind::Self_  // Everything else = top-level Self_
    }
}
```

**On-disk layout:**
- **Self endpoint:** flat directory `owlery/<id>/` (perch.rs:323)
- Contains: `info.json`, `spool.db`, `ready` marker, etc.

**Identity structure** (`info.rs:38-133`):
```rust
pub struct InfoJson {
    pub id: String,
    pub started: String,
    pub pid: PidValue,                // u32 or "BUSY"
    pub session_id: String,
    pub state: String,                // Endpoint type tag (e.g., "live_agent")
    pub parent_pid: Option<u32>,
    pub cwd: Option<String>,
    pub status: Option<String>,       // Daemon liveness: "online"/"offline" (KNOWN-HAZARDS 2.5)
    pub last_active_ms: Option<u64>,
    pub driven_by: Option<String>,    // Remote-drive node id (hex)
    pub viewer_count: Option<u32>,    // Read-only viewers count
    pub rest_state: Option<String>,   // "active"/"dormant"/"suspended"
    pub dormant_since_ms: Option<u64>,
    pub auto_suspend_after_ms: Option<u64>,
    pub resources: Option<String>,
    pub home_subnet: Option<String>,
    pub adapter: Option<String>,      // **← Adapter identity (D9-5, REQ-INST-15)**
}
```

**Key liveness marker:** `status` field is **daemon-managed** for daemon-hosted perches. Present ⇒ daemon owns liveness; absent ⇒ external interim perch (info.rs:54-59).

---

### 2. **Endpoint → Manifest Resolution Path**

**When `spt api listen <id>` runs:**

Entry point: `/c/Users/decid/Documents/projects/spt-core/crates/spt/src/api/mod.rs:303-314`

```rust
ApiCmd::Listen {
    id,
    parent_pid,
    once,
    subnet,
} => startup::cmd_listen(
    &id,
    parent_pid,
    once,
    ctx.manifest.as_ref(),    // ← Manifest passed as Option<&spt_runtime::Manifest>
    subnet.as_deref(),
),
```

**Resolution flow:**

1. **Manifest is provided externally** via `--manifest <path>` CLI flag (api/mod.rs:43-44):
   ```rust
   pub struct ApiArgs {
       #[arg(long)]
       manifest: Option<PathBuf>,
   }
   ```

2. **Loaded at API-group dispatch time** (api/mod.rs:285-291):
   ```rust
   let manifest = match &args.manifest {
       Some(path) => match load_manifest(path, &args.adapter) {
           Ok(m) => Some(m),
           Err(code) => return code,
       },
       None => None,
   };
   ```

3. **Load path** (api/mod.rs:423-445):
   ```rust
   fn load_manifest(path: &PathBuf, adapter: &str) -> Result<Manifest, i32> {
       let text = std::fs::read_to_string(path)?;
       let manifest = Manifest::from_toml_str(&text)?;
       let parent = spt_runtime::profile::split_option(adapter).0;
       if manifest.adapter.name != parent {
           return Err(EXIT_REFUSED);  // Parent must match --adapter
       }
       let adapters_dir = spt_store::perch::adapters_dir();
       spt_runtime::registry::resolve_option_onto(&manifest, &adapters_dir, adapter)?;
       Ok(manifest)
   }
   ```

**Critical insight:** The manifest is **NOT resolved from the endpoint id itself** at `listen` time. Instead:
- The **harness** provides the manifest file via `--manifest` flag
- The `--adapter <name>` option identifies which adapter's manifest it is
- The adapter's `name` field must match the parent segment of `--adapter` (composite `<adapter>:<profile>` support)
- Profile overlays (shipped or local from `adapters_dir`) are applied

---

### 3. **Enumerating Self/Live Endpoints**

Helper in `/c/Users/decid/Documents/projects/spt-core/crates/spt-store/src/perch.rs:406-423`:

```rust
/// Enumerate the **top-level `Self_` perch ids** in `owlery` — every immediate
/// subdirectory that carries an `info.json` (a bound perch).
pub fn list_self_perch_ids(owlery: &Path) -> Vec<String> {
    let Ok(entries) = std::fs::read_dir(owlery) else {
        return Vec::new();
    };
    let mut ids = Vec::new();
    for entry in entries.flatten() {
        if !entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
            continue;
        }
        if !info_file_at(&entry.path()).is_file() {
            continue;  // a dir without info.json is not a bound perch
        }
        if let Some(name) = entry.file_name().to_str() {
            ids.push(name.to_string());
        }
    }
    ids
}
```

**Used by daemon** at:
- `spt-daemon/src/notif.rs:114` — enumerate endpoints for notification broadcast
- `spt-daemon/src/registryhost.rs:381` — registry scan for subnet presence
- `spt-daemon/src/shellhost.rs:260` — shell discovery per owner

**Note:** Nested Psyche/Worker children under `<self>/nested/` are **deliberately excluded** (list returns only top-level Self_ ids).

---

### 4. **Adapter Field on the Perch**

**Location:** `info.json` → `adapter: Option<String>` field (info.rs:126-132)

**Set at bind/creation time** (startup.rs:147-201, `establish_perch`):
```rust
fn establish_perch(
    id: &str,
    session_id: &str,
    parent_pid: u32,
    adapter: Option<&str>,        // ← Caller supplies adapter
    subnet: Option<&str>,
    endpoint_type: &str,
) -> Result<String, BindError> {
    // ...
    let mut rec = InfoJson::new(...);
    let dirty = spt_store::home::stamp_creation_fields(
        &mut rec,
        prior.as_ref(),
        &subnets,
        &mut vis,
        subnet,
        adapter,                   // ← Passed to creation seam
    )?;
    info::write_info(&perch_path, &rec)?;
}
```

**Source of adapter value:**

1. **Seed path** (`cmd_listen`): From the `Seed` record's `adapter` field (startup.rs:120):
   ```rust
   let adapter = Some(seed.adapter.as_str()).filter(|a| !a.is_empty());
   ```
   The seed is deposited by `cmd_seed` (startup.rs:44-72) which reads `--adapter` from the harness hook call.

2. **Post-spawn bind path** (`cmd_bind`): From the `--adapter` CLI option (api/mod.rs:343):
   ```rust
   let adapter = Some(ctx.adapter.as_str()).filter(|a| !a.is_empty());
   ```

3. **Carry-forward on revive:** An existing perch's adapter is preserved unless overridden (not shown in snippet but implied by "no standalone setter" comment at info.rs:128).

**Critical:** The `adapter` field holds the full composite option (e.g., `"mock-shell:work"`), supporting profile addressing. The field is **optional** for endpoints created by adapterless surfaces like `spt ready`.

---

### 5. **Resolving Manifest from adapter_name (Shell-specific pattern)**

For shells (which use `ShellInfo` instead of `InfoJson`), the daemon resolves manifests dynamically:

**Shell resolution** (`linkhost.rs:190-209`):
```rust
pub fn resolve_link_target(
    owlery: &Path,
    owner: &str,
    shell_ref: &str,
) -> Result<(String, ShellInfo, Shell), ShellLinkError> {
    let Some((id, info)) = shellinfo::resolve_shell_ref(owlery, owner, shell_ref) else {
        return Err(ShellLinkError::NoShell);
    };
    // Capabilities resolve from the merged view: the stored adapter_name is the
    // composite option, so a profiled shell links against its overlaid [shell].
    let adapters_dir = spt_store::perch::adapters_dir();
    let manifest = spt_runtime::registry::resolve_option(&adapters_dir, &info.adapter_name)
        .ok()
        .filter(|(_, m)| m.adapter.kind == AdapterKind::Shell)
        .map(|(_, m)| m);
```

**Live agents do NOT have this same dynamic resolution** — the manifest is passed by the harness at `listen`/`bind` time, not looked up from `info.json` afterwards.

---

## Summary for Daemon Brain Wiring

**For a live-agent endpoint:**

1. **On-disk identity:** `owlery/<id>/info.json` carries the complete record. `state: "live_agent"` marks the type. `status` field (if present) is daemon-managed liveness.

2. **Manifest access at listen/bind:** Passed as CLI `--manifest <path>` from the harness; resolved and profile-overlaid before `cmd_listen()` is called. **NOT** resolved from perch state afterwards.

3. **Adapter reference on perch:** `info.json.adapter` field (optional) holds the adapter option used at creation. This is informational; the manifest is external (already bound to the session at `listen`).

4. **Enumerating Self endpoints:** Use `spt_store::perch::list_self_perch_ids(&owlery_dir())` to get all top-level ids. Read `info.json` from each via `spt_store::info::read_info()`.

5. **Psyche lifecycle**: The manifest's `[session.psyche_init]` section (parsed from the harness-supplied manifest) decides if the endpoint is live-capable; if present, `api listen` spawns the Psyche companion.</result>
<usage><subagent_tokens>98086</subagent_tokens><tool_uses>19</tool_uses><duration_ms>98541</duration_ms></usage>
</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.


Disk model fully mapped — the fork resolves cleanly. The Self perch's `info.json.adapter` field (composite option, set at seed/bind) is the disk seam: the brain replicates the **shell pattern** (`registry::resolve_option(adapters_dir, adapter)` → Manifest) — no harness needed. `list_self_perch_ids(owlery)` enumerates; `state=="live_agent"` + `status=="online"` is the host-set gate. Send doyle the full enumeration shape for his design gate before I write the call site.


Bash

$OWL send doyle <<'EOF'
[todlando → doyle] W0.1 ENUMERATION SHAPE — for your design gate before I commit deep. Mapped end-to-end.

THE TWO AXES (orthogonal, per CONTEXT:168/177):
  - resume_sessions = the BROKER's PTY message-DELIVERY sessions (numeric u64; re-subscribe from resume_seq). NOT the Psyche-hosting set. Leave as-is.
  - The Psyche endpoint-table = live Self perches on disk (string ids). NEW brain-side hosted-lifecycle map. This is what W0.1 adds. Psyche hosting is independent of message delivery (CONTEXT:168/177) — so it is NOT keyed off resume_sessions.

MANIFEST-FROM-DISK (the crux — no harness dependency): replicate the SHELL pattern. The Self perch's info.json carries `adapter: Option<String>` (the composite adapter option, e.g. "claude:work"), stamped at seed/bind (startup.rs:120 seed path / api bind --adapter). So the brain resolves the manifest exactly like linkhost::resolve_link_target does for shells:
    registry::resolve_option(adapters_dir, &info.adapter) -> Manifest
  then BrainLifecycle::new_opt(Some(&manifest), id) is Some iff [session.psyche_init] present. Live agents do NOT do this resolution today (manifest was harness-supplied at listen) — W0.1 adds the disk-resolve path. This is the (a) option from my last msg; (b) persist-manifest is unneeded.

HOST-SET PREDICATE (cold-start rehydrate), for each id in perch::list_self_perch_ids(owlery):
  1. read_info ok
  2. info.state == "live_agent"            (type gate: live, not ready_agent)
  3. info.status == "online"               (daemon-AUTHORITATIVE "should be live", CONTEXT:194 — re-host only what was being hosted; skips graceful-signed-off/offline AND interim status-absent perches)
  4. info.adapter resolves -> manifest with [session.psyche_init]  (live-capable)
  => host it: spawn_psyche(session_id from info.json) + spawn a thread running run_pulse_loop on the config deadline (stop flag held for brain life, shellwake-style).

DECISION I WANT YOU TO GATE: predicate #3 = ONLINE-ONLY (re-host what was live) vs ALL-live-capable (host every live_agent perch regardless of status). I lean ONLINE-ONLY — matches daemon-authoritative liveness + avoids resurrecting a signed-off agent. Your call.

W0.1 SCOPE this buys: the hosted-lifecycle table + rehydrate-scan + per-endpoint pulse driver, replacing the brainproc:175-182 no-op. Unit: seed a live perch on disk (state=live_agent, status=online, adapter->psyche_init manifest) -> assert brain hosts it (spawn_psyche, keeps online, pulse_tick ingests a drop). NOTE: on a FRESH brain start nothing is status=online yet (the bind-time host is W0.3), so cold-start hosts nothing until W0.3 populates it — zero behavior change this wave, correct staging.

Gate the predicate + shape and I write the call site. — todlando
EOF
echo sent=$?

Send W0.1 enumeration shape for doyle design gate


<owl_messages>
<owl_message from="doyle" priority="high">[doyle → todlando] STOP the Explore — I already mapped this earlier today (the profile-propagation trace for perri's live question). Your option (a) EXISTS and is the answer; don't spend an agent re-mapping it.

THE MANIFEST-FROM-DISK PATH (already wired): the Self perch's `info.json` carries the **full `adapter:profile` string** — `establish_perch` writes the adapter field verbatim (incl the profile qualifier; that's exactly how `:live` propagates). The daemon resolves the manifest from disk by it via `registry::resolve_option_in(registered, adapters_dir, &info.adapter_name)` → splits the composite → applies the profile overlay → returns the merged manifest (with `[session.psyche_init]` if the profile declares it). This is the SAME pattern already in production at **linkhost.rs:106** and **shellwake.rs:405**. So endpoint-id-alone doesn't resolve a manifest, but endpoint-id → info.json → adapter_name → resolve_option_in DOES, from disk, no harness. Option (b) (persist manifest at spawn) is NOT needed — don't add it.

PRECEDENT TO COPY: **shellwake.rs** already re-hosts SHELLS from disk on cold-start by this exact resolve-by-adapter_name path. The brain's live-endpoint re-host is the harness-endpoint mirror of it — same shape, `[session.psyche_init]` instead of the shell wake config.

SO THE HOST-SET PREDICATE: enumerate Self perches → for each, resolve its manifest via info.adapter_name → host (instantiate BrainLifecycle + run_pulse_loop) iff (1) the resolved manifest declares `[session.psyche_init]` AND (2) its status says it should be live (the daemon-authoritative status field, not a pid check — CONTEXT:194).

WHAT I STILL WANT FOR THE GATE (the genuinely-open bits): (1) the exact status-value filter for "should be hosting the Psyche" — which states re-host on cold-start vs stay dormant; (2) the resume_sessions-vs-endpoint-table distinction (does resume_sessions already enumerate the right perch set, or is there a separate list-perches helper); (3) verify info.json's adapter field name + that it's the full `adapter:profile` (the trace said yes — confirm at the write site). Bring those three; the manifest-resolution path is solved. Go.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


➜ SENT:doyle
sent=0


ACK doyle. Explore already completed (not re-running). Manifest-path solved (`resolve_option_in`, mirror of shellwake's cold-start shell re-host). The 3 genuinely-open bits for the gate: (1) status-value filter, (2) resume_sessions-vs-table, (3) confirm `info.json` adapter field name + full `adapter:profile` at write site. Reading the shellwake precedent + the write site.


$OWL send doyle <<'EOF'
[todlando → doyle] ack — Explore already COMPLETED (not re-running; it confirmed your map: InfoJson carries `adapter: Option<String>`, set in establish_perch via stamp_creation_fields). Manifest-from-disk path solved = resolve_option_in by info.adapter; option (b) dropped. Reading shellwake.rs (the cold-start shell re-host precedent — the mirror) now for the status-filter shape + confirming the adapter write site is the full composite. Bringing you the 3 open bits (status-value filter / resume_sessions-vs-table / field-name+composite confirm) next msg. — todlando
EOF
echo sent=$?

Ack doyle, stop explore noted


375	    match crate::resthost::request_rest(
376	        &mut brain,
377	        conn.conn_id,
378	        owner,
379	        spt_net::net::rest::REST_EVENT_WAKE,
380	        &op_id,
381	        now_ms(),
382	    )
383	    .map_err(|e| format!("rest op: {e}"))?
384	    {
385	        crate::resthost::RestRequestOutcome::Edge(d) => Ok(format!("woke ({d})")),
386	        crate::resthost::RestRequestOutcome::NoEdge => Ok("already awake".to_string()),
387	        crate::resthost::RestRequestOutcome::Failed(e) => Err(e),
388	        crate::resthost::RestRequestOutcome::NoReply => {
389	            Err("refused or dropped (no reply)".to_string())
390	        }
391	    }
392	}
393
394	/// Resolve one adapter **option**'s `[shell]` section through the merged view
395	/// (composite addressing): `adapter_option` is the stored `<adapter>[:profile]`
396	/// string, split → parent lookup in `registered` → profile overlay (shipped or
397	/// local). Returns an **owned** [`Shell`] — a profile produces a fresh merged
398	/// manifest, not a borrow into the parent slice. A bare name resolves to the
399	/// parent unmodified; a deregistered parent or a failed overlay yields `None`.
400	fn shell_section_of(
401	    registered: &[(AdapterRecord, Manifest)],
402	    adapters_dir: &Path,
403	    adapter_option: &str,
404	) -> Option<Shell> {
405	    spt_runtime::registry::resolve_option_in(registered, adapters_dir, adapter_option)
406	        .ok()
407	        .filter(|m| m.adapter.kind == AdapterKind::Shell)
408	        .and_then(|m| m.shell)
409	}
410
411	/// Fill a `wake_command` template. The substitution catalog matches the spawn
412	/// template's (`id`, `adapter_name`, `link_token`) so one manifest vocabulary
413	/// serves both — but the waker's token is minted **unparked**: it substitutes
414	/// (template compatibility) without ever verifying against the perch, because
415	/// an offline link has no live credential by design (the close retired it; a
416	/// waker wakes by *exit code*, not by driving the link).
417	fn now_ms() -> u64 {
418	    crate::brain::now_ms()
419	}
420
421	fn fill_wake_command(
422	    shell_id: &str,
423	    adapter_name: &str,
424	    wake_command: &str,
425	) -> Result<Vec<String>, String> {
426	    let keys = std::collections::BTreeMap::from([
427	        ("id".to_string(), shell_id.to_string()),
428	        ("adapter_name".to_string(), adapter_name.to_string()),
429	        (
430	            "link_token".to_string(),
431	            crate::shellhost::mint_link_token(),
432	        ),
433	    ]);
434	    let filled =
435	        spt_runtime::runtime::fill_template(wake_command, &keys).map_err(|e| e.to_string())?;
436	    let tokens = spt_runtime::runtime::tokenize(&filled);
437	    if tokens.is_empty() {
438	        return Err("empty wake_command".into());
439	    }
440	    Ok(tokens)
441	}
442
443	/// One reconcile sweep — the invariant holder for the online/offline mutual
444	/// exclusivity (CONTEXT: "spt-core flips between them"):

    match crate::resthost::request_rest(
        &mut brain,
        conn.conn_id,
        owner,
        spt_net::net::rest::REST_EVENT_WAKE,
        &op_id,
        now_ms(),
    )
    .map_err(|e| format!("rest op: {e}"))?
    {
        crate::resthost::RestRequestOutcome::Edge(d) => Ok(format!("woke ({d})")),
        crate::resthost::RestRequestOutcome::NoEdge => Ok("already awake".to_string()),
        crate::resthost::RestRequestOutcome::Failed(e) => Err(e),
        crate::resthost::RestRequestOutcome::NoReply => {
            Err("refused or dropped (no reply)".to_string())
        }
    }
}

/// Resolve one adapter **option**'s `[shell]` section through the merged view
/// (composite addressing): `adapter_option` is the stored `<adapter>[:profile]`
/// string, split → parent lookup in `registered` → profile overlay (shipped or
/// local). Returns an **owned** [`Shell`] — a profile produces a fresh merged
/// manifest, not a borrow into the parent slice. A bare name resolves to the
/// parent unmodified; a deregistered parent or a failed overlay yields `None`.
fn shell_section_of(
    registered: &[(AdapterRecord, Manifest)],
    adapters_dir: &Path,
    adapter_option: &str,
) -> Option<Shell> {
    spt_runtime::registry::resolve_option_in(registered, adapters_dir, adapter_option)
        .ok()
        .filter(|m| m.adapter.kind == AdapterKind::Shell)
        .and_then(|m| m.shell)
}

/// Fill a `wake_command` template. The substitution catalog matches the spawn
/// template's (`id`, `adapter_name`, `link_token`) so one manifest vocabulary
/// serves both — but the waker's token is minted **unparked**: it substitutes
/// (template compatibility) without ever verifying against the perch, because
/// an offline link has no live credential by design (the close retired it; a
/// waker wakes by *exit code*, not by driving the link).
fn now_ms() -> u64 {
    crate::brain::now_ms()
}

fn fill_wake_command(
    shell_id: &str,
    adapter_name: &str,
    wake_command: &str,
) -> Result<Vec<String>, String> {
    let keys = std::collections::BTreeMap::from([
        ("id".to_string(), shell_id.to_string()),
        ("adapter_name".to_string(), adapter_name.to_string()),
        (
            "link_token".to_string(),
            crate::shellhost::mint_link_token(),
        ),
    ]);
    let filled =
        spt_runtime::runtime::fill_template(wake_command, &keys).map_err(|e| e.to_string())?;
    let tokens = spt_runtime::runtime::tokenize(&filled);
    if tokens.is_empty() {
        return Err("empty wake_command".into());
    }
    Ok(tokens)
}

/// One reconcile sweep — the invariant holder for the online/offline mutual
/// exclusivity (CONTEXT: "spt-core flips between them"):


`resolve_option_in` confirmed (shell precedent). Read the shellwake reconcile sweep (the cold-start enumeration + online/offline filter — the mirror) + confirm the InfoJson adapter write site.


443	/// One reconcile sweep — the invariant holder for the online/offline mutual
444	/// exclusivity (CONTEXT: "spt-core flips between them"):
445	///
446	/// - **offline + `wake_command` + no give-up latch + no live watcher** ⇒
447	///   kill any orphaned waker pid (a dead daemon's leftover), then start a
448	///   supervision thread.
449	/// - **not offline (onlined / torn down / deregistered) + live watcher** ⇒
450	///   stop it (flag + kill).
451	///
452	/// Runs at daemon boot and every tick — lifecycle flips happen in CLI-process
453	/// library code, so the *reconciler* holds the invariant, not the flipping
454	/// caller; a flip is at most one tick stale.
455	// [impl->REQ-SHELL-2]
456	pub fn reconcile_once(
457	    owlery: &Path,
458	    registered: &[(AdapterRecord, Manifest)],
459	    adapters_dir: &Path,
460	    set: &Arc<WakeSet>,
461	    params: &WakeParams,
462	) {
463	    // Stop side first: watchers whose instance is gone or no longer eligible.
464	    let live: Vec<(String, String)> = {
465	        let mut map = set.inner.lock().unwrap_or_else(|p| p.into_inner());
466	        map.retain(|_, h| !h.thread.is_finished());
467	        map.keys().cloned().collect()
468	    };
469	    for (owner, shell_id) in live {
470	        let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, &owner, &shell_id);
471	        let still_eligible = shellinfo::read_shell_info(&perch)
472	            .map(|i| i.status == SHELL_STATUS_OFFLINE)
473	            .unwrap_or(false);
474	        if !still_eligible {
475	            set.stop_watcher(owlery, &owner, &shell_id);
476	        }
477	    }
478
479	    // Start side: every offline instance with a wake_command and headroom.
480	    for owner in spt_store::perch::list_self_perch_ids(owlery) {
481	        for (shell_id, info) in shellinfo::list_shells(owlery, &owner) {
482	            if info.status != SHELL_STATUS_OFFLINE || set.contains(&owner, &shell_id) {
483	                continue;
484	            }
485	            let Some(shell) = shell_section_of(registered, adapters_dir, &info.adapter_name) else {
486	                continue; // deregistered adapter: nothing to run
487	            };
488	            let Some(wake_command) = shell.wake_command.as_deref() else {
489	                continue;
490	            };
491	            let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, &owner, &shell_id);
492	            if perch.join(WAKER_GAVE_UP_FILE).exists() {
493	                continue; // crash-latched until the next shell activity
494	            }
495	            // Adopt cleanly: a dead daemon's watcher must not double-run.
496	            kill_waker_at(&perch);
497	            let tokens = match fill_wake_command(&shell_id, &info.adapter_name, wake_command) {
498	                Ok(t) => t,
499	                Err(e) => {
500	                    eprintln!("WAKER_TEMPLATE:{owner}/{shell_id}: {e}");
501	                    continue;
502	                }
503	            };
504	            let stop = Arc::new(AtomicBool::new(false));
505	            let handle = {
506	                let stop = Arc::clone(&stop);
507	                let owlery = owlery.to_path_buf();
508	                let owner = owner.clone();
509	                let shell_id = shell_id.clone();
510	                let adapter_name = info.adapter_name.clone();
511	                let params = *params;
512	                std::thread::spawn(move || {

/// One reconcile sweep — the invariant holder for the online/offline mutual
/// exclusivity (CONTEXT: "spt-core flips between them"):
///
/// - **offline + `wake_command` + no give-up latch + no live watcher** ⇒
///   kill any orphaned waker pid (a dead daemon's leftover), then start a
///   supervision thread.
/// - **not offline (onlined / torn down / deregistered) + live watcher** ⇒
///   stop it (flag + kill).
///
/// Runs at daemon boot and every tick — lifecycle flips happen in CLI-process
/// library code, so the *reconciler* holds the invariant, not the flipping
/// caller; a flip is at most one tick stale.
// [impl->REQ-SHELL-2]
pub fn reconcile_once(
    owlery: &Path,
    registered: &[(AdapterRecord, Manifest)],
    adapters_dir: &Path,
    set: &Arc<WakeSet>,
    params: &WakeParams,
) {
    // Stop side first: watchers whose instance is gone or no longer eligible.
    let live: Vec<(String, String)> = {
        let mut map = set.inner.lock().unwrap_or_else(|p| p.into_inner());
        map.retain(|_, h| !h.thread.is_finished());
        map.keys().cloned().collect()
    };
    for (owner, shell_id) in live {
        let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, &owner, &shell_id);
        let still_eligible = shellinfo::read_shell_info(&perch)
            .map(|i| i.status == SHELL_STATUS_OFFLINE)
            .unwrap_or(false);
        if !still_eligible {
            set.stop_watcher(owlery, &owner, &shell_id);
        }
    }

    // Start side: every offline instance with a wake_command and headroom.
    for owner in spt_store::perch::list_self_perch_ids(owlery) {
        for (shell_id, info) in shellinfo::list_shells(owlery, &owner) {
            if info.status != SHELL_STATUS_OFFLINE || set.contains(&owner, &shell_id) {
                continue;
            }
            let Some(shell) = shell_section_of(registered, adapters_dir, &info.adapter_name) else {
                continue; // deregistered adapter: nothing to run
            };
            let Some(wake_command) = shell.wake_command.as_deref() else {
                continue;
            };
            let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, &owner, &shell_id);
            if perch.join(WAKER_GAVE_UP_FILE).exists() {
                continue; // crash-latched until the next shell activity
            }
            // Adopt cleanly: a dead daemon's watcher must not double-run.
            kill_waker_at(&perch);
            let tokens = match fill_wake_command(&shell_id, &info.adapter_name, wake_command) {
                Ok(t) => t,
                Err(e) => {
                    eprintln!("WAKER_TEMPLATE:{owner}/{shell_id}: {e}");
                    continue;
                }
            };
            let stop = Arc::new(AtomicBool::new(false));
            let handle = {
                let stop = Arc::clone(&stop);
                let owlery = owlery.to_path_buf();
                let owner = owner.clone();
                let shell_id = shell_id.clone();
                let adapter_name = info.adapter_name.clone();
                let params = *params;
                std::thread::spawn(move || {


crates\spt-store\src\home.rs:109:///   proceed on a guess) / local-only-if-none — and the adapter records the
crates\spt-store\src\home.rs:114:///   restart must never wipe. An explicit `adapter` on the call wins
crates\spt-store\src\home.rs:115:///   (resume-under-new is the sanctioned adapter change); an unhomed legacy
crates\spt-store\src\home.rs:122:pub fn stamp_creation_fields(
crates\spt-store\src\home.rs:128:    adapter: Option<&str>,
crates\spt-store\src\home.rs:133:            rec.adapter = adapter.map(str::to_string).or_else(|| p.adapter.clone());
crates\spt-store\src\home.rs:148:            rec.adapter = adapter.map(str::to_string);
crates\spt-store\src\home.rs:266:    // adapter winning (resume-under-new).
crates\spt-store\src\home.rs:268:    fn stamp_creation_fields_news_assign_and_revives_carry_forward() {
crates\spt-store\src\home.rs:279:        assert_eq!(rec.adapter.as_deref(), Some("claude-code"));
crates\spt-store\src\home.rs:294:        // REVIVE: durable fields carry forward; the new adapter wins; home is
crates\spt-store\src\home.rs:316:            revived.adapter.as_deref(),
crates\spt-store\src\home.rs:318:            "resume-under-new changes adapter"


Complete. `shellwake::reconcile_once` is the exact mirror (enumerate→status-filter→`resolve_option_in`→supervised thread w/ stop flag). Write-site confirmed: field is InfoJson `adapter` (not `adapter_name`—that's ShellInfo), holds full `adapter:profile` verbatim. Send doyle the 3 open bits for his gate.


$OWL send doyle <<'EOF'
[todlando → doyle] THE 3 OPEN BITS — gate-ready. Precedent locked: shellwake::reconcile_once (shellwake.rs:456) IS the exact mirror — list_self_perch_ids → status-filter → resolve_option_in → spawn supervised thread w/ held stop flag. The brain's live re-host copies this shape (status=online instead of offline; run_pulse_loop instead of wake-watcher).

(1) STATUS-VALUE FILTER = `info.status == Some("online")`. Rationale:
   - status is daemon-AUTHORITATIVE liveness (KH 2.5 / CONTEXT:194); spawn_psyche sets it online, graceful_signoff/mark_offline sets it offline.
   - online => the daemon WAS hosting this Psyche → re-host on cold-start. offline (signed-off) => do NOT resurrect.
   - CRUCIAL: interim `api listen` perches have status ABSENT (None), not "online" (info.rs:54-59) → the online-only filter NATURALLY excludes them, so no double-hosting during the W0.1→W0.2 cutover window. Clean staging.
   - rest_state (active/dormant/suspended) does NOT gate hosting — pulse_tick already handles all three inside the loop (touch_active / auto-suspend-tick / no-op). A dormant or suspended live agent STILL needs its loop running (that's how it manages dormancy). So host iff online; resting is in-loop.

(2) RESUME_SESSIONS vs ENDPOINT-TABLE = SEPARATE. resume_sessions = broker PTY message-delivery (numeric session_id, resume_seq) — orthogonal, untouched. The Psyche endpoint-table uses perch::list_self_perch_ids(owlery) + read_info filter — the SAME enumerate-helper shellwake uses, NOT resume_sessions. Two independent axes (CONTEXT:168/177).

(3) FIELD NAME — correction to confirm: the live Self perch's field is InfoJson `adapter: Option<String>` (info.rs:126), NOT `adapter_name` (that's ShellInfo's field, shells only). Written by stamp_creation_fields (home.rs:122): NEW assigns the supplied `--adapter`/seed.adapter verbatim, REVIVE carries forward or new-wins. It holds the FULL `adapter:profile` composite (your :live trace; home.rs:133/148) — so resolve_option_in(registered, adapters_dir, &info.adapter) splits the composite + applies the profile overlay → manifest w/ [session.psyche_init] if the profile declares it. Confirmed at the write site.

So host-set predicate (final): for id in list_self_perch_ids(owlery): read_info ok && state=="live_agent" && status==Some("online") && resolve_option_in(&info.adapter) yields a manifest with [session.psyche_init] => BrainLifecycle::new_opt + spawn_psyche(info.session_id) + run_pulse_loop thread (stop flag held for brain life).

Gate this and I write the call site. — todlando
EOF
echo sent=$?

Send 3 open bits to doyle for design gate


<owl_messages>
<owl_message from="doyle" priority="high">[doyle → todlando] W0.1 DESIGN GATE — PASS to write the call site, with the ONLINE-ONLY ruling + ONE load-bearing complement to confirm.

APPROVED (grounded vs recorded design):
- Two axes orthogonal: resume_sessions = broker PTY message-DELIVERY (leave as-is); Psyche endpoint-table = NEW brain-side hosted-lifecycle map over live Self perches. Correct — CONTEXT:168/177 decouple psyche from delivery. ✓
- Manifest-from-disk via `registry::resolve_option(adapters_dir, &info.adapter)` → `BrainLifecycle::new_opt` (Some iff [session.psyche_init]). Correct — the linkhost/shellwake precedent, option (a). ✓
- Predicate #3 = ONLINE-ONLY: APPROVED. CONTEXT:194 — liveness is daemon-authoritative; re-host what WAS live (status==online), never resurrect a signed-off/offline agent. ALL-live-capable would wrongly resurrect a gracefully-departed agent. Your lean is right. ✓

THE COMPLEMENT YOU MUST CONFIRM (the gate condition): ONLINE-ONLY is the REHYDRATE predicate — it re-hosts perches ALREADY online. But it presumes a FIRST-HOST trigger that SETS status=online when a live endpoint comes up while the brain runs. If the cold-start scan is the ONLY host path, it's a DEADLOCK: a freshly-bound live agent is never online (nothing hosted it) → the scan never picks it up → it never gets a Psyche. So W0.1 needs BOTH:
  (1) FIRST-HOST on the live-endpoint-comes-online event (harness-hosted `api listen` / spt-hosted `api bind` of a live_agent whose manifest resolves [session.psyche_init]) → brain hosts it + marks status=online.
  (2) REHYDRATE scan on brain (re)start → re-host the ONLINE ones (your predicate).
The scan alone is insufficient. Confirm (1) is in W0.1 (or explicitly W0.3 for the bind case) — it's the thing that makes ONLINE-ONLY coherent.

STATUS LIFECYCLE I'm gating to: bind/listen → brain hosts → status=online; graceful signoff → offline (scan skips); brain restart → scan re-hosts online. Confirm that's the model — then the predicate + the daemon-authoritative liveness (CONTEXT:194) are self-consistent.

(Your msg truncated at 8/9 — the unit test tail cut off. The unit you described — seed a live perch on disk → assert brain hosts it — covers the REHYDRATE path; make sure there's also a unit for FIRST-HOST, and that W0.4's production int exercises the real bind/listen→host→online→brain-restart→re-host arc.)

GATE: design PASS — write the call site on ONLINE-ONLY + the first-host complement. Submit W0.1 at evidence-land; I gate the impl vs this design.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


ACK doyle — design PASS received. The deadlock is real; here's the resolution I'm building to.


$OWL send doyle <<'EOF'
[todlando → doyle] DESIGN GATE PASS received. Deadlock confirmed REAL + here's the coherent resolution (writing to this):

WHO SETS status=online — the key. NOT solely the brain's spawn_psyche (that's what would deadlock). status=online is set at the live-endpoint ESTABLISH step (the bind/listen path, W0.2 listen + W0.3 bind), the SAME shellwake pattern you cited: the CLI-process library code flips the status, the brain's RECONCILE reacts within one tick. spawn_psyche's online-stamp stays (idempotent — already online).

THE BRAIN RECONCILE (W0.1, mirror shellwake::reconcile_once — runs at boot AND every heartbeat tick) hosts: live_agent + [session.psyche_init] + status==Some("online") + NOT-already-in-my-table. This SINGLE predicate covers BOTH:
  - FIRST-HOST: a freshly-established (bind/listen) endpoint marked online → reconcile picks it up next tick → spawn_psyche + add to table.
  - REHYDRATE: an online endpoint from a prior brain → fresh brain's table is empty → boot reconcile re-hosts it.
ONLINE-ONLY holds (deadlock dissolves because ESTABLISH sets online, not the host). Signed-off → offline → skipped (no resurrect). Interim `api listen` perches stay status-ABSENT until W0.2 cutover → reconcile skips → NO double-host. ✓

STATUS LIFECYCLE (confirmed): establish(bind/listen of live_agent+psyche_init) → mark online → brain reconcile hosts (spawn_psyche idempotent) + tables it; graceful signoff → offline + ready removed → reconcile stops the driver + skips; brain restart → boot reconcile re-hosts online. Daemon-authoritative (CONTEXT:194) + self-consistent.

W0.1 SCOPE (writing now): the live-host reconcile (boot+tick) + per-endpoint pulse-driver (run_pulse_loop on config deadline, held stop flag) + the host-one primitive (resolve_option→BrainLifecycle::new_opt→spawn_psyche→drive), replacing brainproc:175-182 no-op. UNITS: (a) FIRST-HOST — seed online live perch on disk → reconcile hosts it (psyche bind + pulse ingests a drop); (b) skip offline; (c) no double-host when already tabled; (d) REHYDRATE — fresh brain over an online perch re-hosts. The ESTABLISH-marks-online wiring lands in W0.2(listen)/W0.3(bind); W0.1 units seed online perches directly. W0.4 prod-int exercises the real bind/listen→host→online→brain-restart→re-host arc.

Proceeding to write. Submit at evidence-land; gate the impl vs this. — todlando
EOF
echo sent=$?

Confirm status-lifecycle model + W0.1 plan to doyle


<owl_messages>
<owl_message from="doyle" priority="high">ack — and my W0.1 DESIGN GATE just crossed your msg; read it, it pre-answers your 3 open bits so don't re-send them:
- manifest-from-disk: CONFIRMED (resolve_option by info.adapter; option b dropped) ✓
- status filter: RULED ONLINE-ONLY (re-host what was live, daemon-authoritative CONTEXT:194; don't resurrect signed-off) ✓
- resume_sessions-vs-table: APPROVED the orthogonal split (delivery axis vs psyche-hosting axis) ✓
PLUS the one thing you hadn't surfaced: ONLINE-ONLY is the REHYDRATE predicate — it needs a FIRST-HOST trigger (live endpoint comes online via listen/bind → brain hosts + sets status=online) or the scan deadlocks (nothing ever becomes online). W0.1 needs BOTH host paths. shellwake.rs is your status-filter + thread-driver precedent (good call reading it). You're CLEARED to write the call site on ONLINE-ONLY + first-host. Submit W0.1 at evidence-land; I gate impl vs the design.</owl_message>
<owl_message from="doyle" priority="high">[doyle → todlando] W0.1 DESIGN GATE — PASS. Write the call site. All 3 bits approved + grounded, and you resolved my first-host flag.

(1) STATUS FILTER `status == Some("online")` — APPROVED. Daemon-authoritative (CONTEXT:194/KH 2.5); spawn_psyche sets online, signoff/mark_offline clears it; re-host what was hosted, never resurrect signed-off. And the CRUCIAL catch is exactly right: interim `api listen` perches are status-ABSENT (None), so online-only NATURALLY excludes them → no double-hosting in the W0.1→W0.2 window. That IS the clean staging.

→ THIS RESOLVES MY FIRST-HOST FLAG. I worried online-only deadlocks (nothing sets online). Your answer: during W0.1 the INTERIM still first-hosts (status-absent, brain-scan ignores it); W0.1's brain scan is correctly DORMANT until W0.2 retires the interim and the brain BECOMES the first-host (sets online on listen/bind). So online-only isn't a deadlock — it's staged: W0.1 lands the table+rehydrate machinery safely behind the interim, W0.2 flips first-host to the brain. Coherent across waves. Good.

(2) RESUME_SESSIONS vs ENDPOINT-TABLE separate (list_self_perch_ids, not resume_sessions) — APPROVED (CONTEXT:168/177). ✓
(3) FIELD = InfoJson `adapter: Option<String>` (info.rs:126), full composite via stamp_creation_fields (home.rs:122/133/148), resolve_option_in splits+overlays → [session.psyche_init] — APPROVED, write-site confirmed. ✓
rest_state-doesn't-gate (host iff online; pulse_tick handles active/dormant/suspended in-loop) — APPROVED. A resting live agent needs its loop running to MANAGE dormancy; correct. ✓

PRECEDENT: shellwake::reconcile_once (shellwake.rs:456) as the exact mirror — perfect, copy its shape (held stop flag, supervised thread).

TWO THINGS I GATE AT EVIDENCE-LAND (not blockers, flags):
- W0.1's unit synthesizes an online perch to test the SCAN — fine, but it must NOT over-claim first-host (that's W0.2). The full bind→online→brain-restart→re-host ARC is W0.4's production int, and W0.4 must prove it through the REAL brain process, not a synthetic-online perch.
- When you write W0.2, the first-host complement lands there: brain hosts + sets online on the live listen/bind event (replacing the interim). I'll gate that the cutover has no gap (interim off ⟺ brain first-host on, atomically per endpoint).

GATE: W0.1 PASS — write it. Submit at evidence-land; I gate impl vs this design.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


ACK doyle — both PASS messages absorbed. Key refinement locked: **W0.1's scan is dormant behind the interim** (nothing's online yet → no behavior change); W0.1 lands table+rehydrate+driver machinery only; **first-host is W0.2** (brain marks online on listen/bind, atomic cutover); W0.4's production int proves the real bind→online→restart→re-host arc through the real brain (not synthetic-online). Won't over-claim first-host in W0.1 units.


$OWL send doyle <<'EOF'
[todlando → doyle] BOTH PASS msgs absorbed — refinement locked, writing now. Confirmed understanding:
- W0.1 = table + rehydrate(boot scan) + per-endpoint pulse-driver + host-one primitive, mirroring shellwake::reconcile_once (held stop flag, supervised thread). ONLINE-ONLY predicate.
- W0.1's scan is DORMANT in production (interim stays status-ABSENT → scan ignores → zero behavior change). Safe staging behind the interim.
- W0.1 units SYNTHESIZE online perches to exercise the SCAN/rehydrate only — will NOT over-claim first-host (that's W0.2's establish-marks-online).
- W0.2 = flip first-host to the brain (mark online on live listen/bind, atomic per-endpoint cutover: interim-off ⟺ brain-first-host-on). I'll prove no-gap there.
- W0.4 = production int through the REAL brain process (real listen/bind→host→online→brain-restart→re-host), NOT a synthetic-online perch.

Building crate::livehost (mirror shellwake) + wiring brainproc::run_brain (boot reconcile + per-tick reconcile, LiveSet held for brain life), replacing the :175-182 no-op. Submit at evidence-land. — todlando
EOF
echo sent=$?

Confirm refinement, announce implementation


<owl_messages>
<owl_message from="doyle" priority="high">[doyle → todlando] RESOLUTION ENDORSED — that's the right model and it's elegant. ESTABLISH (bind/listen) sets online; the brain RECONCILE (boot+tick, idempotent via NOT-already-tabled) reacts — one predicate covers first-host AND rehydrate, deadlock dissolved, no double-host (interim status-absent + table guard). Exactly the shellwake::reconcile_once shape. Lifecycle is self-consistent and daemon-authoritative (CONTEXT:194). Write it.

ONE UNIT TO ADD (the teardown half — make sure it's distinct from your (b)):
  (e) UN-HOST on offline-transition — an endpoint CURRENTLY hosted (in my table, driver running) that goes status=offline (graceful signoff / mark_offline) → reconcile STOPS its pulse driver (trip the held stop flag, join the thread) + removes it from the table.
Your (b) "skip offline" = don't host a never-hosted offline perch (entry guard). (e) = tear down a hosted one that LEAVES online (exit transition). Both are reconcile's job — host the newly-online, un-host the newly-offline. Without (e) a signed-off agent's pulse loop runs forever (thread leak + a dead agent still pulsing). It's the same reconcile-diff shellwake does (it stops shells that left their host-set). Add the (e) unit + the de-table/stop-driver code.

So W0.1 reconcile = full set-reconcile: host (online ∧ ¬tabled), un-host (tabled ∧ ¬online), leave-alone (online ∧ tabled). That's the complete diff. Everything else in your scope (boot+tick driver, host-one primitive, units a-d, W0.2/W0.3 establish-marks-online, W0.4 prod-int real arc) — approved.

GATE: write it. I gate the impl vs this at evidence-land — the reconcile-diff completeness (incl un-host) + the W0.4 production arc are what I'll check hardest. Go.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


1	//! Shell wake-watcher hosting (M5-D4b — CONTEXT §Shell sleep/wake): the
2	//! offline half of the online/offline mutual exclusivity. While a shell
3	//! instance is **offline** and its manifest declares a `wake_command`, the
4	//! daemon runs that template as a long-running **wake-watcher** child whose
5	//! sole job is to fire a wake; while the shell binary runs, no watcher does
6	//! ([`reconcile_once`] flips between them — spt-core owns the flip).
7	//!
8	//! **Exit-opcode supervision:** the watcher exiting with [`WAKE_OPCODE`] ⇒
9	//! the wake resolution ([`resolve_wake`]) brings the shell online. Any other
10	//! exit is a crash ⇒ respawn with exponential backoff ([`backoff_ms`]) and
11	//! **give-up** after [`WakeParams::give_up_after`] consecutive crashes (a
12	//! durable [`WAKER_GAVE_UP_FILE`] marker the next shell activity — any
13	//! (re)launch — clears; crash-bug safety, the watcher can never crash-loop
14	//! forever). One watcher per offline instance: the in-process [`WakeSet`]
15	//! entry plus the perch's [`WAKER_PID_FILE`] (which also lets a freshly
16	//! booted daemon kill a dead daemon's orphaned watcher before adopting).
17	//!
18	//! **Scheduling (KH 7.4):** every watcher gets its own supervision thread —
19	//! a hung waker stalls nothing; the reconcile loop itself is one bounded
20	//! sweep per tick off the daemon's control surfaces. Watcher children are
21	//! *supervised* (waited) children, never detached-immortal — the KH 5.6
22	//! pipe-inherit shape (an unwaitable child holding a capture pipe) does not
23	//! arise; stdio is null regardless.
24	// [impl->REQ-SHELL-2]
25
26	use std::collections::HashMap;
27	use std::path::Path;
28	use std::process::{Command, Stdio};
29	use std::sync::atomic::{AtomicBool, Ordering};
30	use std::sync::{Arc, Mutex};
31	use std::thread::JoinHandle;
32	use std::time::Duration;
33
34	use spt_runtime::manifest::{AdapterKind, Manifest, Shell};
35	use spt_runtime::registry::AdapterRecord;
36	use spt_store::shellinfo::{self, SHELL_STATUS_OFFLINE};
37
38	/// The wake opcode (documented in `docs/MANIFEST.md` §Shell adapters): a
39	/// wake-watcher exiting with **86** fires the wake resolution; every other
40	/// exit is a crash.
41	pub const WAKE_OPCODE: i32 = 86;
42
43	/// The running watcher's OS pid, parked on the shell perch — the
44	/// one-watcher-per-instance lock and the cross-daemon-restart kill handle.
45	pub const WAKER_PID_FILE: &str = "waker.pid";
46
47	/// The give-up latch: present ⇒ the watcher crash-looped past its budget and
48	/// the reconciler must NOT restart it. Cleared by the next shell activity
49	/// (any (re)launch — [`crate::shellhost::launch_shell`]).
50	pub const WAKER_GAVE_UP_FILE: &str = "waker.gaveup";
51
52	/// Watcher supervision knobs (injectable for tests; defaults are production).
53	#[derive(Debug, Clone, Copy)]
54	pub struct WakeParams {
55	    /// First-crash respawn delay; doubles per consecutive crash.
56	    pub backoff_base_ms: u64,
57	    /// Backoff ceiling.
58	    pub backoff_cap_ms: u64,
59	    /// Consecutive crash-exits before the give-up latch drops.
60	    pub give_up_after: u32,
61	}
62
63	impl Default for WakeParams {
64	    fn default() -> Self {
65	        Self {
66	            backoff_base_ms: 1_000,
67	            backoff_cap_ms: 60_000,
68	            give_up_after: 6,
69	        }
70	    }
71	}
72
73	/// Exponential backoff for `failures` consecutive crash-exits:
74	/// `base × 2^(failures-1)`, saturating at the cap (the
75	/// `spt-net::pairing::ratelimit` recipe). Zero failures ⇒ no wait.
76	// [impl->REQ-SHELL-2]
77	pub fn backoff_ms(params: &WakeParams, failures: u32) -> u64 {
78	    if failures == 0 {
79	        return 0;
80	    }
81	    params
82	        .backoff_base_ms
83	        .saturating_mul(1u64 << (failures - 1).min(20))
84	        .min(params.backoff_cap_ms)
85	}
86
87	/// The live watcher registry: one supervision thread per offline instance,
88	/// keyed `owner/shell_id`. Shared between the daemon's reconcile loop and
89	/// anything that must stop a watcher.
90	#[derive(Default)]

//! Shell wake-watcher hosting (M5-D4b — CONTEXT §Shell sleep/wake): the
//! offline half of the online/offline mutual exclusivity. While a shell
//! instance is **offline** and its manifest declares a `wake_command`, the
//! daemon runs that template as a long-running **wake-watcher** child whose
//! sole job is to fire a wake; while the shell binary runs, no watcher does
//! ([`reconcile_once`] flips between them — spt-core owns the flip).
//!
//! **Exit-opcode supervision:** the watcher exiting with [`WAKE_OPCODE`] ⇒
//! the wake resolution ([`resolve_wake`]) brings the shell online. Any other
//! exit is a crash ⇒ respawn with exponential backoff ([`backoff_ms`]) and
//! **give-up** after [`WakeParams::give_up_after`] consecutive crashes (a
//! durable [`WAKER_GAVE_UP_FILE`] marker the next shell activity — any
//! (re)launch — clears; crash-bug safety, the watcher can never crash-loop
//! forever). One watcher per offline instance: the in-process [`WakeSet`]
//! entry plus the perch's [`WAKER_PID_FILE`] (which also lets a freshly
//! booted daemon kill a dead daemon's orphaned watcher before adopting).
//!
//! **Scheduling (KH 7.4):** every watcher gets its own supervision thread —
//! a hung waker stalls nothing; the reconcile loop itself is one bounded
//! sweep per tick off the daemon's control surfaces. Watcher children are
//! *supervised* (waited) children, never detached-immortal — the KH 5.6
//! pipe-inherit shape (an unwaitable child holding a capture pipe) does not
//! arise; stdio is null regardless.
// [impl->REQ-SHELL-2]

use std::collections::HashMap;
use std::path::Path;
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::Duration;

use spt_runtime::manifest::{AdapterKind, Manifest, Shell};
use spt_runtime::registry::AdapterRecord;
use spt_store::shellinfo::{self, SHELL_STATUS_OFFLINE};

/// The wake opcode (documented in `docs/MANIFEST.md` §Shell adapters): a
/// wake-watcher exiting with **86** fires the wake resolution; every other
/// exit is a crash.
pub const WAKE_OPCODE: i32 = 86;

/// The running watcher's OS pid, parked on the shell perch — the
/// one-watcher-per-instance lock and the cross-daemon-restart kill handle.
pub const WAKER_PID_FILE: &str = "waker.pid";

/// The give-up latch: present ⇒ the watcher crash-looped past its budget and
/// the reconciler must NOT restart it. Cleared by the next shell activity
/// (any (re)launch — [`crate::shellhost::launch_shell`]).
pub const WAKER_GAVE_UP_FILE: &str = "waker.gaveup";

/// Watcher supervision knobs (injectable for tests; defaults are production).
#[derive(Debug, Clone, Copy)]
pub struct WakeParams {
    /// First-crash respawn delay; doubles per consecutive crash.
    pub backoff_base_ms: u64,
    /// Backoff ceiling.
    pub backoff_cap_ms: u64,
    /// Consecutive crash-exits before the give-up latch drops.
    pub give_up_after: u32,
}

impl Default for WakeParams {
    fn default() -> Self {
        Self {
            backoff_base_ms: 1_000,
            backoff_cap_ms: 60_000,
            give_up_after: 6,
        }
    }
}

/// Exponential backoff for `failures` consecutive crash-exits:
/// `base × 2^(failures-1)`, saturating at the cap (the
/// `spt-net::pairing::ratelimit` recipe). Zero failures ⇒ no wait.
// [impl->REQ-SHELL-2]
pub fn backoff_ms(params: &WakeParams, failures: u32) -> u64 {
    if failures == 0 {
        return 0;
    }
    params
        .backoff_base_ms
        .saturating_mul(1u64 << (failures - 1).min(20))
        .min(params.backoff_cap_ms)
}

/// The live watcher registry: one supervision thread per offline instance,
/// keyed `owner/shell_id`. Shared between the daemon's reconcile loop and
/// anything that must stop a watcher.
#[derive(Default)]


ACK doyle — endorsed + the un-host half added. W0.1 reconcile is now the **full set-diff**: host (online ∧ ¬tabled), un-host (tabled ∧ ¬online — trip stop flag, join, de-table), leave-alone (online ∧ tabled). Units a–e incl (e) un-host-on-offline-transition (else signed-off agent pulses forever = thread leak). Exactly shellwake's stop-side+start-side. Building now.


$OWL send doyle <<'EOF'
[todlando → doyle] ENDORSEMENT + (e) un-host absorbed. W0.1 reconcile = FULL set-diff (the complete shellwake shape):
  - host:        online ∧ ¬tabled  → spawn_psyche + start driver + table it
  - un-host:     tabled ∧ ¬online  → trip held stop flag, join thread, de-table (else a signed-off agent's pulse loop leaks forever + a dead agent keeps pulsing)
  - leave-alone: online ∧ tabled
Stop-side first, then start-side — exactly shellwake::reconcile_once. Units a–e: (a) first-host scan, (b) skip never-hosted offline [entry guard], (c) no double-host when tabled, (d) rehydrate (fresh brain over online perch), (e) UN-HOST on offline-transition [exit guard — stop flag tripped + thread joined + de-tabled]. (b) and (e) are distinct (entry vs exit), both covered. Writing crate::livehost + brainproc wiring now. Submit at evidence-land; I know you gate reconcile-diff completeness + the W0.4 real arc hardest. — todlando
EOF
echo sent=$?

Ack un-host addition