305	}
306
307	/// The terminal outcome of the picker — the single action enum every screen
308	/// converges on. `run()` dispatches each variant through the EXISTING bringup
309	/// fns (`cmd_endpoint_run` / `cmd_fork` / the rc pump) — no second bringup path
310	/// (the gate invariant).
311	#[derive(Debug, Clone, PartialEq, Eq)]
312	pub enum Outcome {
313	    /// Bring up (or resume) an endpoint then start/attach/view — routes through
314	    /// `cmd_endpoint_run` (the one bringup core).
315	    Run {
316	        adapter: String,
317	        id: String,
318	        resume: Option<String>,
319	        mode: RunMode,
320	    },
321	    /// Attach to an ALREADY-LIVE endpoint's PTY — routes through the rc pump
322	    /// (`rc::run_attach`), not a re-bringup. `intent` carries the
323	    /// status-conditional choice (Control / Viewer / Take), so View / Attach /
324	    /// Kick all ride the SAME dispatch (single-bringup-path — REQ-KICK-1).
325	    Attach {
326	        id: String,
327	        intent: spt_net::net::attach::AttachIntent,
328	    },
329	    /// Write/update a `spt-<id>` shortcut, then stay (re-enter the loop).
330	    Shortcut {
331	        adapter: String,
332	        id: String,
333	        resume: Option<String>,
334	        mode: RunMode,
335	    },
336	    /// User backed all the way out.
337	    Cancelled,
338	}
339
340	/// The terminal action a Run baked: start (return), attach (default), or view
341	/// (read-only). Maps 1:1 onto `cmd_endpoint_run`'s start/attach/view flags.
342	#[derive(Debug, Clone, Copy, PartialEq, Eq)]
343	pub enum RunMode {
344	    Start,

}

/// The terminal outcome of the picker — the single action enum every screen
/// converges on. `run()` dispatches each variant through the EXISTING bringup
/// fns (`cmd_endpoint_run` / `cmd_fork` / the rc pump) — no second bringup path
/// (the gate invariant).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Outcome {
    /// Bring up (or resume) an endpoint then start/attach/view — routes through
    /// `cmd_endpoint_run` (the one bringup core).
    Run {
        adapter: String,
        id: String,
        resume: Option<String>,
        mode: RunMode,
    },
    /// Attach to an ALREADY-LIVE endpoint's PTY — routes through the rc pump
    /// (`rc::run_attach`), not a re-bringup. `intent` carries the
    /// status-conditional choice (Control / Viewer / Take), so View / Attach /
    /// Kick all ride the SAME dispatch (single-bringup-path — REQ-KICK-1).
    Attach {
        id: String,
        intent: spt_net::net::attach::AttachIntent,
    },
    /// Write/update a `spt-<id>` shortcut, then stay (re-enter the loop).
    Shortcut {
        adapter: String,
        id: String,
        resume: Option<String>,
        mode: RunMode,
    },
    /// User backed all the way out.
    Cancelled,
}

/// The terminal action a Run baked: start (return), attach (default), or view
/// (read-only). Maps 1:1 onto `cmd_endpoint_run`'s start/attach/view flags.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RunMode {
    Start,


740	    }
741
742	    pub fn move_resume(&mut self, delta: isize) {
743	        let len = self.resume_rows().len();
744	        self.resume_cursor = Self::step(self.resume_cursor, len, delta);
745	    }
746
747	    pub fn selected_resume(&self) -> Option<&ResumeRow> {
748	        self.resume_rows().get(self.resume_cursor)
749	    }
750
751	    /// Resume-from-history outcome: bring the endpoint up against the chosen
752	    /// prior session id (`cmd_endpoint_run --resume`). `headless` (the `h` keybind,
753	    /// W4 UX) brings it up detached (`RunMode::Start`); the default ATTACHES
754	    /// (`RunMode::Attach`) so a resume lands you back in the live session.
755	    // [impl->REQ-RUN-PICKER]
756	    pub fn resume_outcome(&self, headless: bool) -> Option<Outcome> {
757	        let session_id = self.selected_resume()?.session_id.clone();
758	        let ep = self.selected_endpoint()?;
759	        Some(Outcome::Run {
760	            adapter: ep.adapter_profile.clone(),
761	            id: ep.id.clone(),
762	            resume: Some(session_id),
763	            mode: if headless { RunMode::Start } else { RunMode::Attach },
764	        })
765	    }
766
767	    /// Re-enter the create-new adapter screen, the spec's "L2@create-new"
768	    /// path shared by Change-harness-adapter / Instantiate-locally (keep the id)
769	    /// and Fork (fresh id). All three converge on `cmd_endpoint_run` — one
770	    /// bringup core. `keep_id` carries the existing endpoint's id forward.
771	    pub fn reenter_create(&mut self, keep_id: bool) {
772	        let id = if keep_id {
773	            self.selected_endpoint().map(|e| e.id.clone()).unwrap_or_default()
774	        } else {

    }

    pub fn move_resume(&mut self, delta: isize) {
        let len = self.resume_rows().len();
        self.resume_cursor = Self::step(self.resume_cursor, len, delta);
    }

    pub fn selected_resume(&self) -> Option<&ResumeRow> {
        self.resume_rows().get(self.resume_cursor)
    }

    /// Resume-from-history outcome: bring the endpoint up against the chosen
    /// prior session id (`cmd_endpoint_run --resume`). `headless` (the `h` keybind,
    /// W4 UX) brings it up detached (`RunMode::Start`); the default ATTACHES
    /// (`RunMode::Attach`) so a resume lands you back in the live session.
    // [impl->REQ-RUN-PICKER]
    pub fn resume_outcome(&self, headless: bool) -> Option<Outcome> {
        let session_id = self.selected_resume()?.session_id.clone();
        let ep = self.selected_endpoint()?;
        Some(Outcome::Run {
            adapter: ep.adapter_profile.clone(),
            id: ep.id.clone(),
            resume: Some(session_id),
            mode: if headless { RunMode::Start } else { RunMode::Attach },
        })
    }

    /// Re-enter the create-new adapter screen, the spec's "L2@create-new"
    /// path shared by Change-harness-adapter / Instantiate-locally (keep the id)
    /// and Fork (fresh id). All three converge on `cmd_endpoint_run` — one
    /// bringup core. `keep_id` carries the existing endpoint's id forward.
    pub fn reenter_create(&mut self, keep_id: bool) {
        let id = if keep_id {
            self.selected_endpoint().map(|e| e.id.clone()).unwrap_or_default()
        } else {


crates\spt-live\src\signoff.rs:245:    fn boundary_writes_resume_commune() {
crates\spt-live\src\resume.rs:160:    // in the cwd (the nested perch dir).
crates\spt-live\src\resume.rs:173:             [session.psyche_resume]\ncommand='{cmd}'\ncwd=\"{{psyche_dir}}\"\n"
crates\spt-term\tests\surface.rs:87:// working directory: a child that prints its cwd reports the dir we passed.
crates\spt-term\tests\surface.rs:93:fn spawn_program_in_lands_the_child_in_the_requested_cwd() {
crates\spt-term\tests\surface.rs:110:    .expect("spawn pwd child in the requested cwd");
crates\spt-term\tests\surface.rs:114:    reader.read_to_end(&mut out).expect("read child cwd to EOF");
crates\spt-term\tests\surface.rs:118:        "child's pwd must be the requested cwd (leaf {leaf:?}), got: {text:?}"
crates\spt-live\src\psyche.rs:92:/// role's `cwd`), and `psyche_prompt` (composed here). The role's
crates\spt-live\src\psyche.rs:149:    // The nested perch dir must exist before spawn — the role's `cwd` is it.
crates\spt-live\src\psyche.rs:209:    // marker file in its cwd (the nested perch dir) — proves both that the perch
crates\spt-live\src\psyche.rs:210:    // is the cwd and that the guard env reached the child.
crates\spt-live\src\psyche.rs:219:             [session.psyche_init]\ncommand='{GUARD_CMD}'\ncwd=\"{{psyche_dir}}\"\n\
crates\spt-live\src\psyche.rs:244:            // The detached child writes the guard value into its cwd; poll for it.
crates\spt\tests\livehost_bootrace_e2e.rs:169:         [session.psyche_init]\ncommand='psychebin ready bootrace-resident-psyche'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
crates\spt\tests\unhost_psyche_reap_e2e.rs:96:         [session.psyche_init]\ncommand='psychebin ready unhost-reap-resident-psyche'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
crates\spt\tests\livehost_bootgate_e2e.rs:95:         [session.psyche_init]\ncommand='psychebin ready bootgate-phantom-psyche'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
crates\spt\tests\ready_resume_ledger_e2e.rs:85:         [session.psyche_init]\ncommand='{NOOP}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
crates\spt\tests\live_resolve_e2e.rs:65:         [session.psyche_init]\ncommand='{NOOP}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
crates\spt-runtime\src\runtime.rs:319:    /// Build a `Command` from a role: fill the template, tokenize, apply cwd,
crates\spt-runtime\src\runtime.rs:341:        if let Some(cwd) = &role.cwd {
crates\spt-runtime\src\runtime.rs:342:            cmd.current_dir(fill_template(cwd, keys)?);
crates\spt-daemon\tests\broker.rs:54:        cwd: None,
crates\spt-daemon\tests\broker.rs:355:            cwd: None,
crates\spt-daemon\tests\broker.rs:437:            cwd: None,
crates\spt-daemon\tests\broker.rs:567:            cwd: None,
crates\spt-term\src\reader.rs:188:    fn dsr_matcher_finds_query_across_chunk_boundary() {
crates\spt\tests\live_firsthost_e2e.rs:82:         [session.psyche_init]\ncommand='{NOOP}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
crates\spt-daemon\tests\brain_swap.rs:60:        cwd: None,
crates\spt\tests\dummy_harness_e2e.rs:124:         [session.psyche_init]\ncommand='psychebin ready {{id}}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
crates\spt\tests\dummy_harness_e2e.rs:371:         [session.psyche_init]\ncommand='psychebin ready {{id}}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
crates\spt-term\src\pty.rs:109:    /// Spawn `program` with `args` under a PTY sized to `size`, in `cwd` when
crates\spt-term\src\pty.rs:111:    /// spawned child) — `None` inherits the caller's cwd, identical to
crates\spt-term\src\pty.rs:118:        cwd: Option<D>,
crates\spt-term\src\pty.rs:126:        Self::spawn_program_in_env(program, args, size, cwd, std::iter::empty::<(&str, &str)>())
crates\spt-term\src\pty.rs:140:        cwd: Option<D>,
crates\spt-term\src\pty.rs:166:        if let Some(dir) = cwd {
crates\spt-term\src\pty.rs:167:            cmd.cwd(dir);
crates\spt\tests\live_bind_firsthost_e2e.rs:75:         [session.psyche_init]\ncommand='{NOOP}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
crates\spt-runtime\src\manifest.rs:277:    pub cwd: Option<String>,

[Showing results with pagination = limit: 40]


Need the ledger append callsite (boundary write path) + ResumeRow build in data.rs.


crates\spt-daemon\tests\two_origin_spanning.rs:81:    spt_store::sessions::append(
crates\spt-daemon\tests\two_origin_spanning.rs:83:        &SessionEntry {
crates\spt-daemon\tests\two_origin_spanning.rs:90:    spt_store::sessions::append(
crates\spt-daemon\tests\two_origin_spanning.rs:92:        &SessionEntry {
crates\spt\tests\ready_resume_ledger_e2e.rs:112:        rows = spt_store::sessions::read_all(&perch);
crates\spt-daemon\src\digest.rs:196:    let ledger = spt_store::sessions::last_k(perch_path, SPAN_SESSIONS);
crates\spt-daemon\src\digest.rs:530:            spt_store::sessions::append(
crates\spt-daemon\src\digest.rs:532:                &spt_store::sessions::SessionEntry {
crates\spt-daemon\src\digest.rs:539:            spt_store::sessions::append(
crates\spt-daemon\src\digest.rs:541:                &spt_store::sessions::SessionEntry {
crates\spt-msg\src\ready.rs:112:            let _ = spt_store::sessions::append(
crates\spt-msg\src\ready.rs:114:                &spt_store::sessions::SessionEntry {
crates\spt-msg\src\ready.rs:331:        let rows = spt_store::sessions::read_all(&perch_path);
crates\spt-msg\src\ready.rs:338:        let offline_rows = spt_store::sessions::read_all(&perch_path);
crates\spt\src\picker\data.rs:331:    let mut rows: Vec<ResumeRow> = sessions::last_k(perch_path, sessions::MAX_LEDGER)
crates\spt\src\picker\data.rs:333:        .map(|e| ResumeRow {
crates\spt\src\picker\model.rs:136:/// Built from `spt_store::sessions::SessionEntry` in `data.rs`.
crates\spt\src\picker\model.rs:138:pub struct ResumeRow {
crates\spt\src\picker\model.rs:148:impl ResumeRow {
crates\spt\src\picker\model.rs:911:        let with_proj = ResumeRow {
crates\spt\src\picker\model.rs:918:        let no_proj = ResumeRow {
crates\spt\src\picker\model.rs:1011:        offline_local.resume_rows = vec![ResumeRow {
crates\spt\src\picker\model.rs:1166:            ResumeRow { session_id: "old".into(), ts: "t1".into(), trigger: "boot".into(), project: String::new() },
crates\spt\src\picker\model.rs:1167:            ResumeRow { session_id: "new".into(), ts: "t2".into(), trigger: "clear".into(), project: String::new() },
crates\spt\src\picker\view.rs:561:        e.resume_rows = vec![ResumeRow {
crates\spt\src\picker\view.rs:585:        e.resume_rows = vec![ResumeRow {
crates\spt\src\api\reporting.rs:66:    let _ = spt_store::sessions::append(
crates\spt\src\api\reporting.rs:68:        &spt_store::sessions::SessionEntry {
crates\spt\src\api\reporting.rs:731:        let rows = spt_store::sessions::read_all(&perch);
crates\spt\src\api\startup.rs:271:        let _ = spt_store::sessions::append(
crates\spt\src\api\startup.rs:273:            &spt_store::sessions::SessionEntry {
crates\spt-store\src\sessions.rs:59:pub struct SessionEntry {
crates\spt-store\src\sessions.rs:118:    fn entry(sid: &str, trigger: SessionTrigger) -> SessionEntry {
crates\spt-store\src\sessions.rs:119:        SessionEntry {


40	/// notifs the endpoint hasn't seen surface into its spool here.
41	// [impl->REQ-API-2]
42	pub fn cmd_boundary(id: &str, mode: &str, to_session: &str) -> i32 {
43	    if to_session.is_empty() {
44	        eprintln!("USAGE: boundary requires a non-empty --to-session-id");
45	        return EXIT_REFUSED;
46	    }
47	    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
48	    let Some(mut rec) = info::read_info(&perch_path) else {
49	        eprintln!("NO_ENDPOINT:{id} has no perch to rebind");
50	        return EXIT_REFUSED;
51	    };
52	    // Keep id / started / parent_pid / cwd / state; rotate only the session id.
53	    rec.session_id = to_session.to_string();
54	    if let Err(e) = info::write_info(&perch_path, &rec) {
55	        eprintln!("BOUNDARY_FAIL:{id}: {e}");
56	        return EXIT_REFUSED;
57	    }
58	    // Append the rotated session to the ledger (REQ-TERM-6) so the digest spans
59	    // this boundary — enumerating the last K sessions, not just the new one. An
60	    // unknown mode falls back to a `clear` trigger. Best-effort — a ledger
61	    // problem must never fail the boundary itself.
62	    let trigger = match mode {
63	        "compact" => spt_store::sessions::SessionTrigger::Compact,
64	        _ => spt_store::sessions::SessionTrigger::Clear,
65	    };
66	    let _ = spt_store::sessions::append(
67	        &perch_path,
68	        &spt_store::sessions::SessionEntry {
69	            ts: spt_store::timefmt::now_rfc3339(),
70	            session_id: to_session.to_string(),
71	            trigger,
72	        },
73	    );
74	    eprintln!("BOUNDARY:{mode}:{id} -> {to_session}");
75	    resurface_notifs(id);
76	    0
77	}
78
79	/// Resurface undismissed notifs to `id` at a reported boundary (D8b — the
80	/// `api boundary` and new-session points; the wake edge rides D9). Best-effort
81	/// by design: a notif problem must never fail the boundary/bind itself.
82	// [impl->REQ-NOTIF-1]
83	pub(super) fn resurface_notifs(id: &str) {
84	    resurface_notifs_with(id, None)

/// notifs the endpoint hasn't seen surface into its spool here.
// [impl->REQ-API-2]
pub fn cmd_boundary(id: &str, mode: &str, to_session: &str) -> i32 {
    if to_session.is_empty() {
        eprintln!("USAGE: boundary requires a non-empty --to-session-id");
        return EXIT_REFUSED;
    }
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    let Some(mut rec) = info::read_info(&perch_path) else {
        eprintln!("NO_ENDPOINT:{id} has no perch to rebind");
        return EXIT_REFUSED;
    };
    // Keep id / started / parent_pid / cwd / state; rotate only the session id.
    rec.session_id = to_session.to_string();
    if let Err(e) = info::write_info(&perch_path, &rec) {
        eprintln!("BOUNDARY_FAIL:{id}: {e}");
        return EXIT_REFUSED;
    }
    // Append the rotated session to the ledger (REQ-TERM-6) so the digest spans
    // this boundary — enumerating the last K sessions, not just the new one. An
    // unknown mode falls back to a `clear` trigger. Best-effort — a ledger
    // problem must never fail the boundary itself.
    let trigger = match mode {
        "compact" => spt_store::sessions::SessionTrigger::Compact,
        _ => spt_store::sessions::SessionTrigger::Clear,
    };
    let _ = spt_store::sessions::append(
        &perch_path,
        &spt_store::sessions::SessionEntry {
            ts: spt_store::timefmt::now_rfc3339(),
            session_id: to_session.to_string(),
            trigger,
        },
    );
    eprintln!("BOUNDARY:{mode}:{id} -> {to_session}");
    resurface_notifs(id);
    0
}

/// Resurface undismissed notifs to `id` at a reported boundary (D8b — the
/// `api boundary` and new-session points; the wake edge rides D9). Best-effort
/// by design: a notif problem must never fail the boundary/bind itself.
// [impl->REQ-NOTIF-1]
pub(super) fn resurface_notifs(id: &str) {
    resurface_notifs_with(id, None)


250	    // W3 (REQ-HAZARD-BIND-CWD-UNSET): record the working directory so the picker
251	    // can group this endpoint under its PROJECT (data.rs → project_id_for_dir).
252	    // Previously NEVER set on bind — the refuted v0.12.1 "fresh agent shows under
253	    // its project" P1. spt-hosted: cmd_bind passes its own current_dir (the broker
254	    // spawned it in project_cwd); harness-hosted: bind_from_seed passes the seed's
255	    // captured cwd. A revive carries the prior cwd forward (a re-bind must not wipe
256	    // it), like home/adapter/controllable.
257	    // [impl->REQ-HAZARD-BIND-CWD-UNSET]
258	    rec.cwd = cwd
259	        .map(str::to_string)
260	        .or_else(|| prior.as_ref().and_then(|p| p.cwd.clone()));
261	    info::write_info(&perch_path, &rec).map_err(|e| BindError::Io(e.to_string()))?;
262	    std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), "")
263	        .map_err(|e| BindError::Io(e.to_string()))?;
264
265	    // Session ledger (REQ-TERM-6): record this session as active so the digest
266	    // can span a later `/clear`|`/compact` boundary. The boot row dedups against
267	    // a same-session revive; a missing/unstamped session is not ledgered (the
268	    // digest spans only when there is a session to enumerate). Best-effort — a
269	    // ledger problem must never fail the bind.
270	    if !session_id.is_empty() {
271	        let _ = spt_store::sessions::append(
272	            &perch_path,
273	            &spt_store::sessions::SessionEntry {
274	                ts: spt_store::timefmt::now_rfc3339(),
275	                session_id: session_id.to_string(),
276	                trigger: spt_store::sessions::SessionTrigger::Boot,
277	            },
278	        );
279	    }
280
281	    // Keep an existing token across a revive; mint one for a fresh perch.
282	    let token = auth::read_token(&perch_path).unwrap_or_else(auth::mint_token);
283	    auth::write_token(&perch_path, &token).map_err(|e| BindError::Io(e.to_string()))?;
284	    Ok(token)

    // W3 (REQ-HAZARD-BIND-CWD-UNSET): record the working directory so the picker
    // can group this endpoint under its PROJECT (data.rs → project_id_for_dir).
    // Previously NEVER set on bind — the refuted v0.12.1 "fresh agent shows under
    // its project" P1. spt-hosted: cmd_bind passes its own current_dir (the broker
    // spawned it in project_cwd); harness-hosted: bind_from_seed passes the seed's
    // captured cwd. A revive carries the prior cwd forward (a re-bind must not wipe
    // it), like home/adapter/controllable.
    // [impl->REQ-HAZARD-BIND-CWD-UNSET]
    rec.cwd = cwd
        .map(str::to_string)
        .or_else(|| prior.as_ref().and_then(|p| p.cwd.clone()));
    info::write_info(&perch_path, &rec).map_err(|e| BindError::Io(e.to_string()))?;
    std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), "")
        .map_err(|e| BindError::Io(e.to_string()))?;

    // Session ledger (REQ-TERM-6): record this session as active so the digest
    // can span a later `/clear`|`/compact` boundary. The boot row dedups against
    // a same-session revive; a missing/unstamped session is not ledgered (the
    // digest spans only when there is a session to enumerate). Best-effort — a
    // ledger problem must never fail the bind.
    if !session_id.is_empty() {
        let _ = spt_store::sessions::append(
            &perch_path,
            &spt_store::sessions::SessionEntry {
                ts: spt_store::timefmt::now_rfc3339(),
                session_id: session_id.to_string(),
                trigger: spt_store::sessions::SessionTrigger::Boot,
            },
        );
    }

    // Keep an existing token across a revive; mint one for a fresh perch.
    let token = auth::read_token(&perch_path).unwrap_or_else(auth::mint_token);
    auth::write_token(&perch_path, &token).map_err(|e| BindError::Io(e.to_string()))?;
    Ok(token)


315	        // Membership: this agent holds context in the project iff its
316	        // per-(agent,project) file is present in the branch tip tree.
317	        let rel = format!("{id}/{PROJECT_CONTEXT_FILE}");
318	        if matches!(store.read_at_tip(&branch, &rel), Ok(Some(_))) {
319	            out.push(project_id.to_string());
320	        }
321	    }
322	    out
323	}
324
325	/// Resume rows from the per-endpoint session ledger (newest→oldest), titled in
326	/// the model. The ledger row carries no project, so the project field is a
327	/// best-effort fill from the endpoint's most-recent project history; the title
328	/// falls back to the boundary trigger when that is empty.
329	fn resume_rows_for(perch_path: &Path, project_history: &[String]) -> Vec<ResumeRow> {
330	    let project = project_history.first().cloned().unwrap_or_default();
331	    let mut rows: Vec<ResumeRow> = sessions::last_k(perch_path, sessions::MAX_LEDGER)
332	        .into_iter()
333	        .map(|e| ResumeRow {
334	            session_id: e.session_id,
335	            ts: e.ts,
336	            trigger: e.trigger.token().to_string(),
337	            project: project.clone(),
338	        })
339	        .collect();
340	    rows.reverse(); // ledger is oldest→newest; show newest first
341	    rows
342	}
343
344	#[cfg(test)]
345	mod tests {
346	    use super::*;
347
348	    fn row(id: &str, is_local: bool, status: EpStatus) -> EndpointRow {
349	        EndpointRow {
350	            id: id.to_string(),
351	            group: if is_local { "local".into() } else { "sub:n".into() },
352	            node: if is_local { "LOCAL".into() } else { "n".into() },
353	            status,
354	            is_local,

        // Membership: this agent holds context in the project iff its
        // per-(agent,project) file is present in the branch tip tree.
        let rel = format!("{id}/{PROJECT_CONTEXT_FILE}");
        if matches!(store.read_at_tip(&branch, &rel), Ok(Some(_))) {
            out.push(project_id.to_string());
        }
    }
    out
}

/// Resume rows from the per-endpoint session ledger (newest→oldest), titled in
/// the model. The ledger row carries no project, so the project field is a
/// best-effort fill from the endpoint's most-recent project history; the title
/// falls back to the boundary trigger when that is empty.
fn resume_rows_for(perch_path: &Path, project_history: &[String]) -> Vec<ResumeRow> {
    let project = project_history.first().cloned().unwrap_or_default();
    let mut rows: Vec<ResumeRow> = sessions::last_k(perch_path, sessions::MAX_LEDGER)
        .into_iter()
        .map(|e| ResumeRow {
            session_id: e.session_id,
            ts: e.ts,
            trigger: e.trigger.token().to_string(),
            project: project.clone(),
        })
        .collect();
    rows.reverse(); // ledger is oldest→newest; show newest first
    rows
}

#[cfg(test)]
mod tests {
    use super::*;

    fn row(id: &str, is_local: bool, status: EpStatus) -> EndpointRow {
        EndpointRow {
            id: id.to_string(),
            group: if is_local { "local".into() } else { "sub:n".into() },
            node: if is_local { "LOCAL".into() } else { "n".into() },
            status,
            is_local,


Full picture now. All surfaces mapped. This is a multi-file build — let me track it and activate the REQ first.


ToolSearch

Question: select:TaskCreate,TaskUpdate


Question: select:TaskCreate,TaskUpdate


TaskCreate

Activate REQ-SESSION-RESUME-TEMPLATE [doc,impl,unit,int]

Set required_stages in traceable-reqs.toml; registry-first before evidence.


Task #1 created successfully: Activate REQ-SESSION-RESUME-TEMPLATE [doc,impl,unit,int]


Add [session.resume] manifest role

manifest.rs: resume: Option<SessionRole> on Session + roles() + is_empty(), mirror psyche_resume.


Task #2 created successfully: Add [session.resume] manifest role

Add [session.resume] manifest role


Add per-row cwd to session ledger

sessions.rs SessionEntry gets cwd: Option<String> additive serde-default; populate at boundary (reporting.rs) + bind (startup.rs) append sites from rec.cwd.


Task #3 created successfully: Add per-row cwd to session ledger

Add per-row cwd to session ledger


Resume-vs-self template selection in spawn path

harnesshost prepare_harness_spawn + launch_harness_brokered_in take is_resume; select session.resume when set+declared else self_ fallback. cmd_endpoint_run threads resume + cwd fallback chain (row->info.cwd->current_dir).


Task #4 created successfully: Resume-vs-self template selection in spawn path


Picker: thread row cwd + title reformat

ResumeRow.cwd, Outcome::Run.cwd, resume_outcome threads cwd, data.rs maps e.cwd, run() dispatch passes cwd to cmd_endpoint_run; ResumeRow::title() local-time reformat (D).


Task #5 created successfully: Picker: thread row cwd + title reformat

Picker: thread row cwd + title reformat


Public docs: [session.resume] (MANIFEST + harness-contract)

docs/MANIFEST.md + harness-contract page teach [session.resume] so perri builds blind. doc->REQ tag. docs-drift regen.


Task #6 created successfully: Public docs: [session.resume] (MANIFEST + harness-contract)


Tests: unit + int (delegate spt-test-engineer)

unit: manifest parse/roundtrip resume, resume-vs-self selection, cwd fallback chain, ledger back-compat. int: dummy-harness resume cmd != self, runs in recorded cwd. Then clippy/traceable/docs-drift gates.


Task #7 created successfully: Tests: unit + int (delegate spt-test-engineer)


Tasks tracked. **Task 1** — activate REQ registry-first.


Updated task #1 status


Edit

required_stages = []  # SEED — doyle design committed (V0.13.0-P2-SESSION-RESUME-DESIGN.md); todlando ACTIVATES [doc,impl,unit,int] registry-first when starting. PLANNED: doc = MANIFEST + harness-contract `[session.resume]` note (perri-buildable). impl = Session.resume role + roles()/is_empty() + cmd_endpoint_run resume-vs-self selection (+self fallback) + per-row ledger cwd (additive serde-default) + cwd fallback chain (row->info.cwd->current_dir) + picker threads row cwd. unit = manifest parse/roundtrip of `[session.resume]` + resume-vs-self selection + cwd fallback chain + ledger row back-compat (old row no cwd deserializes). int = dummy-harness whose resume command differs from self: --resume runs the RESUME command in the recorded cwd (marker assert), fresh runs self. clippy/traceable/docs-drift. doyle gates; then operator HITL (picker Resume reloads transcript in its project dir)."

required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED v0.13.0 P2 (todlando, resume-blank ship-blocker; doyle gates). doc = MANIFEST + harness-contract `[session.resume]` note (perri-buildable). impl = Session.resume role + roles()/is_empty() + cmd_endpoint_run resume-vs-self selection (+self fallback) + per-row ledger cwd (additive serde-default) + cwd fallback chain (row->info.cwd->current_dir) + picker threads row cwd. unit = manifest parse/roundtrip of `[session.resume]` + resume-vs-self selection + cwd fallback chain + ledger row back-compat (old row no cwd deserializes). int = dummy-harness whose resume command differs from self: --resume runs the RESUME command in the recorded cwd (marker assert), fresh runs self. clippy/traceable/docs-drift. doyle gates; then operator HITL (picker Resume reloads transcript in its project dir)."


The file C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml has been updated successfully. (file state is current in your context — no need to Read it back)

title = "Resuming an endpoint session that HAS conversation history brings up a BLANK session. ROOT (doyle, code-grounded + CONTEXT — case-3 spt-core MISSING feature, NOT a perri docs-miss): CONTEXT L127-129 already defines the resume-session seam ('continue-existing: resume an existing harness session under the adapter — its NATIVE resume'), and the manifest already has the resume-variant pattern (Session has BOTH psyche_init AND psyche_resume, manifest.rs:217-219) — but the agent's own session has ONLY self_ (`[session.self]`, no resume sibling). cmd_endpoint_run (cli.rs:1304) re-passes the session_id through `[session.self]` on resume (resume.unwrap_or_else(mint_session_id)), so the adapter's FRESH command (e.g. `claude --session-id ..`) runs again instead of the harness NATIVE resume (`claude -r ..`) -> CC starts a fresh transcript -> blank. spt-core forwards session_id + cwd faithfully; it just has no way to express the native-resume invocation. SECOND GAP: CC resolves a transcript by session_id + cwd, but the session ledger records only {ts, session_id, trigger} (no cwd), so picker Resume-from-history (cross-project rows) can't restore the right cwd. FIX (doyle design, V0.13.0-P2-SESSION-RESUME-DESIGN.md, mirrors psyche_init->psyche_resume exactly): (A) add a `[session.resume]` role (resume: Option<SessionRole> on Session + roles()/is_empty()); cmd_endpoint_run selects it when --resume is set AND it's declared (fill {id}/{session_id}=resumed id/{session_name} + the resume cwd), else FALL BACK to `[session.self]` (full back-compat). (B) record cwd PER ledger row (operator ruling): {ts, session_id, trigger, cwd} additive serde-default; resume cwd = resumed row cwd -> else perch info.cwd -> else current_dir (back-compat for old rows + single-project endpoints); picker threads the selected row's cwd through Outcome::Run -> cmd_endpoint_run. (C) public docs (MANIFEST + harness-contract) teach `[session.resume]` so perri builds the adapter side BLIND. Adapter follow-on (perri, AFTER spt-core ships+docs): declare `[session.resume] command = claude -r {session_id} --remote-control {id} --dangerously-skip-permissions` from the resume cwd. Completes REQ-READY-AGENT-RESUME / REQ-RUN-PICKER resume-from-history. (v0.13.0)"

-required_stages = []  # SEED — doyle design committed (V0.13.0-P2-SESSION-RESUME-DESIGN.md); todlando ACTIVATES [doc,impl,unit,int] registry-first when starting. PLANNED: doc = MANIFEST + harness-contract `[session.resume]` note (perri-buildable). impl = Session.resume role + roles()/is_empty() + cmd_endpoint_run resume-vs-self selection (+self fallback) + per-row ledger cwd (additive serde-default) + cwd fallback chain (row->info.cwd->current_dir) + picker threads row cwd. unit = manifest parse/roundtrip of `[session.resume]` + resume-vs-self selection + cwd fallback chain + ledger row back-compat (old row no cwd deserializes). int = dummy-harness whose resume command differs from self: --resume runs the RESUME command in the recorded cwd (marker assert), fresh runs self. clippy/traceable/docs-drift. doyle gates; then operator HITL (picker Resume reloads transcript in its project dir)."

+required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED v0.13.0 P2 (todlando, resume-blank ship-blocker; doyle gates). doc = MANIFEST + harness-contract `[session.resume]` note (perri-buildable). impl = Session.resume role + roles()/is_empty() + cmd_endpoint_run resume-vs-self selection (+self fallback) + per-row ledger cwd (additive serde-default) + cwd fallback chain (row->info.cwd->current_dir) + picker threads row cwd. unit = manifest parse/roundtrip of `[session.resume]` + resume-vs-self selection + cwd fallback chain + ledger row back-compat (old row no cwd deserializes). int = dummy-harness whose resume command differs from self: --resume runs the RESUME command in the recorded cwd (marker assert), fresh runs self. clippy/traceable/docs-drift. doyle gates; then operator HITL (picker Resume reloads transcript in its project dir)."

# ── P1 (operator HITL 2026-06-19, post-deploy dogfood; doyle /diagnose + design): rc paste broken (ctrl+V + right-click) ──


Task 1 done. **Task 2** — `[session.resume]` manifest role.


Updated task #2 status


    #[serde(rename = "self", default, skip_serializing_if = "Option::is_none")]
    pub self_: Option<SessionRole>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub psyche_init: Option<SessionRole>,

    #[serde(rename = "self", default, skip_serializing_if = "Option::is_none")]
    pub self_: Option<SessionRole>,
    /// `[session.resume]` — the agent's OWN-session NATIVE resume (the `self_`
    /// sibling, mirroring `psyche_init`→`psyche_resume`). Selected over `self_`
    /// only when a bringup carries `--resume <session>` AND this role is declared;
    /// absent ⇒ fall back to `[session.self]` (full back-compat). Keys spt-core
    /// fills are the SAME catalog as `self`: `{id}`, `{session_id}` (the resumed
    /// id), `{session_name}`, `{adapter_name}` (REQ-SESSION-RESUME-TEMPLATE).
    // [impl->REQ-SESSION-RESUME-TEMPLATE]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub resume: Option<SessionRole>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub psyche_init: Option<SessionRole>,


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\manifest.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    #[serde(rename = "self", default, skip_serializing_if = "Option::is_none")]
    pub self_: Option<SessionRole>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub psyche_init: Option<SessionRole>,

    #[serde(rename = "self", default, skip_serializing_if = "Option::is_none")]
    pub self_: Option<SessionRole>,
    /// `[session.resume]` — the agent's OWN-session NATIVE resume (the `self_`
    /// sibling, mirroring `psyche_init`→`psyche_resume`). Selected over `self_`
    /// only when a bringup carries `--resume <session>` AND this role is declared;
    /// absent ⇒ fall back to `[session.self]` (full back-compat). Keys spt-core
    /// fills are the SAME catalog as `self`: `{id}`, `{session_id}` (the resumed
    /// id), `{session_name}`, `{adapter_name}` (REQ-SESSION-RESUME-TEMPLATE).
    // [impl->REQ-SESSION-RESUME-TEMPLATE]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub resume: Option<SessionRole>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub psyche_init: Option<SessionRole>,

+    /// `[session.resume]` — the agent's OWN-session NATIVE resume (the `self_`

+    /// sibling, mirroring `psyche_init`→`psyche_resume`). Selected over `self_`

+    /// only when a bringup carries `--resume <session>` AND this role is declared;

+    /// absent ⇒ fall back to `[session.self]` (full back-compat). Keys spt-core

+    /// fills are the SAME catalog as `self`: `{id}`, `{session_id}` (the resumed

+    /// id), `{session_name}`, `{adapter_name}` (REQ-SESSION-RESUME-TEMPLATE).

+    // [impl->REQ-SESSION-RESUME-TEMPLATE]

+    #[serde(default, skip_serializing_if = "Option::is_none")]


            && self.self_.is_none()
            && self.psyche_init.is_none()

            && self.self_.is_none()
            && self.resume.is_none()
            && self.psyche_init.is_none()


            && self.self_.is_none()
            && self.psyche_init.is_none()

            && self.self_.is_none()
            && self.resume.is_none()
            && self.psyche_init.is_none()


            ("self", &self.self_),
            ("psyche_init", &self.psyche_init),

            ("self", &self.self_),
            ("resume", &self.resume),
            ("psyche_init", &self.psyche_init),


            ("self", &self.self_),
            ("psyche_init", &self.psyche_init),

            ("self", &self.self_),
            ("resume", &self.resume),
            ("psyche_init", &self.psyche_init),


Task 2 done. **Task 3** — ledger cwd field + populate at append sites.


Updated task #3 status


    /// The harness session id that became active.
    pub session_id: String,
    /// What caused the rotation.
    pub trigger: SessionTrigger,
}

    /// The harness session id that became active.
    pub session_id: String,
    /// What caused the rotation.
    pub trigger: SessionTrigger,
    /// The project working directory the session was active in (the W3
    /// `info.cwd` set at bind, or the spawn cwd). **Additive serde-default**: a
    /// pre-migration row missing `cwd` deserializes as `None`. A harness resolves
    /// a transcript by `session_id` + `cwd`, so picker Resume-from-history
    /// (cross-project rows) restores the right dir from here.
    // [impl->REQ-SESSION-RESUME-TEMPLATE]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cwd: Option<String>,
}


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\sessions.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    /// The harness session id that became active.
    pub session_id: String,
    /// What caused the rotation.
    pub trigger: SessionTrigger,
    /// The project working directory the session was active in (the W3
    /// `info.cwd` set at bind, or the spawn cwd). **Additive serde-default**: a
    /// pre-migration row missing `cwd` deserializes as `None`. A harness resolves
    /// a transcript by `session_id` + `cwd`, so picker Resume-from-history
    /// (cross-project rows) restores the right dir from here.
    // [impl->REQ-SESSION-RESUME-TEMPLATE]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cwd: Option<String>,
}

//! Per-endpoint **session ledger** — `<perch>/sessions.log` (ADR-0019 /
//! REQ-TERM-6), heir to the sister project's `psyches/tracked/agents/<id>/
//! sessions.log`.
//!
//! A live agent that `/clear`s or `/compact`s keeps its perch identity but
//! rotates its harness `session_id` (a new transcript file). The single
//! `{session_id}` a `[history]`/`[digest]` source resolves therefore *follows*
//! the thread but cannot *span* it. This ledger records each rotation so the
//! digest can enumerate the **last K sessions** and bridge a boundary
//! (ADR-0019 §thread-spanning).
//!
//! One JSONL row per rotation: `{ts, session_id, trigger}`. The ledger is
//! **bounded** ([`MAX_LEDGER`]) — the sister project's seal-on-boot failure grew
//! its equivalent unboundedly (DIGEST-MILESTONE-PLAN risk #2); appending rotates
//! the oldest rows out. A re-bind of the *same* session is not a new boundary and
//! is deduped.
// [impl->REQ-TERM-6]

use std::io;
use std::path::Path;

use serde::{Deserialize, Serialize};

use crate::atomic::atomic_write_string;

/// The ledger filename under a perch.
pub const LEDGER_FILE: &str = "sessions.log";

/// On-disk row cap. The ledger is a rolling tail — older rows rotate out on
/// append so the file can never grow without bound (risk #2).
pub const MAX_LEDGER: usize = 64;

/// Why a session became active.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SessionTrigger {
    /// First bind of the perch (`api listen` / revive).
    Boot,
    /// A `/clear` boundary (`api boundary clear`).
    Clear,
    /// A `/compact` boundary (`api boundary compact`).
    Compact,
}

impl SessionTrigger {
    /// The lowercase token (matches the serde wire form) — used as the boundary
    /// marker `kind` in the spanned digest.
    pub fn token(self) -> &'static str {
        match self {
            SessionTrigger::Boot => "boot",
            SessionTrigger::Clear => "clear",
            SessionTrigger::Compact => "compact",
        }
    }
}

/// One ledger row: a `session_id` became active at `ts` via `trigger`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionEntry {
    /// RFC3339-UTC instant (see [`crate::timefmt`]).
    pub ts: String,
    /// The harness session id that became active.
    pub session_id: String,
    /// What caused the rotation.
    pub trigger: SessionTrigger,
}

/// Every ledger row in order (oldest→newest). Tolerant: a blank or unparseable
/// line is skipped, the rest still load (the degrade-don't-crash store stance).
pub fn read_all(perch: &Path) -> Vec<SessionEntry> {
    let Ok(s) = std::fs::read_to_string(perch.join(LEDGER_FILE)) else {
        return Vec::new();
    };
    s.lines()
        .filter(|l| !l.trim().is_empty())
        .filter_map(|l| serde_json::from_str(l).ok())
        .collect()
}

/// The last `k` rows (oldest→newest within the tail). `k == 0` ⇒ empty.
pub fn last_k(perch: &Path, k: usize) -> Vec<SessionEntry> {
    if k == 0 {
        return Vec::new();
    }
    let all = read_all(perch);
    let start = all.len().saturating_sub(k);
    all[start..].to_vec()
}

/// Append `entry`, deduping an immediate repeat of the same `session_id` (a
/// re-bind of the live session is not a new boundary) and bounding the ledger to
/// [`MAX_LEDGER`] (oldest rows rotate out). The whole bounded ledger is rewritten
/// atomically (it is tiny — ≤ `MAX_LEDGER` short rows — so a full rewrite avoids
/// partial-line corruption). Returns `true` if a row was written, `false` on a
/// same-session dedup.
// [impl->REQ-TERM-6]
pub fn append(perch: &Path, entry: &SessionEntry) -> io::Result<bool> {
    let mut entries = read_all(perch);
    if entries.last().map(|e| e.session_id.as_str()) == Some(entry.session_id.as_str()) {
        return Ok(false); // same session re-bound — not a new boundary
    }
    entries.push(entry.clone());
    let start = entries.len().saturating_sub(MAX_LEDGER);
    let body: String = entries[start..]
        .iter()
        .map(|e| serde_json::to_string(e).unwrap_or_default())
        .map(|l| l + "\n")
        .collect();
    std::fs::create_dir_all(perch)?;
    atomic_write_string(&perch.join(LEDGER_FILE), &body)?;
    Ok(true)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn entry(sid: &str, trigger: SessionTrigger) -> SessionEntry {
        SessionEntry {
            ts: crate::timefmt::rfc3339_utc(1_700_000_000),
            session_id: sid.to_string(),
            trigger,
        }
    }

    // [unit->REQ-TERM-6] rows append in order and read back oldest→newest.
    #[test]
    fn appends_and_reads_in_order() {
        let dir = tempfile::tempdir().unwrap();
        let perch = dir.path().join("perch");
        assert!(append(&perch, &entry("s1", SessionTrigger::Boot)).unwrap());
        assert!(append(&perch, &entry("s2", SessionTrigger::Clear)).unwrap());
        let all = read_all(&perch);
        assert_eq!(all.len(), 2);
        assert_eq!(all[0].session_id, "s1");
        assert_eq!(all[0].trigger, SessionTrigger::Boot);
        assert_eq!(all[1].session_id, "s2");
        assert_eq!(all[1].trigger, SessionTrigger::Clear);
    }

    // [unit->REQ-TERM-6] a re-bind of the same session is deduped (not a new
    // boundary); a different session appends.
    #[test]
    fn same_session_rebind_is_deduped() {
        let dir = tempfile::tempdir().unwrap();
        let perch = dir.path().join("perch");
        assert!(append(&perch, &entry("s1", SessionTrigger::Boot)).unwrap());
        assert!(
            !append(&perch, &entry("s1", SessionTrigger::Boot)).unwrap(),
            "same session re-bind → no new row"
        );
        assert_eq!(read_all(&perch).len(), 1);
        assert!(append(&perch, &entry("s2", SessionTrigger::Clear)).unwrap());
        assert_eq!(read_all(&perch).len(), 2);
    }

    // [unit->REQ-TERM-6] last_k returns the newest K rows; the ledger is bounded so
    // it never grows past MAX_LEDGER.
    #[test]
    fn last_k_and_bounding() {
        let dir = tempfile::tempdir().unwrap();
        let perch = dir.path().join("perch");
        for i in 0..(MAX_LEDGER + 10) {
            append(&perch, &entry(&format!("s{i}"), SessionTrigger::Clear)).unwrap();
        }
        assert_eq!(read_all(&perch).len(), MAX_LEDGER, "ledger is bounded");
        let last3 = last_k(&perch, 3);
        assert_eq!(last3.len(), 3);
        let n = MAX_LEDGER + 10;
        assert_eq!(last3[2].session_id, format!("s{}", n - 1), "newest last");
        assert_eq!(last3[0].session_id, format!("s{}", n - 3));
        assert!(last_k(&perch, 0).is_empty());
    }

    // [unit->REQ-TERM-6] a blank/garbage line is skipped, the valid rows still load.
    #[test]
    fn tolerant_of_garbage_lines() {
        let dir = tempfile::tempdir().unwrap();
        let perch = dir.path().join("perch");
        std::fs::create_dir_all(&perch).unwrap();
        std::fs::write(
            perch.join(LEDGER_FILE),
            "not json\n\n{\"ts\":\"t\",\"session_id\":\"good\",\"trigger\":\"boot\"}\n",
        )
        .unwrap();
        let all = read_all(&perch);
        assert_eq!(all.len(), 1);
        assert_eq!(all[0].session_id, "good");
    }
}

+    /// The project working directory the session was active in (the W3

+    /// `info.cwd` set at bind, or the spawn cwd). **Additive serde-default**: a

+    /// pre-migration row missing `cwd` deserializes as `None`. A harness resolves

+    /// a transcript by `session_id` + `cwd`, so picker Resume-from-history

+    /// (cross-project rows) restores the right dir from here.


        SessionEntry {
            ts: crate::timefmt::rfc3339_utc(1_700_000_000),
            session_id: sid.to_string(),
            trigger,
        }

        SessionEntry {
            ts: crate::timefmt::rfc3339_utc(1_700_000_000),
            session_id: sid.to_string(),
            trigger,
            cwd: None,
        }


        SessionEntry {
            ts: crate::timefmt::rfc3339_utc(1_700_000_000),
            session_id: sid.to_string(),
            trigger,
            cwd: None,
        }

//! Per-endpoint **session ledger** — `<perch>/sessions.log` (ADR-0019 /
//! REQ-TERM-6), heir to the sister project's `psyches/tracked/agents/<id>/
//! sessions.log`.
//!
//! A live agent that `/clear`s or `/compact`s keeps its perch identity but
//! rotates its harness `session_id` (a new transcript file). The single
//! `{session_id}` a `[history]`/`[digest]` source resolves therefore *follows*
//! the thread but cannot *span* it. This ledger records each rotation so the
//! digest can enumerate the **last K sessions** and bridge a boundary
//! (ADR-0019 §thread-spanning).
//!
//! One JSONL row per rotation: `{ts, session_id, trigger}`. The ledger is
//! **bounded** ([`MAX_LEDGER`]) — the sister project's seal-on-boot failure grew
//! its equivalent unboundedly (DIGEST-MILESTONE-PLAN risk #2); appending rotates
//! the oldest rows out. A re-bind of the *same* session is not a new boundary and
//! is deduped.
// [impl->REQ-TERM-6]

use std::io;
use std::path::Path;

use serde::{Deserialize, Serialize};

use crate::atomic::atomic_write_string;

/// The ledger filename under a perch.
pub const LEDGER_FILE: &str = "sessions.log";

/// On-disk row cap. The ledger is a rolling tail — older rows rotate out on
/// append so the file can never grow without bound (risk #2).
pub const MAX_LEDGER: usize = 64;

/// Why a session became active.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SessionTrigger {
    /// First bind of the perch (`api listen` / revive).
    Boot,
    /// A `/clear` boundary (`api boundary clear`).
    Clear,
    /// A `/compact` boundary (`api boundary compact`).
    Compact,
}

impl SessionTrigger {
    /// The lowercase token (matches the serde wire form) — used as the boundary
    /// marker `kind` in the spanned digest.
    pub fn token(self) -> &'static str {
        match self {
            SessionTrigger::Boot => "boot",
            SessionTrigger::Clear => "clear",
            SessionTrigger::Compact => "compact",
        }
    }
}

/// One ledger row: a `session_id` became active at `ts` via `trigger`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionEntry {
    /// RFC3339-UTC instant (see [`crate::timefmt`]).
    pub ts: String,
    /// The harness session id that became active.
    pub session_id: String,
    /// What caused the rotation.
    pub trigger: SessionTrigger,
    /// The project working directory the session was active in (the W3
    /// `info.cwd` set at bind, or the spawn cwd). **Additive serde-default**: a
    /// pre-migration row missing `cwd` deserializes as `None`. A harness resolves
    /// a transcript by `session_id` + `cwd`, so picker Resume-from-history
    /// (cross-project rows) restores the right dir from here.
    // [impl->REQ-SESSION-RESUME-TEMPLATE]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cwd: Option<String>,
}

/// Every ledger row in order (oldest→newest). Tolerant: a blank or unparseable
/// line is skipped, the rest still load (the degrade-don't-crash store stance).
pub fn read_all(perch: &Path) -> Vec<SessionEntry> {
    let Ok(s) = std::fs::read_to_string(perch.join(LEDGER_FILE)) else {
        return Vec::new();
    };
    s.lines()
        .filter(|l| !l.trim().is_empty())
        .filter_map(|l| serde_json::from_str(l).ok())
        .collect()
}

/// The last `k` rows (oldest→newest within the tail). `k == 0` ⇒ empty.
pub fn last_k(perch: &Path, k: usize) -> Vec<SessionEntry> {
    if k == 0 {
        return Vec::new();
    }
    let all = read_all(perch);
    let start = all.len().saturating_sub(k);
    all[start..].to_vec()
}

/// Append `entry`, deduping an immediate repeat of the same `session_id` (a
/// re-bind of the live session is not a new boundary) and bounding the ledger to
/// [`MAX_LEDGER`] (oldest rows rotate out). The whole bounded ledger is rewritten
/// atomically (it is tiny — ≤ `MAX_LEDGER` short rows — so a full rewrite avoids
/// partial-line corruption). Returns `true` if a row was written, `false` on a
/// same-session dedup.
// [impl->REQ-TERM-6]
pub fn append(perch: &Path, entry: &SessionEntry) -> io::Result<bool> {
    let mut entries = read_all(perch);
    if entries.last().map(|e| e.session_id.as_str()) == Some(entry.session_id.as_str()) {
        return Ok(false); // same session re-bound — not a new boundary
    }
    entries.push(entry.clone());
    let start = entries.len().saturating_sub(MAX_LEDGER);
    let body: String = entries[start..]
        .iter()
        .map(|e| serde_json::to_string(e).unwrap_or_default())
        .map(|l| l + "\n")
        .collect();
    std::fs::create_dir_all(perch)?;
    atomic_write_string(&perch.join(LEDGER_FILE), &body)?;
    Ok(true)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn entry(sid: &str, trigger: SessionTrigger) -> SessionEntry {
        SessionEntry {
            ts: crate::timefmt::rfc3339_utc(1_700_000_000),
            session_id: sid.to_string(),
            trigger,
        }
    }

    // [unit->REQ-TERM-6] rows append in order and read back oldest→newest.
    #[test]
    fn appends_and_reads_in_order() {
        let dir = tempfile::tempdir().unwrap();
        let perch = dir.path().join("perch");
        assert!(append(&perch, &entry("s1", SessionTrigger::Boot)).unwrap());
        assert!(append(&perch, &entry("s2", SessionTrigger::Clear)).unwrap());
        let all = read_all(&perch);
        assert_eq!(all.len(), 2);
        assert_eq!(all[0].session_id, "s1");
        assert_eq!(all[0].trigger, SessionTrigger::Boot);
        assert_eq!(all[1].session_id, "s2");
        assert_eq!(all[1].trigger, SessionTrigger::Clear);
    }

    // [unit->REQ-TERM-6] a re-bind of the same session is deduped (not a new
    // boundary); a different session appends.
    #[test]
    fn same_session_rebind_is_deduped() {
        let dir = tempfile::tempdir().unwrap();
        let perch = dir.path().join("perch");
        assert!(append(&perch, &entry("s1", SessionTrigger::Boot)).unwrap());
        assert!(
            !append(&perch, &entry("s1", SessionTrigger::Boot)).unwrap(),
            "same session re-bind → no new row"
        );
        assert_eq!(read_all(&perch).len(), 1);
        assert!(append(&perch, &entry("s2", SessionTrigger::Clear)).unwrap());
        assert_eq!(read_all(&perch).len(), 2);
    }

    // [unit->REQ-TERM-6] last_k returns the newest K rows; the ledger is bounded so
    // it never grows past MAX_LEDGER.
    #[test]
    fn last_k_and_bounding() {
        let dir = tempfile::tempdir().unwrap();
        let perch = dir.path().join("perch");
        for i in 0..(MAX_LEDGER + 10) {
            append(&perch, &entry(&format!("s{i}"), SessionTrigger::Clear)).unwrap();
        }
        assert_eq!(read_all(&perch).len(), MAX_LEDGER, "ledger is bounded");
        let last3 = last_k(&perch, 3);
        assert_eq!(last3.len(), 3);
        let n = MAX_LEDGER + 10;
        assert_eq!(last3[2].session_id, format!("s{}", n - 1), "newest last");
        assert_eq!(last3[0].session_id, format!("s{}", n - 3));
        assert!(last_k(&perch, 0).is_empty());
    }

    // [unit->REQ-TERM-6] a blank/garbage line is skipped, the valid rows still load.
    #[test]
    fn tolerant_of_garbage_lines() {
        let dir = tempfile::tempdir().unwrap();
        let perch = dir.path().join("perch");
        std::fs::create_dir_all(&perch).unwrap();
        std::fs::write(
            perch.join(LEDGER_FILE),
            "not json\n\n{\"ts\":\"t\",\"session_id\":\"good\",\"trigger\":\"boot\"}\n",
        )
        .unwrap();
        let all = read_all(&perch);
        assert_eq!(all.len(), 1);
        assert_eq!(all[0].session_id, "good");
    }
}


Now populate cwd at the real append sites. Boundary (reporting.rs) + bind (startup.rs):


        &spt_store::sessions::SessionEntry {
            ts: spt_store::timefmt::now_rfc3339(),
            session_id: to_session.to_string(),
            trigger,
        },

        &spt_store::sessions::SessionEntry {
            ts: spt_store::timefmt::now_rfc3339(),
            session_id: to_session.to_string(),
            trigger,
            // Record the session's project cwd so picker Resume restores the
            // right dir (REQ-SESSION-RESUME-TEMPLATE). `rec.cwd` is preserved
            // across the boundary rotation above (W3 bind-cwd).
            cwd: rec.cwd.clone(),
        },


The file C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\reporting.rs has been updated successfully. (file state is current in your context — no need to Read it back)

        &spt_store::sessions::SessionEntry {
            ts: spt_store::timefmt::now_rfc3339(),
            session_id: to_session.to_string(),
            trigger,
        },

        &spt_store::sessions::SessionEntry {
            ts: spt_store::timefmt::now_rfc3339(),
            session_id: to_session.to_string(),
            trigger,
            // Record the session's project cwd so picker Resume restores the
            // right dir (REQ-SESSION-RESUME-TEMPLATE). `rec.cwd` is preserved
            // across the boundary rotation above (W3 bind-cwd).
            cwd: rec.cwd.clone(),
        },

+            // Record the session's project cwd so picker Resume restores the

+            // right dir (REQ-SESSION-RESUME-TEMPLATE). `rec.cwd` is preserved

+            // across the boundary rotation above (W3 bind-cwd).


            &spt_store::sessions::SessionEntry {
                ts: spt_store::timefmt::now_rfc3339(),
                session_id: session_id.to_string(),
                trigger: spt_store::sessions::SessionTrigger::Boot,
            },

            &spt_store::sessions::SessionEntry {
                ts: spt_store::timefmt::now_rfc3339(),
                session_id: session_id.to_string(),
                trigger: spt_store::sessions::SessionTrigger::Boot,
                // Record the bind cwd (W3 `rec.cwd`, just written above) so a
                // later picker Resume of this boot session restores its project
                // dir (REQ-SESSION-RESUME-TEMPLATE).
                cwd: rec.cwd.clone(),
            },


The file C:\Users\decid\Documents\projects\spt-core\crates\spt\src\api\startup.rs has been updated successfully. (file state is current in your context — no need to Read it back)

            &spt_store::sessions::SessionEntry {
                ts: spt_store::timefmt::now_rfc3339(),
                session_id: session_id.to_string(),
                trigger: spt_store::sessions::SessionTrigger::Boot,
            },

            &spt_store::sessions::SessionEntry {
                ts: spt_store::timefmt::now_rfc3339(),
                session_id: session_id.to_string(),
                trigger: spt_store::sessions::SessionTrigger::Boot,
                // Record the bind cwd (W3 `rec.cwd`, just written above) so a
                // later picker Resume of this boot session restores its project
                // dir (REQ-SESSION-RESUME-TEMPLATE).
                cwd: rec.cwd.clone(),
            },

+                // Record the bind cwd (W3 `rec.cwd`, just written above) so a

+                // later picker Resume of this boot session restores its project

+                // dir (REQ-SESSION-RESUME-TEMPLATE).


Remaining construction sites need the field. Reading ready.rs + digest.rs + the test.


100	            .map_err(|e| format!("Failed to write ready marker: {}", e))?;
101
102	        // Session ledger (REQ-READY-AGENT-RESUME): a harness-hosted ready bind
103	        // writes info.json directly and — unlike the live `establish_perch` path
104	        // (startup.rs:250) — never ledgered, so an offline ready perch carried
105	        // ZERO session rows and the picker's offline Resume-from-history (which
106	        // gates on ledger rows) never offered it. Append a Boot row here, mirroring
107	        // the live path: the boot row dedups against a same-session re-bind, an
108	        // empty session is not ledgered, and a ledger problem must never fail the
109	        // bind (best-effort).
110	        // [impl->REQ-READY-AGENT-RESUME]
111	        if !sid.is_empty() {
112	            let _ = spt_store::sessions::append(
113	                &perch_path,
114	                &spt_store::sessions::SessionEntry {
115	                    ts: spt_store::timefmt::now_rfc3339(),
116	                    session_id: sid.clone(),
117	                    trigger: spt_store::sessions::SessionTrigger::Boot,
118	                },
119	            );
120	        }
121
122	        // Bind + register the listener address. Blocking with a short poll so
123	        // recv() stays responsive.
124	        let listener = PollListener::bind_blocking(id, RECV_POLL)?;

            .map_err(|e| format!("Failed to write ready marker: {}", e))?;

        // Session ledger (REQ-READY-AGENT-RESUME): a harness-hosted ready bind
        // writes info.json directly and — unlike the live `establish_perch` path
        // (startup.rs:250) — never ledgered, so an offline ready perch carried
        // ZERO session rows and the picker's offline Resume-from-history (which
        // gates on ledger rows) never offered it. Append a Boot row here, mirroring
        // the live path: the boot row dedups against a same-session re-bind, an
        // empty session is not ledgered, and a ledger problem must never fail the
        // bind (best-effort).
        // [impl->REQ-READY-AGENT-RESUME]
        if !sid.is_empty() {
            let _ = spt_store::sessions::append(
                &perch_path,
                &spt_store::sessions::SessionEntry {
                    ts: spt_store::timefmt::now_rfc3339(),
                    session_id: sid.clone(),
                    trigger: spt_store::sessions::SessionTrigger::Boot,
                },
            );
        }

        // Bind + register the listener address. Blocking with a short poll so
        // recv() stays responsive.
        let listener = PollListener::bind_blocking(id, RECV_POLL)?;


520	            std::fs::write(asrc.join("manifest.toml"), manifest).unwrap();
521	            spt_runtime::registry::register(&perch::adapters_dir(), &asrc, 1).unwrap();
522
523	            // The endpoint records the adapter; the ledger holds both sessions.
524	            let perch = perch::resolve_perch_path("span", ParentHint::Infer);
525	            std::fs::create_dir_all(&perch).unwrap();
526	            let mut rec =
527	                spt_store::info::InfoJson::new("span", "0", std::process::id(), "B", "live_agent");
528	            rec.adapter = Some("ccspan".to_string());
529	            spt_store::info::write_info(&perch, &rec).unwrap();
530	            spt_store::sessions::append(
531	                &perch,
532	                &spt_store::sessions::SessionEntry {
533	                    ts: spt_store::timefmt::rfc3339_utc(1_700_000_000),
534	                    session_id: "A".into(),
535	                    trigger: spt_store::sessions::SessionTrigger::Boot,
536	                },
537	            )
538	            .unwrap();
539	            spt_store::sessions::append(
540	                &perch,
541	                &spt_store::sessions::SessionEntry {
542	                    ts: spt_store::timefmt::rfc3339_utc(1_700_000_100),
543	                    session_id: "B".into(),
544	                    trigger: spt_store::sessions::SessionTrigger::Clear,
545	                },
546	            )
547	            .unwrap();
548
549	            let d = project_endpoint_digest("span", &DigestOverride::default());

            std::fs::write(asrc.join("manifest.toml"), manifest).unwrap();
            spt_runtime::registry::register(&perch::adapters_dir(), &asrc, 1).unwrap();

            // The endpoint records the adapter; the ledger holds both sessions.
            let perch = perch::resolve_perch_path("span", ParentHint::Infer);
            std::fs::create_dir_all(&perch).unwrap();
            let mut rec =
                spt_store::info::InfoJson::new("span", "0", std::process::id(), "B", "live_agent");
            rec.adapter = Some("ccspan".to_string());
            spt_store::info::write_info(&perch, &rec).unwrap();
            spt_store::sessions::append(
                &perch,
                &spt_store::sessions::SessionEntry {
                    ts: spt_store::timefmt::rfc3339_utc(1_700_000_000),
                    session_id: "A".into(),
                    trigger: spt_store::sessions::SessionTrigger::Boot,
                },
            )
            .unwrap();
            spt_store::sessions::append(
                &perch,
                &spt_store::sessions::SessionEntry {
                    ts: spt_store::timefmt::rfc3339_utc(1_700_000_100),
                    session_id: "B".into(),
                    trigger: spt_store::sessions::SessionTrigger::Clear,
                },
            )
            .unwrap();

            let d = project_endpoint_digest("span", &DigestOverride::default());


23:use spt_store::info::{self, InfoJson};
72:        let prior = info::read_info(&perch_path);
74:        let mut rec = InfoJson::new(
95:        info::write_info(&perch_path, &rec)
240:        assert!(info::read_info(&perch_path).is_some());
254:        let rec = InfoJson::new(&id, "now", std::process::id(), "sess", "ready_agent");
255:        info::write_info(&perch_path, &rec).unwrap();
315:        assert!(info::read_info(&perch_path).is_some());


72	        let prior = info::read_info(&perch_path);
73	        let sid = session_id();
74	        let mut rec = InfoJson::new(
75	            id,
76	            &now_stamp(),
77	            std::process::id(),
78	            &sid,
79	            "ready_agent",
80	        );
81	        let subnets = spt_store::subnet::SubnetStore::load();
82	        let mut vis = spt_store::visibility::VisibilityStore::load();
83	        let dirty = spt_store::home::stamp_creation_fields(
84	            &mut rec,
85	            prior.as_ref(),
86	            &subnets,
87	            &mut vis,
88	            subnet,
89	            None,
90	        )
91	        .map_err(|e| format!("HOME: {e}"))?;
92	        if dirty {
93	            let _ = vis.save();
94	        }
95	        info::write_info(&perch_path, &rec)
96	            .map_err(|e| format!("Failed to write info.json: {}", e))?;

        let prior = info::read_info(&perch_path);
        let sid = session_id();
        let mut rec = InfoJson::new(
            id,
            &now_stamp(),
            std::process::id(),
            &sid,
            "ready_agent",
        );
        let subnets = spt_store::subnet::SubnetStore::load();
        let mut vis = spt_store::visibility::VisibilityStore::load();
        let dirty = spt_store::home::stamp_creation_fields(
            &mut rec,
            prior.as_ref(),
            &subnets,
            &mut vis,
            subnet,
            None,
        )
        .map_err(|e| format!("HOME: {e}"))?;
        if dirty {
            let _ = vis.save();
        }
        info::write_info(&perch_path, &rec)
            .map_err(|e| format!("Failed to write info.json: {}", e))?;


Set required_stages in traceable-reqs.toml; registry-first before evidence.

Add [session.resume] manifest role

manifest.rs: resume: Option<SessionRole> on Session + roles() + is_empty(), mirror psyche_resume.

Add per-row cwd to session ledger

sessions.rs SessionEntry gets cwd: Option<String> additive serde-default; populate at boundary (reporting.rs) + bind (startup.rs) append sites from rec.cwd.

harnesshost prepare_harness_spawn + launch_harness_brokered_in take is_resume; select session.resume when set+declared else self_ fallback. cmd_endpoint_run threads resume + cwd fallback chain (row->info.cwd->current_dir).

Picker: thread row cwd + title reformat

ResumeRow.cwd, Outcome::Run.cwd, resume_outcome threads cwd, data.rs maps e.cwd, run() dispatch passes cwd to cmd_endpoint_run; ResumeRow::title() local-time reformat (D).

docs/MANIFEST.md + harness-contract page teach [session.resume] so perri builds blind. doc->REQ tag. docs-drift regen.

unit: manifest parse/roundtrip resume, resume-vs-self selection, cwd fallback chain, ledger back-compat. int: dummy-harness resume cmd != self, runs in recorded cwd. Then clippy/traceable/docs-drift gates.


ready.rs append — mirror with `rec.cwd`. Tests get `cwd: None`.


                &spt_store::sessions::SessionEntry {
                    ts: spt_store::timefmt::now_rfc3339(),
                    session_id: sid.clone(),
                    trigger: spt_store::sessions::SessionTrigger::Boot,
                },

                &spt_store::sessions::SessionEntry {
                    ts: spt_store::timefmt::now_rfc3339(),
                    session_id: sid.clone(),
                    trigger: spt_store::sessions::SessionTrigger::Boot,
                    // Mirror info.cwd if the ready bind tracked one (REQ-SESSION-RESUME-TEMPLATE).
                    cwd: rec.cwd.clone(),
                },


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-msg\src\ready.rs has been updated successfully. (file state is current in your context — no need to Read it back)

                &spt_store::sessions::SessionEntry {
                    ts: spt_store::timefmt::now_rfc3339(),
                    session_id: sid.clone(),
                    trigger: spt_store::sessions::SessionTrigger::Boot,
                },

                &spt_store::sessions::SessionEntry {
                    ts: spt_store::timefmt::now_rfc3339(),
                    session_id: sid.clone(),
                    trigger: spt_store::sessions::SessionTrigger::Boot,
                    // Mirror info.cwd if the ready bind tracked one (REQ-SESSION-RESUME-TEMPLATE).
                    cwd: rec.cwd.clone(),
                },

+                    // Mirror info.cwd if the ready bind tracked one (REQ-SESSION-RESUME-TEMPLATE).

+                    cwd: rec.cwd.clone(),


                &spt_store::sessions::SessionEntry {
                    ts: spt_store::timefmt::rfc3339_utc(1_700_000_000),
                    session_id: "A".into(),
                    trigger: spt_store::sessions::SessionTrigger::Boot,
                },

                &spt_store::sessions::SessionEntry {
                    ts: spt_store::timefmt::rfc3339_utc(1_700_000_000),
                    session_id: "A".into(),
                    trigger: spt_store::sessions::SessionTrigger::Boot,
                    cwd: None,
                },


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\digest.rs has been updated successfully. (file state is current in your context — no need to Read it back)

                &spt_store::sessions::SessionEntry {
                    ts: spt_store::timefmt::rfc3339_utc(1_700_000_000),
                    session_id: "A".into(),
                    trigger: spt_store::sessions::SessionTrigger::Boot,
                },

                &spt_store::sessions::SessionEntry {
                    ts: spt_store::timefmt::rfc3339_utc(1_700_000_000),
                    session_id: "A".into(),
                    trigger: spt_store::sessions::SessionTrigger::Boot,
                    cwd: None,
                },


                &spt_store::sessions::SessionEntry {
                    ts: spt_store::timefmt::rfc3339_utc(1_700_000_100),
                    session_id: "B".into(),
                    trigger: spt_store::sessions::SessionTrigger::Clear,
                },

                &spt_store::sessions::SessionEntry {
                    ts: spt_store::timefmt::rfc3339_utc(1_700_000_100),
                    session_id: "B".into(),
                    trigger: spt_store::sessions::SessionTrigger::Clear,
                    cwd: None,
                },


                &spt_store::sessions::SessionEntry {
                    ts: spt_store::timefmt::rfc3339_utc(1_700_000_100),
                    session_id: "B".into(),
                    trigger: spt_store::sessions::SessionTrigger::Clear,
                },

                &spt_store::sessions::SessionEntry {
                    ts: spt_store::timefmt::rfc3339_utc(1_700_000_100),
                    session_id: "B".into(),
                    trigger: spt_store::sessions::SessionTrigger::Clear,
                    cwd: None,
                },


79	    rec.adapter = Some("cc".to_string());
80	    write_info(&perch, &rec).unwrap();
81	    spt_store::sessions::append(
82	        &perch,
83	        &SessionEntry {
84	            ts: "2026-06-13T21:00:00Z".into(),
85	            session_id: "A".into(),
86	            trigger: SessionTrigger::Boot,
87	        },
88	    )
89	    .unwrap();
90	    spt_store::sessions::append(
91	        &perch,
92	        &SessionEntry {
93	            ts: "2026-06-13T21:00:05Z".into(),
94	            session_id: "B".into(),
95	            trigger: SessionTrigger::Clear,
96	        },
97	    )
98	    .unwrap();

    rec.adapter = Some("cc".to_string());
    write_info(&perch, &rec).unwrap();
    spt_store::sessions::append(
        &perch,
        &SessionEntry {
            ts: "2026-06-13T21:00:00Z".into(),
            session_id: "A".into(),
            trigger: SessionTrigger::Boot,
        },
    )
    .unwrap();
    spt_store::sessions::append(
        &perch,
        &SessionEntry {
            ts: "2026-06-13T21:00:05Z".into(),
            session_id: "B".into(),
            trigger: SessionTrigger::Clear,
        },
    )
    .unwrap();


        &SessionEntry {
            ts: "2026-06-13T21:00:00Z".into(),
            session_id: "A".into(),
            trigger: SessionTrigger::Boot,
        },

        &SessionEntry {
            ts: "2026-06-13T21:00:00Z".into(),
            session_id: "A".into(),
            trigger: SessionTrigger::Boot,
            cwd: None,
        },


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\two_origin_spanning.rs has been updated successfully. (file state is current in your context — no need to Read it back)

        &SessionEntry {
            ts: "2026-06-13T21:00:00Z".into(),
            session_id: "A".into(),
            trigger: SessionTrigger::Boot,
            cwd: None,
        },

//! The M10 capstone integration (ADR-0019) — the dual-consumer + thread-spanning
//! + two-origin proof, end to end, from the same harness source files.
//!
//! This is the proof the milestone exists for: the **same** per-session log files
//! feed two consumers with opposite needs without conflict —
//!
//! - the **echo-commune** reads them **opaque + verbatim** through `[history]`
//!   (full fidelity, extra adapter fields preserved), while
//! - the **digest** reads them **contract-typed** through its own `[digest]`
//!   extractor (REQ-TERM-5) —
//!
//! the digest **spans** a `/clear` across two sessions with a visible boundary
//! marker (REQ-TERM-6), and **interleaves** spt's own context-injection entries
//! (psyche download, owl message) with the extracted activity by `ts`
//! (REQ-TERM-7). The M9 dual-consumer conflict (one normalizer forced to serve
//! both) is gone — two seams, one source.
// [int->REQ-TERM-5]
// [int->REQ-TERM-6]
// [int->REQ-TERM-7]

use std::collections::BTreeMap;
use std::time::Duration;

use spt_daemon::{project_endpoint_digest, render_digest, DigestOverride};
use spt_live::fetch_history;
use spt_runtime::manifest::{History, HistoryStrategy};
use spt_store::info::{write_info, InfoJson};
use spt_store::perch::{self, resolve_perch_path, ParentHint};
use spt_store::sessions::{SessionEntry, SessionTrigger};
use spt_term::DigestEntry;

// A stdin pass-through: the source bytes round-trip to stdout. As a `[history]`
// normalizer it preserves full fidelity (echo's opaque needs); as a `[digest]`
// extractor over a contract-shaped source it is the trivial identity extractor.
#[cfg(unix)]
const PASSTHROUGH: &str = "cat";
#[cfg(windows)]
const PASSTHROUGH: &str = "cmd /C findstr /r \".\"";

#[test]
fn one_source_two_consumers_spans_and_merges() {
    let home = tempfile::tempdir().expect("tempdir");
    std::env::set_var("SPT_HOME", home.path());
    let hp = home.path();

    // Two per-session logs, each a contract record carrying an EXTRA adapter field
    // (the fidelity the echo consumer keeps and the digest contract ignores).
    std::fs::write(
        hp.join("sess-A.jsonl"),
        "{\"role\":\"input\",\"text\":\"before clear\",\"ts\":\"2026-06-13T21:00:00Z\",\"raw_extra\":\"FULL-FIDELITY-A\"}\n",
    )
    .unwrap();
    std::fs::write(
        hp.join("sess-B.jsonl"),
        "{\"role\":\"input\",\"text\":\"after clear\",\"ts\":\"2026-06-13T21:00:10Z\",\"raw_extra\":\"FULL-FIDELITY-B\"}\n",
    )
    .unwrap();

    // One adapter, TWO seams over the SAME source files: [history] (opaque, for
    // echo) and [digest] (contract, for the digest). Single-quoted TOML literals
    // carry the path + findstr quotes verbatim.
    let src_tpl = hp.join("sess-{session_id}.jsonl");
    let manifest = format!(
        "[adapter]\nname = \"cc\"\nkind = \"harness\"\nversion = \"1\"\nmin_spt_core_version = \"0\"\n\n\
         [history]\nstrategy = \"locate_normalize\"\nlocate_template = '{src}'\nnormalize_command = '{pass}'\n\n\
         [digest]\nextractor = '{pass}'\nsource = '{src}'\n",
        src = src_tpl.display(),
        pass = PASSTHROUGH,
    );
    let asrc = perch::spt_home().join("srcs").join("cc");
    std::fs::create_dir_all(&asrc).unwrap();
    std::fs::write(asrc.join("manifest.toml"), manifest).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &asrc, 1).unwrap();

    // Establish the endpoint + its session ledger (boot A, then /clear → B).
    let perch = resolve_perch_path("cc", ParentHint::Infer);
    std::fs::create_dir_all(&perch).unwrap();
    let mut rec = InfoJson::new("cc", "0", std::process::id(), "B", "live_agent");
    rec.adapter = Some("cc".to_string());
    write_info(&perch, &rec).unwrap();
    spt_store::sessions::append(
        &perch,
        &SessionEntry {
            ts: "2026-06-13T21:00:00Z".into(),
            session_id: "A".into(),
            trigger: SessionTrigger::Boot,
        },
    )
    .unwrap();
    spt_store::sessions::append(
        &perch,
        &SessionEntry {
            ts: "2026-06-13T21:00:05Z".into(),
            session_id: "B".into(),
            trigger: SessionTrigger::Clear,
        },
    )
    .unwrap();

    // spt's own context injections (the second origin) → digest.log, by ts: a
    // psyche download in session A, an owl message in session B.
    spt_store::history::append_context_entry(
        &perch,
        "psyche_download",
        "downloaded 3 psyche notes",
        "2026-06-13T21:00:01Z",
    )
    .unwrap();
    spt_store::history::append_context_entry(
        &perch,
        "owl_message",
        "ping from doyle",
        "2026-06-13T21:00:11Z",
    )
    .unwrap();

    // ── Consumer 1: the echo-commune reads [history] OPAQUE + verbatim ──────────
    let history = History {
        strategy: HistoryStrategy::LocateNormalize,
        fetcher: None,
        locate_template: Some(src_tpl.display().to_string()),
        normalize_command: Some(PASSTHROUGH.to_string()),
    };
    let mut keys = BTreeMap::new();
    keys.insert("session_id".to_string(), "A".to_string());
    let echo = fetch_history(&history, &keys, "cc", ParentHint::Infer, Duration::from_secs(10))
        .expect("history fetch");
    assert_eq!(echo.len(), 1);
    assert!(
        echo[0].raw.contains("FULL-FIDELITY-A"),
        "echo keeps the adapter's full-fidelity line verbatim: {}",
        echo[0].raw
    );

    // ── Consumer 2: the digest reads [digest] CONTRACT, spanned + merged ────────
    let digest = project_endpoint_digest("cc", &DigestOverride::default());
    let inputs: Vec<_> = digest.turns.iter().filter_map(|t| t.input.as_deref()).collect();
    assert_eq!(
        inputs,
        vec!["before clear", "after clear"],
        "the digest SPANS the /clear (both sessions, window not reset)"
    );

    // A /clear boundary divider separates the sessions (REQ-TERM-6).
    assert!(
        digest.turns.iter().any(|t| t.entries.iter().any(|e| matches!(
            e,
            DigestEntry::Boundary { kind, .. } if kind == "clear"
        ))),
        "a /clear boundary marker is present"
    );

    // The psyche download (ts in session A) interleaves into the first turn, the
    // owl message (ts in session B) into the second — two-origin by ts (REQ-TERM-7).
    let turn_a = &digest.turns[0];
    assert!(
        turn_a.entries.iter().any(|e| matches!(
            e,
            DigestEntry::Context { kind, .. } if kind == "psyche_download"
        )),
        "psyche download interleaves into session A's turn: {:?}",
        turn_a.entries
    );
    let last = digest.turns.last().unwrap();
    assert!(
        last.entries.iter().any(|e| matches!(
            e,
            DigestEntry::Context { kind, body, .. } if kind == "owl_message" && body.contains("doyle")
        )),
        "owl message interleaves into session B's turn: {:?}",
        last.entries
    );

    // The contract dropped the adapter's extra field (digest is contract-typed,
    // not opaque) — the two consumers genuinely differ on the same source.
    let rendered = render_digest(&digest);
    assert!(
        !rendered.contains("FULL-FIDELITY"),
        "the digest is contract-typed (extra adapter fields ignored): {rendered}"
    );
    assert!(rendered.contains("── /clear ──"), "boundary renders distinctively: {rendered}");
}


        &SessionEntry {
            ts: "2026-06-13T21:00:05Z".into(),
            session_id: "B".into(),
            trigger: SessionTrigger::Clear,
        },

        &SessionEntry {
            ts: "2026-06-13T21:00:05Z".into(),
            session_id: "B".into(),
            trigger: SessionTrigger::Clear,
            cwd: None,
        },


        &SessionEntry {
            ts: "2026-06-13T21:00:05Z".into(),
            session_id: "B".into(),
            trigger: SessionTrigger::Clear,
            cwd: None,
        },

//! The M10 capstone integration (ADR-0019) — the dual-consumer + thread-spanning
//! + two-origin proof, end to end, from the same harness source files.
//!
//! This is the proof the milestone exists for: the **same** per-session log files
//! feed two consumers with opposite needs without conflict —
//!
//! - the **echo-commune** reads them **opaque + verbatim** through `[history]`
//!   (full fidelity, extra adapter fields preserved), while
//! - the **digest** reads them **contract-typed** through its own `[digest]`
//!   extractor (REQ-TERM-5) —
//!
//! the digest **spans** a `/clear` across two sessions with a visible boundary
//! marker (REQ-TERM-6), and **interleaves** spt's own context-injection entries
//! (psyche download, owl message) with the extracted activity by `ts`
//! (REQ-TERM-7). The M9 dual-consumer conflict (one normalizer forced to serve
//! both) is gone — two seams, one source.
// [int->REQ-TERM-5]
// [int->REQ-TERM-6]
// [int->REQ-TERM-7]

use std::collections::BTreeMap;
use std::time::Duration;

use spt_daemon::{project_endpoint_digest, render_digest, DigestOverride};
use spt_live::fetch_history;
use spt_runtime::manifest::{History, HistoryStrategy};
use spt_store::info::{write_info, InfoJson};
use spt_store::perch::{self, resolve_perch_path, ParentHint};
use spt_store::sessions::{SessionEntry, SessionTrigger};
use spt_term::DigestEntry;

// A stdin pass-through: the source bytes round-trip to stdout. As a `[history]`
// normalizer it preserves full fidelity (echo's opaque needs); as a `[digest]`
// extractor over a contract-shaped source it is the trivial identity extractor.
#[cfg(unix)]
const PASSTHROUGH: &str = "cat";
#[cfg(windows)]
const PASSTHROUGH: &str = "cmd /C findstr /r \".\"";

#[test]
fn one_source_two_consumers_spans_and_merges() {
    let home = tempfile::tempdir().expect("tempdir");
    std::env::set_var("SPT_HOME", home.path());
    let hp = home.path();

    // Two per-session logs, each a contract record carrying an EXTRA adapter field
    // (the fidelity the echo consumer keeps and the digest contract ignores).
    std::fs::write(
        hp.join("sess-A.jsonl"),
        "{\"role\":\"input\",\"text\":\"before clear\",\"ts\":\"2026-06-13T21:00:00Z\",\"raw_extra\":\"FULL-FIDELITY-A\"}\n",
    )
    .unwrap();
    std::fs::write(
        hp.join("sess-B.jsonl"),
        "{\"role\":\"input\",\"text\":\"after clear\",\"ts\":\"2026-06-13T21:00:10Z\",\"raw_extra\":\"FULL-FIDELITY-B\"}\n",
    )
    .unwrap();

    // One adapter, TWO seams over the SAME source files: [history] (opaque, for
    // echo) and [digest] (contract, for the digest). Single-quoted TOML literals
    // carry the path + findstr quotes verbatim.
    let src_tpl = hp.join("sess-{session_id}.jsonl");
    let manifest = format!(
        "[adapter]\nname = \"cc\"\nkind = \"harness\"\nversion = \"1\"\nmin_spt_core_version = \"0\"\n\n\
         [history]\nstrategy = \"locate_normalize\"\nlocate_template = '{src}'\nnormalize_command = '{pass}'\n\n\
         [digest]\nextractor = '{pass}'\nsource = '{src}'\n",
        src = src_tpl.display(),
        pass = PASSTHROUGH,
    );
    let asrc = perch::spt_home().join("srcs").join("cc");
    std::fs::create_dir_all(&asrc).unwrap();
    std::fs::write(asrc.join("manifest.toml"), manifest).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &asrc, 1).unwrap();

    // Establish the endpoint + its session ledger (boot A, then /clear → B).
    let perch = resolve_perch_path("cc", ParentHint::Infer);
    std::fs::create_dir_all(&perch).unwrap();
    let mut rec = InfoJson::new("cc", "0", std::process::id(), "B", "live_agent");
    rec.adapter = Some("cc".to_string());
    write_info(&perch, &rec).unwrap();
    spt_store::sessions::append(
        &perch,
        &SessionEntry {
            ts: "2026-06-13T21:00:00Z".into(),
            session_id: "A".into(),
            trigger: SessionTrigger::Boot,
            cwd: None,
        },
    )
    .unwrap();
    spt_store::sessions::append(
        &perch,
        &SessionEntry {
            ts: "2026-06-13T21:00:05Z".into(),
            session_id: "B".into(),
            trigger: SessionTrigger::Clear,
        },
    )
    .unwrap();

    // spt's own context injections (the second origin) → digest.log, by ts: a
    // psyche download in session A, an owl message in session B.
    spt_store::history::append_context_entry(
        &perch,
        "psyche_download",
        "downloaded 3 psyche notes",
        "2026-06-13T21:00:01Z",
    )
    .unwrap();
    spt_store::history::append_context_entry(
        &perch,
        "owl_message",
        "ping from doyle",
        "2026-06-13T21:00:11Z",
    )
    .unwrap();

    // ── Consumer 1: the echo-commune reads [history] OPAQUE + verbatim ──────────
    let history = History {
        strategy: HistoryStrategy::LocateNormalize,
        fetcher: None,
        locate_template: Some(src_tpl.display().to_string()),
        normalize_command: Some(PASSTHROUGH.to_string()),
    };
    let mut keys = BTreeMap::new();
    keys.insert("session_id".to_string(), "A".to_string());
    let echo = fetch_history(&history, &keys, "cc", ParentHint::Infer, Duration::from_secs(10))
        .expect("history fetch");
    assert_eq!(echo.len(), 1);
    assert!(
        echo[0].raw.contains("FULL-FIDELITY-A"),
        "echo keeps the adapter's full-fidelity line verbatim: {}",
        echo[0].raw
    );

    // ── Consumer 2: the digest reads [digest] CONTRACT, spanned + merged ────────
    let digest = project_endpoint_digest("cc", &DigestOverride::default());
    let inputs: Vec<_> = digest.turns.iter().filter_map(|t| t.input.as_deref()).collect();
    assert_eq!(
        inputs,
        vec!["before clear", "after clear"],
        "the digest SPANS the /clear (both sessions, window not reset)"
    );

    // A /clear boundary divider separates the sessions (REQ-TERM-6).
    assert!(
        digest.turns.iter().any(|t| t.entries.iter().any(|e| matches!(
            e,
            DigestEntry::Boundary { kind, .. } if kind == "clear"
        ))),
        "a /clear boundary marker is present"
    );

    // The psyche download (ts in session A) interleaves into the first turn, the
    // owl message (ts in session B) into the second — two-origin by ts (REQ-TERM-7).
    let turn_a = &digest.turns[0];
    assert!(
        turn_a.entries.iter().any(|e| matches!(
            e,
            DigestEntry::Context { kind, .. } if kind == "psyche_download"
        )),
        "psyche download interleaves into session A's turn: {:?}",
        turn_a.entries
    );
    let last = digest.turns.last().unwrap();
    assert!(
        last.entries.iter().any(|e| matches!(
            e,
            DigestEntry::Context { kind, body, .. } if kind == "owl_message" && body.contains("doyle")
        )),
        "owl message interleaves into session B's turn: {:?}",
        last.entries
    );

    // The contract dropped the adapter's extra field (digest is contract-typed,
    // not opaque) — the two consumers genuinely differ on the same source.
    let rendered = render_digest(&digest);
    assert!(
        !rendered.contains("FULL-FIDELITY"),
        "the digest is contract-typed (extra adapter fields ignored): {rendered}"
    );
    assert!(rendered.contains("── /clear ──"), "boundary renders distinctively: {rendered}");
}


Task 3 done. **Task 4** — resume-vs-self template selection.


Updated task #3 status


Updated task #4 status


/// Validate the adapter and fill its `[session.self]` command for one endpoint,
/// returning the tokenized `program + args`. Pure (no spawn) — the broker call in
/// [`launch_harness_brokered_in`] is the only side effect. Fails closed: a
/// non-harness adapter, a missing `[session.self]`, an unresolved `{placeholder}`,
/// or an empty command all error before anything is spawned.
// [impl->REQ-HOST-RUN-1]
pub fn prepare_harness_spawn(
    endpoint_id: &str,
    adapter_name: &str,
    session_id: &str,
    manifest: &Manifest,
) -> Result<PreparedSpawn, String> {
    if manifest.adapter.kind != AdapterKind::Harness {
        return Err(format!(
            "adapter '{adapter_name}' is kind={:?}, not a harness — \
             `spt endpoint run` brings up harness endpoints",
            manifest.adapter.kind
        ));
    }
    let role = manifest.session.self_.as_ref().ok_or_else(|| {
        format!("adapter '{adapter_name}' declares no [session.self] — nothing to bring up")
    })?;

/// Validate the adapter and fill its session command for one endpoint, returning
/// the tokenized `program + args`. Pure (no spawn) — the broker call in
/// [`launch_harness_brokered_in`] is the only side effect. Fails closed: a
/// non-harness adapter, a missing role command, an unresolved `{placeholder}`,
/// or an empty command all error before anything is spawned.
///
/// `is_resume`: select the NATIVE-resume role over the fresh one — when `true`
/// AND the adapter declares `[session.resume]`, the resume template is filled
/// (the harness runs e.g. `claude -r {session_id}` instead of a fresh
/// `claude --session-id {session_id}`, which would start a blank transcript). A
/// `true` with NO `[session.resume]` declared FALLS BACK to `[session.self]`
/// (full back-compat); a `false` always uses `[session.self]`.
// [impl->REQ-HOST-RUN-1]
// [impl->REQ-SESSION-RESUME-TEMPLATE]
pub fn prepare_harness_spawn(
    endpoint_id: &str,
    adapter_name: &str,
    session_id: &str,
    manifest: &Manifest,
    is_resume: bool,
) -> Result<PreparedSpawn, String> {
    if manifest.adapter.kind != AdapterKind::Harness {
        return Err(format!(
            "adapter '{adapter_name}' is kind={:?}, not a harness — \
             `spt endpoint run` brings up harness endpoints",
            manifest.adapter.kind
        ));
    }
    // Resume selects `[session.resume]` when declared, else falls back to
    // `[session.self]` (an adapter with no resume sibling behaves exactly as
    // today). A fresh bringup always uses `[session.self]`.
    let role = if is_resume {
        manifest
            .session
            .resume
            .as_ref()
            .or(manifest.session.self_.as_ref())
    } else {
        manifest.session.self_.as_ref()
    }
    .ok_or_else(|| {
        format!("adapter '{adapter_name}' declares no [session.self] — nothing to bring up")
    })?;


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\harnesshost.rs has been updated successfully. (file state is current in your context — no need to Read it back)

/// Validate the adapter and fill its `[session.self]` command for one endpoint,
/// returning the tokenized `program + args`. Pure (no spawn) — the broker call in
/// [`launch_harness_brokered_in`] is the only side effect. Fails closed: a
/// non-harness adapter, a missing `[session.self]`, an unresolved `{placeholder}`,
/// or an empty command all error before anything is spawned.
// [impl->REQ-HOST-RUN-1]
pub fn prepare_harness_spawn(
    endpoint_id: &str,
    adapter_name: &str,
    session_id: &str,
    manifest: &Manifest,
) -> Result<PreparedSpawn, String> {
    if manifest.adapter.kind != AdapterKind::Harness {
        return Err(format!(
            "adapter '{adapter_name}' is kind={:?}, not a harness — \
             `spt endpoint run` brings up harness endpoints",
            manifest.adapter.kind
        ));
    }
    let role = manifest.session.self_.as_ref().ok_or_else(|| {
        format!("adapter '{adapter_name}' declares no [session.self] — nothing to bring up")
    })?;

/// Validate the adapter and fill its session command for one endpoint, returning
/// the tokenized `program + args`. Pure (no spawn) — the broker call in
/// [`launch_harness_brokered_in`] is the only side effect. Fails closed: a
/// non-harness adapter, a missing role command, an unresolved `{placeholder}`,
/// or an empty command all error before anything is spawned.
///
/// `is_resume`: select the NATIVE-resume role over the fresh one — when `true`
/// AND the adapter declares `[session.resume]`, the resume template is filled
/// (the harness runs e.g. `claude -r {session_id}` instead of a fresh
/// `claude --session-id {session_id}`, which would start a blank transcript). A
/// `true` with NO `[session.resume]` declared FALLS BACK to `[session.self]`
/// (full back-compat); a `false` always uses `[session.self]`.
// [impl->REQ-HOST-RUN-1]
// [impl->REQ-SESSION-RESUME-TEMPLATE]
pub fn prepare_harness_spawn(
    endpoint_id: &str,
    adapter_name: &str,
    session_id: &str,
    manifest: &Manifest,
    is_resume: bool,
) -> Result<PreparedSpawn, String> {
    if manifest.adapter.kind != AdapterKind::Harness {
        return Err(format!(
            "adapter '{adapter_name}' is kind={:?}, not a harness — \
             `spt endpoint run` brings up harness endpoints",
            manifest.adapter.kind
        ));
    }
    // Resume selects `[session.resume]` when declared, else falls back to
    // `[session.self]` (an adapter with no resume sibling behaves exactly as
    // today). A fresh bringup always uses `[session.self]`.
    let role = if is_resume {
        manifest
            .session
            .resume
            .as_ref()
            .or(manifest.session.self_.as_ref())
    } else {
        manifest.session.self_.as_ref()
    }
    .ok_or_else(|| {
        format!("adapter '{adapter_name}' declares no [session.self] — nothing to bring up")
    })?;

-/// Validate the adapter and fill its `[session.self]` command for one endpoint,

-/// returning the tokenized `program + args`. Pure (no spawn) — the broker call in

+/// Validate the adapter and fill its session command for one endpoint, returning

+/// the tokenized `program + args`. Pure (no spawn) — the broker call in

-/// non-harness adapter, a missing `[session.self]`, an unresolved `{placeholder}`,

+/// non-harness adapter, a missing role command, an unresolved `{placeholder}`,

+/// `is_resume`: select the NATIVE-resume role over the fresh one — when `true`

+/// AND the adapter declares `[session.resume]`, the resume template is filled

+/// (the harness runs e.g. `claude -r {session_id}` instead of a fresh

+/// `claude --session-id {session_id}`, which would start a blank transcript). A

+/// `true` with NO `[session.resume]` declared FALLS BACK to `[session.self]`

+/// (full back-compat); a `false` always uses `[session.self]`.

-    let role = manifest.session.self_.as_ref().ok_or_else(|| {

+    // Resume selects `[session.resume]` when declared, else falls back to

+    // `[session.self]` (an adapter with no resume sibling behaves exactly as

+    // today). A fresh bringup always uses `[session.self]`.

+            .or(manifest.session.self_.as_ref())

+        manifest.session.self_.as_ref()

// The guaranteed-fill catalog for a self-spawn. `id` rides argv so the harness


return Err("empty [session.self] command".into());

return Err("empty session command".into());


-        return Err("empty [session.self] command".into());

+        return Err("empty session command".into());

// [impl->REQ-HAZARD-ENV-SUBST] every [env.<VAR>] direction=inject value is

// {key}-substituted from the SAME catalog and passed to the broker for the


/// `cwd`: the project-scoped working directory the harness PTY lands in
/// (REQ-HOST-RUN-2). `None` inherits the broker's cwd (the W1 bringup shortcut);
/// `Some(dir)` is what a project-scoped launcher (`cc <id>` at a project root)
/// passes so the harness reads the RIGHT `.claude`/session history/digest source.
// [impl->REQ-HOST-RUN-1]
// [impl->REQ-HOST-RUN-2]
pub fn launch_harness_brokered_in(
    broker_name: &str,
    endpoint_id: &str,
    adapter_name: &str,
    session_id: &str,
    manifest: &Manifest,
    cwd: Option<&str>,
) -> Result<HarnessSpawned, String> {
    let PreparedSpawn {
        tokens,
        env,
        translation_binary,
    } = prepare_harness_spawn(endpoint_id, adapter_name, session_id, manifest)?;

/// `cwd`: the project-scoped working directory the harness PTY lands in
/// (REQ-HOST-RUN-2). `None` inherits the broker's cwd (the W1 bringup shortcut);
/// `Some(dir)` is what a project-scoped launcher (`cc <id>` at a project root)
/// passes so the harness reads the RIGHT `.claude`/session history/digest source.
///
/// `is_resume`: forwarded to [`prepare_harness_spawn`] — `true` selects the
/// adapter's `[session.resume]` native-resume command (REQ-SESSION-RESUME-TEMPLATE)
/// when declared, else falls back to `[session.self]`.
// [impl->REQ-HOST-RUN-1]
// [impl->REQ-HOST-RUN-2]
// [impl->REQ-SESSION-RESUME-TEMPLATE]
pub fn launch_harness_brokered_in(
    broker_name: &str,
    endpoint_id: &str,
    adapter_name: &str,
    session_id: &str,
    manifest: &Manifest,
    cwd: Option<&str>,
    is_resume: bool,
) -> Result<HarnessSpawned, String> {
    let PreparedSpawn {
        tokens,
        env,
        translation_binary,
    } = prepare_harness_spawn(endpoint_id, adapter_name, session_id, manifest, is_resume)?;