crates\spt-daemon\src\broker.rs:52:    NetStreamsReply, PairJoinReply, PairJoinReq, ResizeReq, SessionInfo, SessionsReply, SpawnReq,
crates\spt-daemon\src\broker.rs:538:        let req: SpawnReq =
crates\spt-daemon\src\broker.rs:540:        let session = PtySession::spawn_program(
crates\spt-daemon\src\applyhost.rs:296:    use crate::msg::SpawnReq;
crates\spt-daemon\src\applyhost.rs:564:    fn echo_spawn_req() -> SpawnReq {
crates\spt-daemon\src\applyhost.rs:569:        SpawnReq {
crates\spt-daemon\src\applyhost.rs:591:        let sid = seat.spawn_session(echo_spawn_req()).expect("spawn");
crates\spt-daemon\src\applyhost.rs:633:        let sid = seat.spawn_session(echo_spawn_req()).expect("spawn");
crates\spt-daemon\src\brain.rs:41:    SessionsReply, SpawnReq, Spawned, SubscribeReq, KIND_APPLIED, KIND_BRAIN_RESTART,
crates\spt-daemon\src\brain.rs:362:    pub fn spawn_session(&mut self, req: SpawnReq) -> io::Result<u64> {
crates\spt-daemon\src\brain.rs:363:        self.spawn_session_pid(req)
crates\spt-daemon\src\brain.rs:367:    /// [`spawn_session`](Self::spawn_session) that also surfaces the child's
crates\spt-daemon\src\brain.rs:381:    pub fn spawn_session_pid(&mut self, req: SpawnReq) -> io::Result<(u64, Option<u32>)> {
crates\spt-daemon\src\brain.rs:384:            serde_json::to_value(req).expect("SpawnReq serializes"),
crates\spt-daemon\src\brain.rs:752:    /// until the reply (consuming interleaved events like [`Brain::spawn_session`]).
crates\spt-daemon\src\harnesshost.rs:19://! the broker-spawned child inherits the broker's environment. No `SpawnReq` env
crates\spt-daemon\src\harnesshost.rs:31:use crate::msg::SpawnReq;
crates\spt-daemon\src\harnesshost.rs:117:        .spawn_session_pid(SpawnReq {
crates\spt-daemon\src\msg.rs:131:pub struct SpawnReq {
crates\spt-daemon\src\msg.rs:647:        let req: SpawnReq = serde_json::from_value(wire).unwrap();
crates\spt-daemon\src\notif.rs:307:        .spawn_session(NOTIF_ROLE, &notif_keys(row))
crates\spt-daemon\src\shellhost.rs:36:use crate::msg::SpawnReq;
crates\spt-daemon\src\shellhost.rs:223:        .spawn_session_pid(SpawnReq {


126	pub const KIND_BRAIN_RESTARTED: &str = "brain-restarted";
127
128	/// `spawn` payload — the program + args + initial geometry. The broker spawns
129	/// it under a PTY (no shell interpretation; argv is passed through).
130	#[derive(Debug, Clone, Serialize, Deserialize)]
131	pub struct SpawnReq {
132	    /// The program to exec (resolved on `PATH` by the OS spawn).
133	    pub program: String,
134	    /// Its arguments, verbatim.
135	    #[serde(default)]
136	    pub args: Vec<String>,
137	    /// Initial PTY rows.
138	    pub rows: u16,
139	    /// Initial PTY cols.
140	    pub cols: u16,
141	    /// The endpoint this session belongs to (D9-1: the remote-drive
142	    /// dispatcher's gate subject — `serve_attach` gates on the *owning*
143	    /// endpoint's whitelist, resolved from the broker's table, never from
144	    /// wire bytes). Empty = unlabeled (a legacy/test spawn); the dispatcher
145	    /// refuses to serve an unlabeled session, fail-closed.
146	    #[serde(default)]
147	    pub endpoint: String,
148	}
149
150	/// `spawned` payload — the broker-minted session id + the child's OS pid.
151	#[derive(Debug, Clone, Serialize, Deserialize)]
152	pub struct Spawned {
153	    /// The id the brain uses to address this session in later commands.
154	    pub session_id: u64,
155	    /// The child's OS pid, if the backend exposes one.

pub const KIND_BRAIN_RESTARTED: &str = "brain-restarted";

/// `spawn` payload — the program + args + initial geometry. The broker spawns
/// it under a PTY (no shell interpretation; argv is passed through).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpawnReq {
    /// The program to exec (resolved on `PATH` by the OS spawn).
    pub program: String,
    /// Its arguments, verbatim.
    #[serde(default)]
    pub args: Vec<String>,
    /// Initial PTY rows.
    pub rows: u16,
    /// Initial PTY cols.
    pub cols: u16,
    /// The endpoint this session belongs to (D9-1: the remote-drive
    /// dispatcher's gate subject — `serve_attach` gates on the *owning*
    /// endpoint's whitelist, resolved from the broker's table, never from
    /// wire bytes). Empty = unlabeled (a legacy/test spawn); the dispatcher
    /// refuses to serve an unlabeled session, fail-closed.
    #[serde(default)]
    pub endpoint: String,
}

/// `spawned` payload — the broker-minted session id + the child's OS pid.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Spawned {
    /// The id the brain uses to address this session in later commands.
    pub session_id: u64,
    /// The child's OS pid, if the backend exposes one.


532	    /// Every hosted session gets its **own** drain + exit-waiter threads (KH
533	    /// 7.4): a hung child blocks only its own threads — shell instances ride
534	    /// exactly this isolation (one owner's hung shell binary stalls neither
535	    /// another owner's delivery nor the broker's control dispatch).
536	    // [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]
537	    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {
538	        let req: SpawnReq =
539	            serde_json::from_value(env.payload).map_err(|e| format!("bad spawn payload: {e}"))?;
540	        let session = PtySession::spawn_program(
541	            &req.program,
542	            &req.args,
543	            SurfaceSize::new(req.rows, req.cols),
544	        )
545	        .map_err(|e| format!("spawn failed: {e}"))?;
546	        let session = Arc::new(session);
547	        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
548	        let pid = session.process_id();
549
550	        // The output log, with the spawning connection pre-attached so no chunk
551	        // is missed before an explicit subscribe.
552	        let log = Arc::new(Mutex::new(OutputLog::new(id, self.log_cap_chunks)));
553	        log.lock().unwrap().attach(Arc::clone(send), 0);
554
555	        // Output drain → appends to the log (which live-sends to the subscriber).
556	        let log_drain = Arc::clone(&log);
557	        let drain = session
558	            .drain(move |chunk| {
559	                log_drain.lock().unwrap().append(chunk);
560	            })
561	            .map_err(|e| format!("drain failed: {e}"))?;
562
563	        // Exit waiter → one `exit` frame when the child is reaped, then the dead
564	        // session is removed from the table. Runs on its own thread; `wait`
565	        // holds the child lock but `kill` uses a split killer.
566	        let sessions_exit = Arc::clone(&self.sessions);
567	        let log_exit = Arc::clone(&log);
568	        let waiter = Arc::clone(&session);
569	        thread::spawn(move || {
570	            if let Ok(status) = waiter.wait() {
571	                let ev = ExitEvent {

    /// Every hosted session gets its **own** drain + exit-waiter threads (KH
    /// 7.4): a hung child blocks only its own threads — shell instances ride
    /// exactly this isolation (one owner's hung shell binary stalls neither
    /// another owner's delivery nor the broker's control dispatch).
    // [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]
    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {
        let req: SpawnReq =
            serde_json::from_value(env.payload).map_err(|e| format!("bad spawn payload: {e}"))?;
        let session = PtySession::spawn_program(
            &req.program,
            &req.args,
            SurfaceSize::new(req.rows, req.cols),
        )
        .map_err(|e| format!("spawn failed: {e}"))?;
        let session = Arc::new(session);
        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
        let pid = session.process_id();

        // The output log, with the spawning connection pre-attached so no chunk
        // is missed before an explicit subscribe.
        let log = Arc::new(Mutex::new(OutputLog::new(id, self.log_cap_chunks)));
        log.lock().unwrap().attach(Arc::clone(send), 0);

        // Output drain → appends to the log (which live-sends to the subscriber).
        let log_drain = Arc::clone(&log);
        let drain = session
            .drain(move |chunk| {
                log_drain.lock().unwrap().append(chunk);
            })
            .map_err(|e| format!("drain failed: {e}"))?;

        // Exit waiter → one `exit` frame when the child is reaped, then the dead
        // session is removed from the table. Runs on its own thread; `wait`
        // holds the child lock but `kill` uses a split killer.
        let sessions_exit = Arc::clone(&self.sessions);
        let log_exit = Arc::clone(&log);
        let waiter = Arc::clone(&session);
        thread::spawn(move || {
            if let Ok(status) = waiter.wait() {
                let ev = ExitEvent {


Plan: additive `SpawnReq.cwd: Option<String>` → spt-term `spawn_program_in(cwd)` → broker dispatch_spawn → harnesshost param → cli derives project cwd. Find all `SpawnReq {` literals needing `cwd: None`:


crates\spt-daemon\tests\daemon_e2e.rs:58:fn echo_req() -> SpawnReq {
crates\spt-daemon\tests\daemon_e2e.rs:63:    SpawnReq {
crates\spt-daemon\tests\budget.rs:84:        let req = SpawnReq {
crates\spt-daemon\tests\broker.rs:41:fn echo_spawn_req() -> SpawnReq {
crates\spt-daemon\tests\broker.rs:46:    SpawnReq {
crates\spt-daemon\tests\brain_swap.rs:49:fn echo_spawn_req() -> SpawnReq {
crates\spt-daemon\tests\brain_swap.rs:54:    SpawnReq {
crates\spt-daemon\tests\attach.rs:87:fn echo_spawn_req() -> SpawnReq {
crates\spt-daemon\tests\attach.rs:92:    SpawnReq {
crates\spt\tests\brain_survive.rs:85:fn echo_spawn_req() -> SpawnReq {
crates\spt\tests\brain_survive.rs:90:    SpawnReq {
crates\spt-daemon\tests\idempotent.rs:44:fn echo_spawn_req() -> SpawnReq {
crates\spt-daemon\tests\idempotent.rs:49:    SpawnReq {
crates\spt-daemon\tests\handoff.rs:39:fn echo_spawn_req() -> SpawnReq {
crates\spt-daemon\tests\handoff.rs:44:    SpawnReq {
crates\spt-daemon\tests\dispatch.rs:436:        .spawn_session(spt_daemon::msg::SpawnReq {
crates\spt-daemon\tests\dispatch.rs:445:        .spawn_session(spt_daemon::msg::SpawnReq {
crates\spt-daemon\src\applyhost.rs:564:    fn echo_spawn_req() -> SpawnReq {
crates\spt-daemon\src\applyhost.rs:569:        SpawnReq {
crates\spt-daemon\tests\twohost.rs:553:        .spawn_session(spt_daemon::msg::SpawnReq {
crates\spt-daemon\tests\resume.rs:72:fn producer_req(endpoint: &str) -> SpawnReq {
crates\spt-daemon\tests\resume.rs:96:    SpawnReq {
crates\spt-daemon\src\harnesshost.rs:117:        .spawn_session_pid(SpawnReq {
crates\spt-daemon\src\msg.rs:131:pub struct SpawnReq {
crates\spt-daemon\src\shellhost.rs:223:        .spawn_session_pid(SpawnReq {


Additive field breaks ~17 literals (mechanical `cwd: None`). Start with core: msg.rs field first.


Edit

    /// wire bytes). Empty = unlabeled (a legacy/test spawn); the dispatcher
    /// refuses to serve an unlabeled session, fail-closed.
    #[serde(default)]
    pub endpoint: String,
}

    /// wire bytes). Empty = unlabeled (a legacy/test spawn); the dispatcher
    /// refuses to serve an unlabeled session, fail-closed.
    #[serde(default)]
    pub endpoint: String,
    /// The working directory to spawn the PTY child in (REQ-HOST-RUN-2). `None`
    /// inherits the broker's cwd — the legacy/test behavior and the W1 bringup
    /// shortcut. `Some(dir)` lands the child in a PROJECT-scoped cwd: the
    /// consumer (Claude Code) reads its `.claude`, session history, and digest
    /// source relative to cwd, so a project-scoped launcher MUST place the
    /// harness in the user's project root, not the daemon's. Additive +
    /// serde-defaulted: an N-1 brain/broker omits it and gets the inherited cwd.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cwd: Option<String>,
}


proceed with W1.5 next steps. first verify the plan with doyle


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

    /// wire bytes). Empty = unlabeled (a legacy/test spawn); the dispatcher
    /// refuses to serve an unlabeled session, fail-closed.
    #[serde(default)]
    pub endpoint: String,
}

    /// wire bytes). Empty = unlabeled (a legacy/test spawn); the dispatcher
    /// refuses to serve an unlabeled session, fail-closed.
    #[serde(default)]
    pub endpoint: String,
    /// The working directory to spawn the PTY child in (REQ-HOST-RUN-2). `None`
    /// inherits the broker's cwd — the legacy/test behavior and the W1 bringup
    /// shortcut. `Some(dir)` lands the child in a PROJECT-scoped cwd: the
    /// consumer (Claude Code) reads its `.claude`, session history, and digest
    /// source relative to cwd, so a project-scoped launcher MUST place the
    /// harness in the user's project root, not the daemon's. Additive +
    /// serde-defaulted: an N-1 brain/broker omits it and gets the inherited cwd.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cwd: Option<String>,
}

+    /// The working directory to spawn the PTY child in (REQ-HOST-RUN-2). `None`

+    /// inherits the broker's cwd — the legacy/test behavior and the W1 bringup

+    /// shortcut. `Some(dir)` lands the child in a PROJECT-scoped cwd: the

+    /// consumer (Claude Code) reads its `.claude`, session history, and digest

+    /// source relative to cwd, so a project-scoped launcher MUST place the

+    /// harness in the user's project root, not the daemon's. Additive +

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

/// `spawned` payload — the broker-minted session id + the child's OS pid.


Now spt-term `spawn_program_in` with cwd. Read pty.rs spawn region:


55	    /// child holds the only remaining slave handle, which lets EOF propagate on
56	    /// the Unix raw-pipe backend once the child exits. (On Windows the no-EOF
57	    /// behaviour is governed by the writer, handled by the A2 drain discipline.)
58	    //
59	    // [impl->REQ-TERM-1]
60	    pub fn spawn(cmd: CommandBuilder, size: SurfaceSize) -> Result<Self, SurfaceError> {
61	        let pty_system = native_pty_system();
62	        let pair = pty_system
63	            .openpty(size.into())
64	            .map_err(|e| SurfaceError::OpenPty(e.to_string()))?;
65
66	        let child = pair
67	            .slave
68	            .spawn_command(cmd)
69	            .map_err(|e| SurfaceError::Spawn(e.to_string()))?;
70	        // Drop the slave so the child owns the only slave-side handle.
71	        drop(pair.slave);
72
73	        let reader = pair
74	            .master
75	            .try_clone_reader()
76	            .map_err(|e| SurfaceError::Io(io_other(e)))?;
77	        let writer = pair
78	            .master
79	            .take_writer()
80	            .map_err(|e| SurfaceError::Io(io_other(e)))?;
81	        let killer = child.clone_killer();
82	        let pid = child.process_id();
83
84	        Ok(Self {
85	            master: Mutex::new(pair.master),
86	            writer: Arc::new(Mutex::new(writer)),
87	            child: Mutex::new(child),
88	            killer: Mutex::new(killer),
89	            reader: Mutex::new(Some(reader)),
90	            pid,
91	        })
92	    }
93
94	    /// Convenience: spawn `program` with `args` under a PTY sized to `size`.

    /// child holds the only remaining slave handle, which lets EOF propagate on
    /// the Unix raw-pipe backend once the child exits. (On Windows the no-EOF
    /// behaviour is governed by the writer, handled by the A2 drain discipline.)
    //
    // [impl->REQ-TERM-1]
    pub fn spawn(cmd: CommandBuilder, size: SurfaceSize) -> Result<Self, SurfaceError> {
        let pty_system = native_pty_system();
        let pair = pty_system
            .openpty(size.into())
            .map_err(|e| SurfaceError::OpenPty(e.to_string()))?;

        let child = pair
            .slave
            .spawn_command(cmd)
            .map_err(|e| SurfaceError::Spawn(e.to_string()))?;
        // Drop the slave so the child owns the only slave-side handle.
        drop(pair.slave);

        let reader = pair
            .master
            .try_clone_reader()
            .map_err(|e| SurfaceError::Io(io_other(e)))?;
        let writer = pair
            .master
            .take_writer()
            .map_err(|e| SurfaceError::Io(io_other(e)))?;
        let killer = child.clone_killer();
        let pid = child.process_id();

        Ok(Self {
            master: Mutex::new(pair.master),
            writer: Arc::new(Mutex::new(writer)),
            child: Mutex::new(child),
            killer: Mutex::new(killer),
            reader: Mutex::new(Some(reader)),
            pid,
        })
    }

    /// Convenience: spawn `program` with `args` under a PTY sized to `size`.


    /// Convenience: spawn `program` with `args` under a PTY sized to `size`.
    pub fn spawn_program<P, A, S>(
        program: P,
        args: A,
        size: SurfaceSize,
    ) -> Result<Self, SurfaceError>
    where
        P: AsRef<std::ffi::OsStr>,
        A: IntoIterator<Item = S>,
        S: AsRef<std::ffi::OsStr>,
    {
        let mut cmd = CommandBuilder::new(program);
        for a in args {
            cmd.arg(a);
        }
        Self::spawn(cmd, size)
    }

    /// Convenience: spawn `program` with `args` under a PTY sized to `size`,
    /// inheriting the caller's working directory.
    pub fn spawn_program<P, A, S>(
        program: P,
        args: A,
        size: SurfaceSize,
    ) -> Result<Self, SurfaceError>
    where
        P: AsRef<std::ffi::OsStr>,
        A: IntoIterator<Item = S>,
        S: AsRef<std::ffi::OsStr>,
    {
        Self::spawn_program_in(program, args, size, None::<&std::ffi::OsStr>)
    }

    /// Spawn `program` with `args` under a PTY sized to `size`, in `cwd` when
    /// `Some` (REQ-HOST-RUN-2: a project-scoped working directory for the
    /// spawned child) — `None` inherits the caller's cwd, identical to
    /// [`Self::spawn_program`].
    // [impl->REQ-HOST-RUN-2]
    pub fn spawn_program_in<P, A, S, D>(
        program: P,
        args: A,
        size: SurfaceSize,
        cwd: Option<D>,
    ) -> Result<Self, SurfaceError>
    where
        P: AsRef<std::ffi::OsStr>,
        A: IntoIterator<Item = S>,
        S: AsRef<std::ffi::OsStr>,
        D: AsRef<std::ffi::OsStr>,
    {
        let mut cmd = CommandBuilder::new(program);
        for a in args {
            cmd.arg(a);
        }
        if let Some(dir) = cwd {
            cmd.cwd(dir);
        }
        Self::spawn(cmd, size)
    }


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

    /// Convenience: spawn `program` with `args` under a PTY sized to `size`,
    /// inheriting the caller's working directory.
    pub fn spawn_program<P, A, S>(
        program: P,
        args: A,
        size: SurfaceSize,
    ) -> Result<Self, SurfaceError>
    where
        P: AsRef<std::ffi::OsStr>,
        A: IntoIterator<Item = S>,
        S: AsRef<std::ffi::OsStr>,
    {
        Self::spawn_program_in(program, args, size, None::<&std::ffi::OsStr>)
    }

    /// Spawn `program` with `args` under a PTY sized to `size`, in `cwd` when
    /// `Some` (REQ-HOST-RUN-2: a project-scoped working directory for the
    /// spawned child) — `None` inherits the caller's cwd, identical to
    /// [`Self::spawn_program`].
    // [impl->REQ-HOST-RUN-2]
    pub fn spawn_program_in<P, A, S, D>(
        program: P,
        args: A,
        size: SurfaceSize,
        cwd: Option<D>,
    ) -> Result<Self, SurfaceError>
    where
        P: AsRef<std::ffi::OsStr>,
        A: IntoIterator<Item = S>,
        S: AsRef<std::ffi::OsStr>,
        D: AsRef<std::ffi::OsStr>,
    {
        let mut cmd = CommandBuilder::new(program);
        for a in args {
            cmd.arg(a);
        }
        if let Some(dir) = cwd {
            cmd.cwd(dir);
        }
        Self::spawn(cmd, size)
    }

//! Native-PTY session backend — ConPTY on Windows 10+, `forkpty(3)` on Unix,
//! both via `portable-pty` (the exact crate + handoff shape Spikes #1 / #4
//! proved).
//!
//! [`PtySession`] spawns a child under a real PTY, owns the master reader and
//! writer, and serves the [`SessionSurface`] contract. It is **single-surface
//! per handle** — one child's PTY, not the multiplexed fleet (that supervisor is
//! the M3b broker).
//!
//! ## Concurrency discipline (Spike #5, binding)
//!
//! `resize()` races the reader thread on Windows; the master is therefore held
//! behind a `Mutex` and every `resize` takes that lock. The writer is a shared
//! `Arc<Mutex<…>>` so the A2 DSR drain thread can answer ConPTY's startup
//! cursor query through the same serialized writer the caller injects input
//! through — no torn writes.

use std::io::{Read, Write};
use std::sync::{Arc, Mutex};

use portable_pty::{native_pty_system, Child, ChildKiller, CommandBuilder, ExitStatus, MasterPty};

use crate::surface::{SessionSurface, SurfaceError, SurfaceSize};

/// Shared, serialized handle to the PTY master writer. Cloned to the A2 drain
/// thread so the DSR auto-answer and caller input never interleave mid-write.
pub(crate) type SharedWriter = Arc<Mutex<Box<dyn Write + Send>>>;

/// A child process running under a native PTY, exposed as a [`SessionSurface`].
///
/// Build one with [`PtySession::spawn`]; drive input via [`SessionSurface::write_input`]
/// (or the A3 `send_keys`/`send_line` helpers), drain output via
/// [`PtySession::take_reader`], and observe lifecycle via [`PtySession::try_wait`] /
/// [`PtySession::wait`] / [`PtySession::kill`].
pub struct PtySession {
    /// The master end, behind a lock so `resize` never races the reader (Spike #5).
    master: Mutex<Box<dyn MasterPty + Send>>,
    /// The single PTY writer, shared so the A2 drain thread can answer DSR.
    writer: SharedWriter,
    /// The spawned child; behind a lock for `wait`/`try_wait`.
    child: Mutex<Box<dyn Child + Send + Sync>>,
    /// A kill handle split from the child so we can terminate from another
    /// thread even while `wait` blocks (`portable-pty` `ChildKiller`).
    killer: Mutex<Box<dyn ChildKiller + Send + Sync>>,
    /// The master reader, taken at most once (`take_reader`).
    reader: Mutex<Option<Box<dyn Read + Send>>>,
    /// The child OS pid, captured at spawn.
    pid: Option<u32>,
}

impl PtySession {
    /// Spawn `cmd` under a fresh native PTY sized to `size`.
    ///
    /// The slave end is dropped immediately after the child is spawned: the
    /// child holds the only remaining slave handle, which lets EOF propagate on
    /// the Unix raw-pipe backend once the child exits. (On Windows the no-EOF
    /// behaviour is governed by the writer, handled by the A2 drain discipline.)
    //
    // [impl->REQ-TERM-1]
    pub fn spawn(cmd: CommandBuilder, size: SurfaceSize) -> Result<Self, SurfaceError> {
        let pty_system = native_pty_system();
        let pair = pty_system
            .openpty(size.into())
            .map_err(|e| SurfaceError::OpenPty(e.to_string()))?;

        let child = pair
            .slave
            .spawn_command(cmd)
            .map_err(|e| SurfaceError::Spawn(e.to_string()))?;
        // Drop the slave so the child owns the only slave-side handle.
        drop(pair.slave);

        let reader = pair
            .master
            .try_clone_reader()
            .map_err(|e| SurfaceError::Io(io_other(e)))?;
        let writer = pair
            .master
            .take_writer()
            .map_err(|e| SurfaceError::Io(io_other(e)))?;
        let killer = child.clone_killer();
        let pid = child.process_id();

        Ok(Self {
            master: Mutex::new(pair.master),
            writer: Arc::new(Mutex::new(writer)),
            child: Mutex::new(child),
            killer: Mutex::new(killer),
            reader: Mutex::new(Some(reader)),
            pid,
        })
    }

    /// Convenience: spawn `program` with `args` under a PTY sized to `size`.
    pub fn spawn_program<P, A, S>(
        program: P,
        args: A,
        size: SurfaceSize,
    ) -> Result<Self, SurfaceError>
    where
        P: AsRef<std::ffi::OsStr>,
        A: IntoIterator<Item = S>,
        S: AsRef<std::ffi::OsStr>,
    {
        let mut cmd = CommandBuilder::new(program);
        for a in args {
            cmd.arg(a);
        }
        Self::spawn(cmd, size)
    }

    /// Take the master reader. Returns [`SurfaceError::ReaderTaken`] on the
    /// second call — output is drained by exactly one consumer (the A2 drain
    /// thread in the hosted path; a test consumer otherwise).
    pub fn take_reader(&self) -> Result<Box<dyn Read + Send>, SurfaceError> {
        self.reader
            .lock()
            .unwrap()
            .take()
            .ok_or(SurfaceError::ReaderTaken)
    }

    /// The shared writer handle — cloned to the A2 DSR drain thread so its
    /// auto-answer goes through the same serialized writer the caller injects
    /// input through (no torn writes).
    pub(crate) fn shared_writer(&self) -> SharedWriter {
        Arc::clone(&self.writer)
    }

    /// Poll for child exit without blocking.
    pub fn try_wait(&self) -> Result<Option<ExitStatus>, SurfaceError> {
        self.child
            .lock()
            .unwrap()
            .try_wait()
            .map_err(SurfaceError::Io)
    }

    /// Block until the child exits, yielding its status.
    pub fn wait(&self) -> Result<ExitStatus, SurfaceError> {
        self.child.lock().unwrap().wait().map_err(SurfaceError::Io)
    }

    /// Terminate the child. Safe to call from another thread while `wait`
    /// blocks (uses the split `ChildKiller`). Confirm the reap via
    /// [`PtySession::wait`] / [`PtySession::try_wait`].
    pub fn kill(&self) -> Result<(), SurfaceError> {
        let res = self.killer.lock().unwrap().kill();
        #[cfg(windows)]
        {
            // portable-pty 0.8's Windows `ChildKiller` returns the spurious
            // `last_os_error` even when `TerminateProcess` *succeeds* (the
            // upstream success check is inverted; `WinChild::kill` swallows it,
            // the cloned killer does not). Its Result is therefore untrustworthy
            // — kill is best-effort; confirm death by reaping.
            let _ = res;
            Ok(())
        }
        #[cfg(unix)]
        {
            res.map_err(SurfaceError::Io)
        }
    }

    /// The child's OS process id, if the backend exposes one.
    pub fn process_id(&self) -> Option<u32> {
        self.pid
    }
}

// [impl->REQ-TERM-1]
impl SessionSurface for PtySession {
    fn write_input(&self, bytes: &[u8]) -> Result<(), SurfaceError> {
        let mut w = self.writer.lock().unwrap();
        w.write_all(bytes).map_err(SurfaceError::Io)?;
        w.flush().map_err(SurfaceError::Io)
    }

    fn resize(&self, size: SurfaceSize) -> Result<(), SurfaceError> {
        // Hold the master lock so resize never races a concurrent reader/resize
        // (Spike #5: resize races the reader on Windows).
        self.master
            .lock()
            .unwrap()
            .resize(size.into())
            .map_err(|e| SurfaceError::Io(io_other(e)))
    }
}

/// Fold a `portable-pty` error (a private `anyhow::Error` alias) into a
/// `std::io::Error` so the crate's public surface never leaks `anyhow`. Generic
/// over `Display` to avoid naming the un-exported error type.
fn io_other<E: std::fmt::Display>(e: E) -> std::io::Error {
    std::io::Error::other(e.to_string())
}

-    /// Convenience: spawn `program` with `args` under a PTY sized to `size`.

+    /// Convenience: spawn `program` with `args` under a PTY sized to `size`,

+    /// inheriting the caller's working directory.

+        Self::spawn_program_in(program, args, size, None::<&std::ffi::OsStr>)

+    /// Spawn `program` with `args` under a PTY sized to `size`, in `cwd` when

+    /// `Some` (REQ-HOST-RUN-2: a project-scoped working directory for the

+    /// spawned child) — `None` inherits the caller's cwd, identical to

+    pub fn spawn_program_in<P, A, S, D>(


Now broker dispatch_spawn → honor `req.cwd`:


        let session = PtySession::spawn_program(
            &req.program,
            &req.args,
            SurfaceSize::new(req.rows, req.cols),
        )
        .map_err(|e| format!("spawn failed: {e}"))?;

        // REQ-HOST-RUN-2: spawn in the requested project cwd when carried;
        // `None` inherits the broker's cwd (legacy/test + W1 bringup shortcut).
        let session = PtySession::spawn_program_in(
            &req.program,
            &req.args,
            SurfaceSize::new(req.rows, req.cols),
            req.cwd.as_deref(),
        )
        .map_err(|e| format!("spawn failed: {e}"))?;


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

        let session = PtySession::spawn_program(
            &req.program,
            &req.args,
            SurfaceSize::new(req.rows, req.cols),
        )
        .map_err(|e| format!("spawn failed: {e}"))?;

        // REQ-HOST-RUN-2: spawn in the requested project cwd when carried;
        // `None` inherits the broker's cwd (legacy/test + W1 bringup shortcut).
        let session = PtySession::spawn_program_in(
            &req.program,
            &req.args,
            SurfaceSize::new(req.rows, req.cols),
            req.cwd.as_deref(),
        )
        .map_err(|e| format!("spawn failed: {e}"))?;

fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {

serde_json::from_value(env.payload).map_err(|e| format!("bad spawn payload: {e}"))?;

-        let session = PtySession::spawn_program(

+        // REQ-HOST-RUN-2: spawn in the requested project cwd when carried;

+        // `None` inherits the broker's cwd (legacy/test + W1 bringup shortcut).

+        let session = PtySession::spawn_program_in(


Now harnesshost — thread cwd through. Read the spawn site + signature:


60	/// [`launch_harness_brokered_in`] is the only side effect. Fails closed: a
61	/// non-harness adapter, a missing `[session.self]`, an unresolved `{placeholder}`,
62	/// or an empty command all error before anything is spawned.
63	// [impl->REQ-HOST-RUN-1]
64	pub fn prepare_harness_spawn(
65	    endpoint_id: &str,
66	    adapter_name: &str,
67	    session_id: &str,
68	    manifest: &Manifest,
69	) -> Result<Vec<String>, String> {
70	    if manifest.adapter.kind != AdapterKind::Harness {
71	        return Err(format!(
72	            "adapter '{adapter_name}' is kind={:?}, not a harness — \
73	             `spt endpoint run` brings up harness endpoints",
74	            manifest.adapter.kind
75	        ));
76	    }
77	    let role = manifest.session.self_.as_ref().ok_or_else(|| {
78	        format!("adapter '{adapter_name}' declares no [session.self] — nothing to bring up")
79	    })?;
80	    // The guaranteed-fill catalog for a self-spawn. `id` rides argv so the harness
81	    // binds to exactly this endpoint; `session_name` defaults to the id.
82	    let keys = BTreeMap::from([
83	        ("id".to_string(), endpoint_id.to_string()),
84	        ("adapter_name".to_string(), adapter_name.to_string()),
85	        ("session_id".to_string(), session_id.to_string()),
86	        ("session_name".to_string(), endpoint_id.to_string()),
87	    ]);
88	    let filled =
89	        spt_runtime::runtime::fill_template(&role.command, &keys).map_err(|e| e.to_string())?;
90	    let tokens = spt_runtime::runtime::tokenize(&filled);
91	    if tokens.is_empty() {
92	        return Err("empty [session.self] command".into());
93	    }
94	    Ok(tokens)
95	}
96
97	/// Spawn an adapter's `[session.self]` harness into a broker-held PTY (the
98	/// spt-hosted bringup). The broker session is labeled `endpoint_id` so
99	/// `spt rc <id>` resolves it; the harness self-registers its perch on bind. The
100	/// broker outlives this call — the session keeps running after it returns.
101	///
102	/// `session_id`: the spawn-time `{session_id}` fill — a provisional [`mint_session_id`]
103	/// for a fresh create, or the prior session for `--resume`.
104	// [impl->REQ-HOST-RUN-1]
105	pub fn launch_harness_brokered_in(
106	    broker_name: &str,
107	    endpoint_id: &str,
108	    adapter_name: &str,
109	    session_id: &str,
110	    manifest: &Manifest,
111	) -> Result<HarnessSpawned, String> {
112	    let tokens = prepare_harness_spawn(endpoint_id, adapter_name, session_id, manifest)?;
113	    let (program, args) = tokens.split_first().expect("non-empty checked");
114	    let mut brain =
115	        Brain::cold_start(broker_name, now_ms()).map_err(|e| format!("broker connect: {e}"))?;
116	    let (session_id, pid) = brain
117	        .spawn_session_pid(SpawnReq {
118	            program: program.clone(),
119	            args: args.to_vec(),
120	            rows: DEFAULT_HARNESS_ROWS,
121	            cols: DEFAULT_HARNESS_COLS,
122	            endpoint: endpoint_id.to_string(),
123	        })
124	        .map_err(|e| format!("broker spawn: {e}"))?;
125	    Ok(HarnessSpawned { session_id, pid })
126	}
127
128	#[cfg(test)]
129	mod tests {
130	    use super::*;
131
132	    fn harness_manifest(self_command: &str) -> Manifest {
133	        let toml_src = format!(
134	            "[adapter]\nname = \"mock\"\nkind = \"harness\"\nversion = \"1\"\n\

/// [`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<Vec<String>, 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")
    })?;
    // The guaranteed-fill catalog for a self-spawn. `id` rides argv so the harness
    // binds to exactly this endpoint; `session_name` defaults to the id.
    let keys = BTreeMap::from([
        ("id".to_string(), endpoint_id.to_string()),
        ("adapter_name".to_string(), adapter_name.to_string()),
        ("session_id".to_string(), session_id.to_string()),
        ("session_name".to_string(), endpoint_id.to_string()),
    ]);
    let filled =
        spt_runtime::runtime::fill_template(&role.command, &keys).map_err(|e| e.to_string())?;
    let tokens = spt_runtime::runtime::tokenize(&filled);
    if tokens.is_empty() {
        return Err("empty [session.self] command".into());
    }
    Ok(tokens)
}

/// Spawn an adapter's `[session.self]` harness into a broker-held PTY (the
/// spt-hosted bringup). The broker session is labeled `endpoint_id` so
/// `spt rc <id>` resolves it; the harness self-registers its perch on bind. The
/// broker outlives this call — the session keeps running after it returns.
///
/// `session_id`: the spawn-time `{session_id}` fill — a provisional [`mint_session_id`]
/// for a fresh create, or the prior session for `--resume`.
// [impl->REQ-HOST-RUN-1]
pub fn launch_harness_brokered_in(
    broker_name: &str,
    endpoint_id: &str,
    adapter_name: &str,
    session_id: &str,
    manifest: &Manifest,
) -> Result<HarnessSpawned, String> {
    let tokens = prepare_harness_spawn(endpoint_id, adapter_name, session_id, manifest)?;
    let (program, args) = tokens.split_first().expect("non-empty checked");
    let mut brain =
        Brain::cold_start(broker_name, now_ms()).map_err(|e| format!("broker connect: {e}"))?;
    let (session_id, pid) = brain
        .spawn_session_pid(SpawnReq {
            program: program.clone(),
            args: args.to_vec(),
            rows: DEFAULT_HARNESS_ROWS,
            cols: DEFAULT_HARNESS_COLS,
            endpoint: endpoint_id.to_string(),
        })
        .map_err(|e| format!("broker spawn: {e}"))?;
    Ok(HarnessSpawned { session_id, pid })
}

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

    fn harness_manifest(self_command: &str) -> Manifest {
        let toml_src = format!(
            "[adapter]\nname = \"mock\"\nkind = \"harness\"\nversion = \"1\"\n\


/// `session_id`: the spawn-time `{session_id}` fill — a provisional [`mint_session_id`]
/// for a fresh create, or the prior session for `--resume`.
// [impl->REQ-HOST-RUN-1]
pub fn launch_harness_brokered_in(
    broker_name: &str,
    endpoint_id: &str,
    adapter_name: &str,
    session_id: &str,
    manifest: &Manifest,
) -> Result<HarnessSpawned, String> {
    let tokens = prepare_harness_spawn(endpoint_id, adapter_name, session_id, manifest)?;
    let (program, args) = tokens.split_first().expect("non-empty checked");
    let mut brain =
        Brain::cold_start(broker_name, now_ms()).map_err(|e| format!("broker connect: {e}"))?;
    let (session_id, pid) = brain
        .spawn_session_pid(SpawnReq {
            program: program.clone(),
            args: args.to_vec(),
            rows: DEFAULT_HARNESS_ROWS,
            cols: DEFAULT_HARNESS_COLS,
            endpoint: endpoint_id.to_string(),
        })
        .map_err(|e| format!("broker spawn: {e}"))?;
    Ok(HarnessSpawned { session_id, pid })
}

/// `session_id`: the spawn-time `{session_id}` fill — a provisional [`mint_session_id`]
/// for a fresh create, or the prior session for `--resume`.
///
/// `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 tokens = prepare_harness_spawn(endpoint_id, adapter_name, session_id, manifest)?;
    let (program, args) = tokens.split_first().expect("non-empty checked");
    let mut brain =
        Brain::cold_start(broker_name, now_ms()).map_err(|e| format!("broker connect: {e}"))?;
    let (session_id, pid) = brain
        .spawn_session_pid(SpawnReq {
            program: program.clone(),
            args: args.to_vec(),
            rows: DEFAULT_HARNESS_ROWS,
            cols: DEFAULT_HARNESS_COLS,
            endpoint: endpoint_id.to_string(),
            cwd: cwd.map(str::to_string),
        })
        .map_err(|e| format!("broker spawn: {e}"))?;
    Ok(HarnessSpawned { session_id, pid })
}


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)

/// `session_id`: the spawn-time `{session_id}` fill — a provisional [`mint_session_id`]
/// for a fresh create, or the prior session for `--resume`.
///
/// `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 tokens = prepare_harness_spawn(endpoint_id, adapter_name, session_id, manifest)?;
    let (program, args) = tokens.split_first().expect("non-empty checked");
    let mut brain =
        Brain::cold_start(broker_name, now_ms()).map_err(|e| format!("broker connect: {e}"))?;
    let (session_id, pid) = brain
        .spawn_session_pid(SpawnReq {
            program: program.clone(),
            args: args.to_vec(),
            rows: DEFAULT_HARNESS_ROWS,
            cols: DEFAULT_HARNESS_COLS,
            endpoint: endpoint_id.to_string(),
            cwd: cwd.map(str::to_string),
        })
        .map_err(|e| format!("broker spawn: {e}"))?;
    Ok(HarnessSpawned { session_id, pid })
}

//! Harness-instance hosting (M12-W1 — spt-hosted harness bringup, REQ-HOST-RUN-1):
//! the spt-HOSTED counterpart to harness-hosted launch.
//!
//! **Who spawns the harness.** Today a harness is *harness-hosted*: an external
//! launcher runs the adapter's `[session.self]` command and the harness binds its
//! own perch on startup (`api listen`/`api bind` → `establish_perch`). spt-hosting
//! changes *only who owns the PTY*: the daemon spawns `[session.self]` into a
//! **broker-held PTY** ([`launch_harness_brokered_in`]). The harness still
//! self-registers its perch on bind — there is no daemon pre-register step.
//!
//! **Why broker-held.** The broker owns the PTY, so the harness survives a
//! `spt rc` detach — PTY ownership stays with the broker, no termination on
//! detach (the KNOWN-HAZARDS attach-lifecycle invariant, satisfied by
//! construction). The broker session is labeled with the endpoint id so
//! `spt rc <id>` resolves exactly this PTY.
//!
//! **Id plumbing.** The endpoint id rides argv via `{id}` substitution into the
//! opaque `[session.self]` command (the harness parses `--id` and binds to it);
//! the broker-spawned child inherits the broker's environment. No `SpawnReq` env
//! field is involved (the `[env]` manifest doc: "spt-hosted inherits from the
//! broker"). This mirrors [`crate::shellhost::launch_shell_brokered_in`] but feeds
//! `[session.self]` (not `[shell].spawn`), uses real terminal PTY dims, and mints
//! no link token (a harness binds via `api`, not the shell link handshake).
// [impl->REQ-HOST-RUN-1]

use std::collections::BTreeMap;

use spt_runtime::manifest::{AdapterKind, Manifest};

use crate::brain::{now_ms, Brain};
use crate::msg::SpawnReq;

/// Default PTY geometry for a freshly brought-up harness, before an attach
/// resizes it to the operator's real terminal. A harness *is* a terminal (unlike
/// a shell's wide anti-wrap stdin conduit), so a conventional 80×24 is right.
pub const DEFAULT_HARNESS_ROWS: u16 = 24;
pub const DEFAULT_HARNESS_COLS: u16 = 80;

/// Outcome of an spt-hosted harness bringup: the broker session id (the attach
/// handle) plus the OS pid of the spawned harness (`None` if the broker backend
/// exposed none).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HarnessSpawned {
    pub session_id: u64,
    pub pid: Option<u32>,
}

/// Mint a provisional spawn-time session id (16 hex chars). The *real* session id
/// is reported post-spawn by the harness (`identity.session_id_source =
/// "post_spawn"`); this only fills the `{session_id}` placeholder at spawn so the
/// command template resolves. A fresh create mints one; `--resume` supplies the
/// prior session id instead.
pub fn mint_session_id() -> String {
    let seed = spt_proto::identity::Identity::generate().seed();
    seed[..8].iter().map(|b| format!("{b:02x}")).collect()
}

/// 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<Vec<String>, 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")
    })?;
    // The guaranteed-fill catalog for a self-spawn. `id` rides argv so the harness
    // binds to exactly this endpoint; `session_name` defaults to the id.
    let keys = BTreeMap::from([
        ("id".to_string(), endpoint_id.to_string()),
        ("adapter_name".to_string(), adapter_name.to_string()),
        ("session_id".to_string(), session_id.to_string()),
        ("session_name".to_string(), endpoint_id.to_string()),
    ]);
    let filled =
        spt_runtime::runtime::fill_template(&role.command, &keys).map_err(|e| e.to_string())?;
    let tokens = spt_runtime::runtime::tokenize(&filled);
    if tokens.is_empty() {
        return Err("empty [session.self] command".into());
    }
    Ok(tokens)
}

/// Spawn an adapter's `[session.self]` harness into a broker-held PTY (the
/// spt-hosted bringup). The broker session is labeled `endpoint_id` so
/// `spt rc <id>` resolves it; the harness self-registers its perch on bind. The
/// broker outlives this call — the session keeps running after it returns.
///
/// `session_id`: the spawn-time `{session_id}` fill — a provisional [`mint_session_id`]
/// for a fresh create, or the prior session for `--resume`.
// [impl->REQ-HOST-RUN-1]
pub fn launch_harness_brokered_in(
    broker_name: &str,
    endpoint_id: &str,
    adapter_name: &str,
    session_id: &str,
    manifest: &Manifest,
) -> Result<HarnessSpawned, String> {
    let tokens = prepare_harness_spawn(endpoint_id, adapter_name, session_id, manifest)?;
    let (program, args) = tokens.split_first().expect("non-empty checked");
    let mut brain =
        Brain::cold_start(broker_name, now_ms()).map_err(|e| format!("broker connect: {e}"))?;
    let (session_id, pid) = brain
        .spawn_session_pid(SpawnReq {
            program: program.clone(),
            args: args.to_vec(),
            rows: DEFAULT_HARNESS_ROWS,
            cols: DEFAULT_HARNESS_COLS,
            endpoint: endpoint_id.to_string(),
        })
        .map_err(|e| format!("broker spawn: {e}"))?;
    Ok(HarnessSpawned { session_id, pid })
}

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

    fn harness_manifest(self_command: &str) -> Manifest {
        let toml_src = format!(
            "[adapter]\nname = \"mock\"\nkind = \"harness\"\nversion = \"1\"\n\
             min_spt_core_version = \"0\"\n\n[session.self]\ncommand = '{self_command}'\n\
             keys = [\"id\", \"session_id\"]\n"
        );
        spt_runtime::Manifest::from_toml_str(&toml_src).unwrap()
    }

    // [unit->REQ-HOST-RUN-1] the spt-hosted bringup is template-driven: the
    // endpoint id and the spawn-time session id ride argv via {id}/{session_id}
    // substitution into [session.self] (so the harness binds to exactly this
    // endpoint), tokenized program-first.
    #[test]
    fn prepare_fills_id_and_session_into_self_command() {
        let m = harness_manifest("mock-session --id {id} --session-id {session_id}");
        let tokens = prepare_harness_spawn("doyle", "mock", "sess-abc", &m).unwrap();
        assert_eq!(
            tokens,
            vec![
                "mock-session".to_string(),
                "--id".to_string(),
                "doyle".to_string(),
                "--session-id".to_string(),
                "sess-abc".to_string(),
            ]
        );
    }

    // [unit->REQ-HOST-RUN-1] fails closed before any spawn: a shell-kind adapter
    // is refused (endpoint run hosts harnesses), a manifest with no [session.self]
    // is refused (nothing to bring up), and an unresolved {placeholder} errs
    // naming the missing key.
    #[test]
    fn prepare_fails_closed_on_wrong_kind_missing_role_and_bad_template() {
        // Shell-kind adapter refused.
        let shell = spt_runtime::Manifest::from_toml_str(
            "[adapter]\nname = \"sh\"\nkind = \"shell\"\nversion = \"1\"\n\
             min_spt_core_version = \"0\"\n\n[shell]\nspawn = 'sh'\n",
        )
        .unwrap();
        assert!(prepare_harness_spawn("e", "sh", "s", &shell)
            .unwrap_err()
            .contains("not a harness"));

        // Harness with no [session.self] refused.
        let no_self = spt_runtime::Manifest::from_toml_str(
            "[adapter]\nname = \"h\"\nkind = \"harness\"\nversion = \"1\"\n\
             min_spt_core_version = \"0\"\n",
        )
        .unwrap();
        assert!(prepare_harness_spawn("e", "h", "s", &no_self)
            .unwrap_err()
            .contains("no [session.self]"));

        // Unknown {placeholder} errs naming the key.
        let bad = harness_manifest("mock-session --id {id} --boom {not_a_key}");
        assert!(prepare_harness_spawn("e", "mock", "s", &bad)
            .unwrap_err()
            .contains("not_a_key"));
    }

    // [unit->REQ-HOST-RUN-1] a provisional session id is fresh, hex, and unique
    // per mint (it only fills the spawn-time {session_id}; the harness reports the
    // real one post-spawn).
    #[test]
    fn mint_session_id_is_fresh_hex() {
        let a = mint_session_id();
        let b = mint_session_id();
        assert_eq!(a.len(), 16);
        assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
        assert_ne!(a, b, "per-mint distinct");
    }
}

+/// `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.

+            cwd: cwd.map(str::to_string),


Now cli cmd_endpoint_run — derive project cwd + pass it. Read it:


1059:fn cmd_endpoint_run(
1104:    match spt_daemon::harnesshost::launch_harness_brokered_in(


1059	fn cmd_endpoint_run(
1060	    adapter: &str,
1061	    id: &str,
1062	    resume: Option<String>,
1063	    start: bool,
1064	    // `--attach` is the default terminal action; the bool is accepted for flag
1065	    // symmetry but `start`/`view` are what branch (attach = neither).
1066	    _attach: bool,
1067	    view: bool,
1068	) -> i32 {
1069	    // id charset (the harness binds to exactly this id via argv).
1070	    // [impl->REQ-HAZARD-ID-CHARSET]
1071	    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
1072	        eprintln!("INVALID_ID:{id}: {e}");
1073	        return 2;
1074	    }
1075	    // Resolve the harness adapter option (`<adapter>[:profile]`) through the
1076	    // merged view (split → parent lookup → overlay), exactly like shell spawn.
1077	    let adapters_dir = spt_store::perch::adapters_dir();
1078	    let manifest = match spt_runtime::registry::resolve_option(&adapters_dir, adapter) {
1079	        Ok((_, m)) if m.adapter.kind == spt_runtime::manifest::AdapterKind::Harness => m,
1080	        Ok(_) => {
1081	            eprintln!(
1082	                "ENDPOINT_RUN_NOT_HARNESS:{adapter}: not a kind=\"harness\" adapter — \
1083	                 `endpoint run` brings up harness endpoints (shells use `shell spawn`)"
1084	            );
1085	            return 1;
1086	        }
1087	        Err(e) => {
1088	            eprintln!(
1089	                "ENDPOINT_RUN_ADAPTER_UNREGISTERED:{adapter}: not an active registered \
1090	                 harness adapter / valid profile on this node ({e}; spt adapter list)"
1091	            );
1092	            return 1;
1093	        }
1094	    };
1095
1096	    // A fresh bringup mints a provisional spawn-time session id; --resume carries
1097	    // a prior one (the harness reports the real id post-spawn either way).
1098	    let session_id = resume.unwrap_or_else(spt_daemon::harnesshost::mint_session_id);
1099
1100	    if let Err(e) = spt_daemon::daemon::ensure_running() {
1101	        eprintln!("ENDPOINT_RUN_DAEMON_FAIL:{id}: {e}");
1102	        return 1;
1103	    }
1104	    match spt_daemon::harnesshost::launch_harness_brokered_in(
1105	        &spt_daemon::endpoint::broker_socket_name(),
1106	        id,
1107	        adapter,
1108	        &session_id,
1109	        &manifest,
1110	    ) {
1111	        Ok(spawned) => {
1112	            eprintln!(
1113	                "ENDPOINT_RUN:{id} adapter={adapter} pid={:?} session={session_id} \
1114	                 (harness binds its perch on startup)",
1115	                spawned.pid
1116	            );
1117	        }
1118	        Err(e) => {
1119	            eprintln!("ENDPOINT_RUN_FAIL:{id}: {e}");
1120	            return 1;
1121	        }
1122	    }
1123
1124	    // Terminal action: start (return now) | view (read-only attach) | attach
1125	    // (default). Attach/view route through the SAME rc pump as `spt rc`.
1126	    if start {
1127	        eprintln!("ENDPOINT_RUN_STARTED:{id} (attach with `spt rc {id}`)");
1128	        return 0;

fn cmd_endpoint_run(
    adapter: &str,
    id: &str,
    resume: Option<String>,
    start: bool,
    // `--attach` is the default terminal action; the bool is accepted for flag
    // symmetry but `start`/`view` are what branch (attach = neither).
    _attach: bool,
    view: bool,
) -> i32 {
    // id charset (the harness binds to exactly this id via argv).
    // [impl->REQ-HAZARD-ID-CHARSET]
    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
        eprintln!("INVALID_ID:{id}: {e}");
        return 2;
    }
    // Resolve the harness adapter option (`<adapter>[:profile]`) through the
    // merged view (split → parent lookup → overlay), exactly like shell spawn.
    let adapters_dir = spt_store::perch::adapters_dir();
    let manifest = match spt_runtime::registry::resolve_option(&adapters_dir, adapter) {
        Ok((_, m)) if m.adapter.kind == spt_runtime::manifest::AdapterKind::Harness => m,
        Ok(_) => {
            eprintln!(
                "ENDPOINT_RUN_NOT_HARNESS:{adapter}: not a kind=\"harness\" adapter — \
                 `endpoint run` brings up harness endpoints (shells use `shell spawn`)"
            );
            return 1;
        }
        Err(e) => {
            eprintln!(
                "ENDPOINT_RUN_ADAPTER_UNREGISTERED:{adapter}: not an active registered \
                 harness adapter / valid profile on this node ({e}; spt adapter list)"
            );
            return 1;
        }
    };

    // A fresh bringup mints a provisional spawn-time session id; --resume carries
    // a prior one (the harness reports the real id post-spawn either way).
    let session_id = resume.unwrap_or_else(spt_daemon::harnesshost::mint_session_id);

    if let Err(e) = spt_daemon::daemon::ensure_running() {
        eprintln!("ENDPOINT_RUN_DAEMON_FAIL:{id}: {e}");
        return 1;
    }
    match spt_daemon::harnesshost::launch_harness_brokered_in(
        &spt_daemon::endpoint::broker_socket_name(),
        id,
        adapter,
        &session_id,
        &manifest,
    ) {
        Ok(spawned) => {
            eprintln!(
                "ENDPOINT_RUN:{id} adapter={adapter} pid={:?} session={session_id} \
                 (harness binds its perch on startup)",
                spawned.pid
            );
        }
        Err(e) => {
            eprintln!("ENDPOINT_RUN_FAIL:{id}: {e}");
            return 1;
        }
    }

    // Terminal action: start (return now) | view (read-only attach) | attach
    // (default). Attach/view route through the SAME rc pump as `spt rc`.
    if start {
        eprintln!("ENDPOINT_RUN_STARTED:{id} (attach with `spt rc {id}`)");
        return 0;


    if let Err(e) = spt_daemon::daemon::ensure_running() {
        eprintln!("ENDPOINT_RUN_DAEMON_FAIL:{id}: {e}");
        return 1;
    }
    match spt_daemon::harnesshost::launch_harness_brokered_in(
        &spt_daemon::endpoint::broker_socket_name(),
        id,
        adapter,
        &session_id,
        &manifest,
    ) {

    if let Err(e) = spt_daemon::daemon::ensure_running() {
        eprintln!("ENDPOINT_RUN_DAEMON_FAIL:{id}: {e}");
        return 1;
    }
    // REQ-HOST-RUN-2: land the harness PTY in the PROJECT cwd — where the user
    // invoked `spt endpoint run` (`cc <id>` at a project root) — NOT the
    // daemon's cwd. A project-scoped harness (Claude Code) reads its `.claude`,
    // session history, and digest source relative to cwd. A failure to read the
    // cwd falls back to the broker-inherited cwd (None) rather than aborting.
    let project_cwd = std::env::current_dir()
        .ok()
        .map(|p| p.to_string_lossy().into_owned());
    match spt_daemon::harnesshost::launch_harness_brokered_in(
        &spt_daemon::endpoint::broker_socket_name(),
        id,
        adapter,
        &session_id,
        &manifest,
        project_cwd.as_deref(),
    ) {


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

    if let Err(e) = spt_daemon::daemon::ensure_running() {
        eprintln!("ENDPOINT_RUN_DAEMON_FAIL:{id}: {e}");
        return 1;
    }
    match spt_daemon::harnesshost::launch_harness_brokered_in(
        &spt_daemon::endpoint::broker_socket_name(),
        id,
        adapter,
        &session_id,
        &manifest,
    ) {

    if let Err(e) = spt_daemon::daemon::ensure_running() {
        eprintln!("ENDPOINT_RUN_DAEMON_FAIL:{id}: {e}");
        return 1;
    }
    // REQ-HOST-RUN-2: land the harness PTY in the PROJECT cwd — where the user
    // invoked `spt endpoint run` (`cc <id>` at a project root) — NOT the
    // daemon's cwd. A project-scoped harness (Claude Code) reads its `.claude`,
    // session history, and digest source relative to cwd. A failure to read the
    // cwd falls back to the broker-inherited cwd (None) rather than aborting.
    let project_cwd = std::env::current_dir()
        .ok()
        .map(|p| p.to_string_lossy().into_owned());
    match spt_daemon::harnesshost::launch_harness_brokered_in(
        &spt_daemon::endpoint::broker_socket_name(),
        id,
        adapter,
        &session_id,
        &manifest,
        project_cwd.as_deref(),
    ) {

+    // REQ-HOST-RUN-2: land the harness PTY in the PROJECT cwd — where the user

+    // invoked `spt endpoint run` (`cc <id>` at a project root) — NOT the

+    // daemon's cwd. A project-scoped harness (Claude Code) reads its `.claude`,

+    // session history, and digest source relative to cwd. A failure to read the

+    // cwd falls back to the broker-inherited cwd (None) rather than aborting.

+    let project_cwd = std::env::current_dir()

+        .map(|p| p.to_string_lossy().into_owned());


Now fix all `SpawnReq {` literals + `launch_harness_brokered_in` callers. Find the latter:


crates\spt\src\cli.rs:1112:    match spt_daemon::harnesshost::launch_harness_brokered_in(
crates\spt-daemon\tests\attach.rs:279:// attach, end to end: `harnesshost::launch_harness_brokered_in` spawns a harness's
crates\spt-daemon\tests\attach.rs:306:    let spawned = spt_daemon::harnesshost::launch_harness_brokered_in(
crates\spt-daemon\src\harnesshost.rs:8://! **broker-held PTY** ([`launch_harness_brokered_in`]). The harness still
crates\spt-daemon\src\harnesshost.rs:60:/// [`launch_harness_brokered_in`] is the only side effect. Fails closed: a
crates\spt-daemon\src\harnesshost.rs:111:pub fn launch_harness_brokered_in(


Only attach.rs:306 + cli call it. Update the existing bringup test call (+`None`, preserves W1 semantics):


304	    ))
305	    .expect("harness manifest");
306	    let spawned = spt_daemon::harnesshost::launch_harness_brokered_in(
307	        &name_a, "cc-ep", "mock", "sess-1", &manifest,
308	    )
309	    .expect("spt-hosted bringup spawns into broker A's PTY");
310
311	    // Operator on B attaches CROSS-NODE to the brought-up session.

    ))
    .expect("harness manifest");
    let spawned = spt_daemon::harnesshost::launch_harness_brokered_in(
        &name_a, "cc-ep", "mock", "sess-1", &manifest,
    )
    .expect("spt-hosted bringup spawns into broker A's PTY");

    // Operator on B attaches CROSS-NODE to the brought-up session.


    let spawned = spt_daemon::harnesshost::launch_harness_brokered_in(
        &name_a, "cc-ep", "mock", "sess-1", &manifest,
    )
    .expect("spt-hosted bringup spawns into broker A's PTY");

    let spawned = spt_daemon::harnesshost::launch_harness_brokered_in(
        &name_a, "cc-ep", "mock", "sess-1", &manifest, None,
    )
    .expect("spt-hosted bringup spawns into broker A's PTY");


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

    let spawned = spt_daemon::harnesshost::launch_harness_brokered_in(
        &name_a, "cc-ep", "mock", "sess-1", &manifest,
    )
    .expect("spt-hosted bringup spawns into broker A's PTY");

    let spawned = spt_daemon::harnesshost::launch_harness_brokered_in(
        &name_a, "cc-ep", "mock", "sess-1", &manifest, None,
    )
    .expect("spt-hosted bringup spawns into broker A's PTY");

-        &name_a, "cc-ep", "mock", "sess-1", &manifest,

+        &name_a, "cc-ep", "mock", "sess-1", &manifest, None,


Sweep SpawnReq literals (+`cwd: None`). Production first — read shellhost + applyhost literals:


221	        Brain::cold_start(broker_name, now_ms()).map_err(|e| format!("broker connect: {e}"))?;
222	    let (_sid, pid) = brain
223	        .spawn_session_pid(SpawnReq {
224	            program: program.clone(),
225	            args: args.to_vec(),
226	            // A shell binary is not a terminal: the PTY is only the stdin
227	            // conduit. Keep it wide so renderer-side wrapping (ConPTY) can
228	            // never split a >80-col stamped frame mid-line.
229	            rows: 24,
230	            cols: 512,
231	            endpoint: shell_session_label(owner, shell_id),
232	        })
233	        .map_err(|e| format!("broker spawn: {e}"))?;
234	    let pid = pid.unwrap_or(0);

        Brain::cold_start(broker_name, now_ms()).map_err(|e| format!("broker connect: {e}"))?;
    let (_sid, pid) = brain
        .spawn_session_pid(SpawnReq {
            program: program.clone(),
            args: args.to_vec(),
            // A shell binary is not a terminal: the PTY is only the stdin
            // conduit. Keep it wide so renderer-side wrapping (ConPTY) can
            // never split a >80-col stamped frame mid-line.
            rows: 24,
            cols: 512,
            endpoint: shell_session_label(owner, shell_id),
        })
        .map_err(|e| format!("broker spawn: {e}"))?;
    let pid = pid.unwrap_or(0);


            rows: 24,
            cols: 512,
            endpoint: shell_session_label(owner, shell_id),
        })

            rows: 24,
            cols: 512,
            endpoint: shell_session_label(owner, shell_id),
            // Shells inherit the broker's cwd (unchanged); project-scoped cwd is
            // a harness-bringup concern (REQ-HOST-RUN-2), not shell spawn.
            cwd: None,
        })


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

            rows: 24,
            cols: 512,
            endpoint: shell_session_label(owner, shell_id),
        })

            rows: 24,
            cols: 512,
            endpoint: shell_session_label(owner, shell_id),
            // Shells inherit the broker's cwd (unchanged); project-scoped cwd is
            // a harness-bringup concern (REQ-HOST-RUN-2), not shell spawn.
            cwd: None,
        })

+            // Shells inherit the broker's cwd (unchanged); project-scoped cwd is

+            // a harness-bringup concern (REQ-HOST-RUN-2), not shell spawn.


564	    fn echo_spawn_req() -> SpawnReq {
565	        #[cfg(unix)]
566	        let (program, args) = ("cat".to_string(), Vec::<String>::new());
567	        #[cfg(windows)]
568	        let (program, args) = ("findstr".to_string(), vec![".".to_string()]);
569	        SpawnReq {
570	            program,
571	            args,
572	            rows: 24,
573	            cols: 80,
574	            endpoint: String::new(),
575	        }
576	    }
577

    fn echo_spawn_req() -> SpawnReq {
        #[cfg(unix)]
        let (program, args) = ("cat".to_string(), Vec::<String>::new());
        #[cfg(windows)]
        let (program, args) = ("findstr".to_string(), vec![".".to_string()]);
        SpawnReq {
            program,
            args,
            rows: 24,
            cols: 80,
            endpoint: String::new(),
        }
    }


            rows: 24,
            cols: 80,
            endpoint: String::new(),
        }
    }

            rows: 24,
            cols: 80,
            endpoint: String::new(),
            cwd: None,
        }
    }


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

            rows: 24,
            cols: 80,
            endpoint: String::new(),
        }
    }

            rows: 24,
            cols: 80,
            endpoint: String::new(),
            cwd: None,
        }
    }

rows: 24,
cols: 80,
endpoint: String::new(),
+            cwd: None,
}
}


Now test literals. Grep their `endpoint:` lines to batch the edits:


crates\spt\tests\brain_survive.rs:71:use spt_daemon::endpoint::broker_socket_name;
crates\spt\tests\brain_survive.rs:77:use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
crates\spt\tests\brain_survive.rs:95:        endpoint: String::new(),
crates\spt-daemon\tests\budget.rs:89:            endpoint: format!("budget-{i}"),
crates\spt-daemon\tests\dispatch.rs:27:use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
crates\spt-daemon\tests\dispatch.rs:441:            endpoint: "ling-drive".to_string(),
crates\spt-daemon\tests\dispatch.rs:450:            endpoint: String::new(),
crates\spt-daemon\tests\attach.rs:33:use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
crates\spt-daemon\tests\attach.rs:97:        endpoint: String::new(),
crates\spt-daemon\tests\attach.rs:136:    endpoint: &str,
crates\spt-daemon\tests\broker.rs:51:        endpoint: String::new(),
crates\spt-daemon\tests\access.rs:24:use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
crates\spt-daemon\tests\mesh.rs:44:use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
crates\spt-daemon\tests\brain_swap.rs:59:        endpoint: String::new(),
crates\spt-daemon\tests\brain_swap.rs:168:    // The post-update brain still fully drives the same endpoint: kill it and see
crates\spt-daemon\tests\handoff.rs:49:        endpoint: String::new(),
crates\spt-daemon\tests\daemon_e2e.rs:68:        endpoint: String::new(),
crates\spt-daemon\tests\idempotent.rs:54:        endpoint: String::new(),
crates\spt-daemon\tests\pairjoin.rs:28:use spt_net::net::endpoint::{BindScope, LocalDiscovery, NetEndpoint, RelayPolicy};
crates\spt-daemon\tests\notifsync.rs:27:use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
crates\spt-daemon\tests\netbroker.rs:27:use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
crates\spt-daemon\tests\netstream.rs:26:use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
crates\spt-daemon\tests\presence.rs:25:use spt_net::net::endpoint::{BindScope, EndpointAddr, LocalDiscovery, RelayPolicy};
crates\spt-daemon\tests\propagate.rs:30:use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
crates\spt-daemon\tests\pump.rs:27:use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
crates\spt-daemon\tests\replicate.rs:26:use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
crates\spt-daemon\tests\reseed.rs:20:use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
crates\spt-daemon\tests\resume.rs:72:fn producer_req(endpoint: &str) -> SpawnReq {
crates\spt-daemon\tests\resume.rs:101:        endpoint: endpoint.to_string(),
crates\spt-daemon\tests\rosterprop.rs:21:use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
crates\spt-daemon\tests\seedproofx.rs:17:use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
crates\spt-daemon\tests\twohost.rs:72:use spt_net::net::endpoint::{
crates\spt-daemon\tests\twohost.rs:558:            endpoint: ID_B.to_string(),
crates\spt-daemon\tests\xfer.rs:28:use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
crates\spt-daemon\tests\sync.rs:37:use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
crates\spt-daemon\tests\wanmsg.rs:30:use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};


Batch the test-literal edits (`+cwd: None`, matching each indent):


        endpoint: String::new(),
        cwd: None,


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


Must Read each file first. Read all the test-literal regions in parallel:


90	    SpawnReq {
91	        program,
92	        args,
93	        rows: 24,
94	        cols: 80,
95	        endpoint: String::new(),
96	    }
97	}

    SpawnReq {
        program,
        args,
        rows: 24,
        cols: 80,
        endpoint: String::new(),
    }
}


83	        let mut brain = connect_retry(&name);
84	        let req = SpawnReq {
85	            program: program.clone(),
86	            args: args.clone(),
87	            rows: 24,
88	            cols: 80,
89	            endpoint: format!("budget-{i}"),
90	        };
91	        let (session, pid) = brain.spawn_session_pid(req).expect("spawn seat");

        let mut brain = connect_retry(&name);
        let req = SpawnReq {
            program: program.clone(),
            args: args.clone(),
            rows: 24,
            cols: 80,
            endpoint: format!("budget-{i}"),
        };
        let (session, pid) = brain.spawn_session_pid(req).expect("spawn seat");


46	    SpawnReq {
47	        program,
48	        args,
49	        rows: 24,
50	        cols: 80,
51	        endpoint: String::new(),
52	    }

    SpawnReq {
        program,
        args,
        rows: 24,
        cols: 80,
        endpoint: String::new(),
    }


54	    SpawnReq {
55	        program,
56	        args,
57	        rows: 24,
58	        cols: 80,
59	        endpoint: String::new(),
60	    }


44	    SpawnReq {
45	        program,
46	        args,
47	        rows: 24,
48	        cols: 80,
49	        endpoint: String::new(),
50	    }


63	    SpawnReq {
64	        program,
65	        args,
66	        rows: 24,
67	        cols: 80,
68	        endpoint: String::new(),
69	    }


49	    SpawnReq {
50	        program,
51	        args,
52	        rows: 24,
53	        cols: 80,
54	        endpoint: String::new(),
55	    }


96	    SpawnReq {
97	        program,
98	        args,
99	        rows: 24,
100	        cols: 80,
101	        endpoint: endpoint.to_string(),
102	    }

    SpawnReq {
        program,
        args,
        rows: 24,
        cols: 80,
        endpoint: endpoint.to_string(),
    }


434	    let mut b_host = connect_retry(&b_name);
435	    let labeled = b_host
436	        .spawn_session(spt_daemon::msg::SpawnReq {
437	            program: program.clone(),
438	            args: args.clone(),
439	            rows: 24,
440	            cols: 80,
441	            endpoint: "ling-drive".to_string(),
442	        })
443	        .expect("spawn labeled");
444	    let unlabeled = b_host
445	        .spawn_session(spt_daemon::msg::SpawnReq {
446	            program,
447	            args,
448	            rows: 24,
449	            cols: 80,
450	            endpoint: String::new(),
451	        })
452	        .expect("spawn unlabeled");
453

    let mut b_host = connect_retry(&b_name);
    let labeled = b_host
        .spawn_session(spt_daemon::msg::SpawnReq {
            program: program.clone(),
            args: args.clone(),
            rows: 24,
            cols: 80,
            endpoint: "ling-drive".to_string(),
        })
        .expect("spawn labeled");
    let unlabeled = b_host
        .spawn_session(spt_daemon::msg::SpawnReq {
            program,
            args,
            rows: 24,
            cols: 80,
            endpoint: String::new(),
        })
        .expect("spawn unlabeled");


551	    let (program, args) = ("findstr".to_string(), vec![".".to_string()]);
552	    let sid = host_brain
553	        .spawn_session(spt_daemon::msg::SpawnReq {
554	            program,
555	            args,
556	            rows: 24,
557	            cols: 80,
558	            endpoint: ID_B.to_string(),
559	        })
560	        .expect("spawn drive target");

    let (program, args) = ("findstr".to_string(), vec![".".to_string()]);
    let sid = host_brain
        .spawn_session(spt_daemon::msg::SpawnReq {
            program,
            args,
            rows: 24,
            cols: 80,
            endpoint: ID_B.to_string(),
        })
        .expect("spawn drive target");


Now apply all edits:


        endpoint: String::new(),
    }
}

        endpoint: String::new(),
        cwd: None,
    }
}


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

        endpoint: String::new(),
    }
}

        endpoint: String::new(),
        cwd: None,
    }
}

rows: 24,
cols: 80,
endpoint: String::new(),
+        cwd: None,
}
}


        endpoint: String::new(),
        cwd: None,
    }


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

        endpoint: String::new(),
        cwd: None,
    }

//! B1 integration: a [`Broker`] hosts one real PTY child and relays its output
//! and injected input over the versioned local IPC, on both OSes.
//!
//! The child is an echo program (`cat` on Unix, `findstr "."` on Windows — the
//! same cross-OS shape spt-term's injection tests use): bytes the brain injects
//! round-trip through the real PTY and come back as `output` events. We then
//! kill it and observe the `exit` event. This exercises the whole B1 contract —
//! spawn under a broker PTY, stream output, accept input, report exit — through
//! the actual named-pipe / UDS transport.
//!
//! The brain is driven **single-threaded**: send a command, then read frames
//! until the expected reply/event arrives (accumulating any interleaved output).
//! interprocess `Stream::split()` shares the underlying fd, so a split client
//! would only signal EOF once *both* halves drop; keeping one `Stream` means
//! dropping it closes the fd, the broker's command loop hits EOF, and it tears
//! the session down deterministically — which we then assert.
// [int->REQ-DAEMON-2]

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

use interprocess::local_socket::Stream;
use spt_daemon::codec::{read_frame, write_frame};
use spt_daemon::frame::{Envelope, Role};
use spt_daemon::msg::{
    decode_bytes, encode_bytes, ExitEvent, InputReq, KillReq, OutputEvent, SpawnReq, Spawned,
    KIND_EXIT, KIND_INPUT, KIND_KILL, KIND_OUTPUT, KIND_SPAWN, KIND_SPAWNED,
};
use spt_daemon::transport::{send_hello, LocalSocketTransport};
use spt_daemon::Broker;

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

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

fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {
    write_frame(conn, &Envelope::new(kind, payload)).expect("send frame");
}

fn main_test() {
    let name = unique_name();
    let broker = Broker::bind(&name).expect("bind broker");

    // The broker accepts one brain and serves it on a handler thread.
    let server_broker = Arc::clone(&broker);
    let server = thread::spawn(move || {
        let handle = server_broker.serve_once().expect("accept brain");
        handle.join().expect("handler thread");
    });

    // Brain side: connect (retry briefly in case accept lags the bind).
    let mut conn = None;
    for _ in 0..200 {
        match LocalSocketTransport::connect(&name) {
            Ok(c) => {
                conn = Some(c);
                break;
            }
            Err(_) => thread::sleep(Duration::from_millis(10)),
        }
    }
    let mut conn = conn.expect("connect to broker");
    // (No per-read timeout: Windows named pipes reject I/O timeouts. The broker
    // always pushes `spawned`/`output`/`exit` frames, so the blocking reads
    // below return; a genuine stall is caught by the test runner's own timeout.)

    send_hello(&mut conn, Role::Brain).expect("send hello");

    // Spawn the echo child under a broker PTY; read until the `spawned` reply,
    // accumulating any output that interleaves.
    send(
        &mut conn,
        KIND_SPAWN,
        serde_json::to_value(echo_spawn_req()).unwrap(),
    );
    let mut out = Vec::<u8>::new();
    let sid = loop {
        let f = read_frame(&mut conn).expect("frame before spawned");
        match f.kind.as_str() {
            KIND_SPAWNED => {
                break serde_json::from_value::<Spawned>(f.payload)
                    .unwrap()
                    .session_id;
            }
            KIND_OUTPUT => accumulate_output(&f, &mut out),
            _ => {}
        }
    };

    // Inject a line; it must round-trip through the real PTY back as output.
    send(
        &mut conn,
        KIND_INPUT,
        serde_json::to_value(InputReq {
            session_id: sid,
            data_b64: encode_bytes(b"INJECT-ALPHA\r"),
            op_id: None,
        })
        .unwrap(),
    );
    let echoed = loop {
        if String::from_utf8_lossy(&out).contains("INJECT-ALPHA") {
            break true;
        }
        match read_frame(&mut conn) {
            Ok(f) if f.kind == KIND_OUTPUT => accumulate_output(&f, &mut out),
            Ok(_) => {}
            Err(_) => break false, // recv timeout → echo never arrived
        }
    };

    // Kill the child and read until the `exit` event.
    send(
        &mut conn,
        KIND_KILL,
        serde_json::to_value(KillReq { session_id: sid }).unwrap(),
    );
    let mut exit: Option<ExitEvent> = None;
    loop {
        match read_frame(&mut conn) {
            Ok(f) if f.kind == KIND_EXIT => {
                exit = Some(serde_json::from_value(f.payload).unwrap());
                break;
            }
            Ok(f) if f.kind == KIND_OUTPUT => accumulate_output(&f, &mut out),
            Ok(_) => {}
            Err(_) => break, // timeout
        }
    }

    // Disconnect: dropping the single Stream closes the fd, so the broker's
    // command loop hits EOF and tears the session down. Joining the handler
    // (via the server thread) lets us assert the teardown happened.
    drop(conn);
    server.join().expect("server thread");

    assert!(
        echoed,
        "injected input must round-trip through the PTY: {:?}",
        String::from_utf8_lossy(&out)
    );
    let exit = exit.expect("exit event after kill");
    assert_eq!(exit.session_id, sid);
    assert_eq!(broker.session_count(), 0, "session torn down on disconnect");
}

fn accumulate_output(frame: &Envelope, out: &mut Vec<u8>) {
    let ev: OutputEvent = serde_json::from_value(frame.payload.clone()).unwrap();
    out.extend_from_slice(&decode_bytes(&ev.data_b64).unwrap());
}

#[test]
fn broker_hosts_pty_child_streams_output_and_accepts_input() {
    main_test();
}


        endpoint: String::new(),
        cwd: None,
    }


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

//! C0 E2E — the M3c keystone: a **brain-only self-update** swaps the brain logic
//! through the update engine with **zero hosted-endpoint interruption**
//! (REQ-UPD-3, absolute for the brain-only class — ADR-0004 §A).
//!
//! This rides the same broker/brain substrate the B2 handoff test proves, but
//! drives the swap through the *update engine* (`plan_update` → `apply_brain_only`)
//! rather than calling `Brain::handoff` directly — the engine *triggers* a
//! handoff, it does not re-implement one (`M3c-PLAN.md` §C0, clean-room posture).
//!
//! Shape: one echo child runs under a broker PTY for the whole test. The outgoing
//! brain injects a marker it never reads, then the engine applies a brain-only
//! update: the new brain re-attaches via the supplied relaunch closure (which
//! stands for "exec the verified new binary → `Brain::handoff`") and must find
//! that marker in the *replayed* buffer — proving the broker buffered output
//! produced while no brain was attached. Throughout, the child's pid is unchanged
//! and exactly one session is hosted: the endpoint never noticed the swap.
//!
//! **Filename note:** this file is `brain_swap.rs`, NOT `update.rs`, on purpose.
//! Windows' installer-detection heuristic (UAC) forces elevation for any
//! executable whose name contains `update`/`setup`/`install`/`patch`; a test
//! binary `update-<hash>.exe` then fails to launch with `os error 740` (requires
//! elevation). Do not rename this to match the module under test.
//!
//! **REQ-UPD-3 `int` re-pointed (restoration D7-1).** This test proves the
//! *in-process* brain-only swap shape (the engine's `apply_brain_only` over an
//! in-thread `Brain::handoff` closure) — valuable engine-mechanics coverage, but
//! NOT process-level survival. The REQ-UPD-3 `int` evidence (no endpoint drops
//! across a brain-only update) now lives in `crates/spt/tests/brain_survive.rs`,
//! which swaps a real on-disk binary and respawns the real `spt daemon brain`
//! child process (ADR-0018 V5). This file keeps exercising the engine path; it is
//! no longer the REQ-UPD-3 int tag.

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

use spt_daemon::brain::{Brain, BrainState, BrokerEvent};
use spt_daemon::msg::SpawnReq;
use spt_daemon::update::{apply_brain_only, plan_update, BrokerAbi, ReleaseSpec, UpdateClass};
use spt_daemon::Broker;

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

fn echo_spawn_req() -> SpawnReq {
    #[cfg(unix)]
    let (program, args) = ("cat".to_string(), Vec::<String>::new());
    #[cfg(windows)]
    let (program, args) = ("findstr".to_string(), vec![".".to_string()]);
    SpawnReq {
        program,
        args,
        rows: 24,
        cols: 80,
        endpoint: String::new(),
    }
}

fn cold_retry(name: &str, now_ms: u64) -> Brain {
    for _ in 0..300 {
        if let Ok(b) = Brain::cold_start(name, now_ms) {
            return b;
        }
        thread::sleep(Duration::from_millis(10));
    }
    panic!("brain cold-start could not connect");
}

/// The relaunch closure C0's engine drives: it stands for "the verified new
/// binary's brain re-attaches over the existing substrate." Retries the connect
/// (the broker may not have freed the prior brain's slot the instant we drop it),
/// mirroring how a freshly-exec'd brain process races the broker on startup.
fn handoff_relaunch(name: &str, prior: BrainState, now_ms: u64) -> std::io::Result<Brain> {
    let mut last = None;
    for _ in 0..300 {
        match Brain::handoff(name, prior.clone(), now_ms) {
            Ok(b) => return Ok(b),
            Err(e) => last = Some(e),
        }
        thread::sleep(Duration::from_millis(10));
    }
    Err(last.unwrap_or_else(|| std::io::Error::other("relaunch never connected")))
}

#[test]
fn brain_only_update_swaps_logic_with_zero_endpoint_interruption() {
    let name = unique_name();
    let broker = Broker::bind(&name).expect("bind broker");

    // The broker is the stable kernel: it outlives every brain generation.
    let serve_broker = Arc::clone(&broker);
    thread::spawn(move || {
        let _ = serve_broker.serve();
    });

    // Generation 0: cold-start the brain, spawn the long-lived hosted endpoint,
    // record its pid — the thing REQ-UPD-3 forbids us from disturbing.
    let mut now = 1_000_000u64;
    let mut brain = cold_retry(&name, now);
    let sid = brain
        .spawn_session(echo_spawn_req())
        .expect("spawn session");
    let child_pid = broker.session_pid(sid);
    assert_eq!(brain.generation(), 0);

    // The engine classifies the release against the RUNNING broker. A pure logic
    // release (same held-resource ABI, a brain the running broker already hosts)
    // ⇒ brain-only: the routine, endpoint-preserving case.
    let running = BrokerAbi::current();
    let release = ReleaseSpec {
        brain_ipc_version: running.ipc_version,
        broker_resource_abi: running.resource_abi,
    };
    let plan = plan_update(&running, release);
    assert_eq!(
        plan.class,
        UpdateClass::BrainOnly,
        "a pure-logic release is brain-only"
    );

    // The outgoing brain hands a marker to its successor and then is swapped out
    // WITHOUT reading it — only a gapless re-attach can recover it from the
    // broker's replay buffer.
    brain
        .send_input(b"UPDATED_MARKER_DONE\r")
        .expect("inject marker for successor");

    // Apply the brain-only update through the engine. This is the swap: snapshot
    // → drop the outgoing brain (the crash boundary the broker tolerates) →
    // relaunch the new brain over the substrate. The engine touches no endpoint.
    now += 1; // wall clock advances so the new generation gets a fresh gen_start
    let mut brain = apply_brain_only(&plan, brain, |state| handoff_relaunch(&name, state, now))
        .expect("brain-only update must apply");

    // ── REQ-UPD-3: the hosted endpoint never noticed the swap. ──
    assert_eq!(
        broker.session_count(),
        1,
        "exactly one session across the update"
    );
    assert_eq!(
        broker.session_pid(sid),
        child_pid,
        "the hosted child's pid is unchanged"
    );

    // The new brain is a fresh generation with a fresh gen_start (the swap really
    // happened — it is not the same brain object).
    assert_eq!(brain.generation(), 1, "the brain logic generation advanced");
    assert_eq!(
        brain.gen_start_ms(),
        now,
        "fresh gen_start on the swapped-in brain (2.4)"
    );

    // Gapless resume: the marker the pre-update brain injected (and never read)
    // arrives via the broker's replayed buffer — output produced across the swap
    // window was not lost.
    let mut buf = Vec::new();
    brain
        .read_output_until(b"UPDATED_MARKER_DONE", &mut buf)
        .expect("the swapped-in brain must replay output gaplessly across the update");

    // The post-update brain still fully drives the same endpoint: kill it and see
    // the exit event + reaping — the session was live and owned, not a husk.
    brain.kill_session().expect("kill session");
    loop {
        if let BrokerEvent::Exit { session_id, .. } = brain.read_event().expect("read until exit") {
            assert_eq!(session_id, sid);
            break;
        }
    }
    drop(brain);

    let mut reaped = false;
    for _ in 0..200 {
        if broker.session_count() == 0 {
            reaped = true;
            break;
        }
        thread::sleep(Duration::from_millis(10));
    }
    assert!(reaped, "the killed session is reaped from the broker table");
}


        endpoint: String::new(),
        cwd: None,
    }


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

//! B2 regression — the milestone's load-bearing proof: a brain can be killed and
//! restarted repeatedly while the broker and its hosted child survive untouched,
//! and each new brain re-attaches to the output stream **gaplessly** with a fresh
//! `gen_start` (Spike #1 made real; KNOWN-HAZARDS 2.3 / 2.4).
//!
//! Shape: one echo child runs under a broker PTY for the whole test. Across 12
//! handoffs, each brain (a) re-attaches from its predecessor's resume cursor and
//! must find — in the *replayed* buffer — the marker its predecessor injected
//! and then died before reading (proving the broker buffered output produced
//! while no brain was attached), (b) injects the next marker for its successor,
//! then (c) "crashes" by dropping its connection. Throughout, the child's pid is
//! unchanged and exactly one session is hosted; `gen_start` advances every
//! handoff and the generation counter increments.
//!
//! A real-process SIGKILL of the brain was proven in Spike #1; here the brain is
//! a library driven in-thread and a dropped connection *is* the crash boundary
//! the broker must tolerate (subscriber vanishes; child + log persist).
// [int->REQ-HAZARD-GEN-START-NOW]
// REQ-DAEMON-2 `int` re-pointed to crates/spt/tests/brain_survive.rs (restoration
// D7-1): this proves the *in-process* handoff shape (a dropped connection as the
// crash boundary, brain driven in-thread) — the process-level survival onto a
// swapped binary is the D7-1 E2E. This file keeps proving the gen-start invariant.

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

use spt_daemon::brain::{Brain, BrainState, BrokerEvent};
use spt_daemon::msg::SpawnReq;
use spt_daemon::Broker;

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

fn echo_spawn_req() -> SpawnReq {
    #[cfg(unix)]
    let (program, args) = ("cat".to_string(), Vec::<String>::new());
    #[cfg(windows)]
    let (program, args) = ("findstr".to_string(), vec![".".to_string()]);
    SpawnReq {
        program,
        args,
        rows: 24,
        cols: 80,
        endpoint: String::new(),
    }
}

fn cold_retry(name: &str, now_ms: u64) -> Brain {
    for _ in 0..300 {
        if let Ok(b) = Brain::cold_start(name, now_ms) {
            return b;
        }
        thread::sleep(Duration::from_millis(10));
    }
    panic!("brain cold-start could not connect");
}

fn handoff_retry(name: &str, prior: BrainState, now_ms: u64) -> Brain {
    for _ in 0..300 {
        if let Ok(b) = Brain::handoff(name, prior.clone(), now_ms) {
            return b;
        }
        thread::sleep(Duration::from_millis(10));
    }
    panic!("brain handoff could not connect");
}

#[test]
fn brain_restart_survives_gaplessly_with_fresh_gen_start() {
    const RESTARTS: u64 = 12; // >= 10 (B2 acceptance)
    let name = unique_name();
    let broker = Broker::bind(&name).expect("bind broker");

    // The broker accepts connections forever on a detached thread; it outlives
    // every brain (that is the whole point).
    let serve_broker = Arc::clone(&broker);
    thread::spawn(move || {
        let _ = serve_broker.serve();
    });

    // Generation 0: cold start, spawn the long-lived child, inject MARKER_1 for
    // the first handed-off brain to discover in the replay buffer, then crash.
    let mut now = 1_000_000u64;
    let mut brain = cold_retry(&name, now);
    let sid = brain
        .spawn_session(echo_spawn_req())
        .expect("spawn session");
    let child_pid = broker.session_pid(sid);
    assert_eq!(brain.generation(), 0);

    let mut gen_starts = vec![brain.gen_start_ms()];
    let mut generations = vec![brain.generation()];

    brain
        .send_input(b"MARKER_1_DONE\r")
        .expect("inject first marker");
    let mut state = brain.snapshot();
    drop(brain); // crash

    let mut buf: Vec<u8> = Vec::new();
    for k in 1..=RESTARTS {
        now += 1; // advance wall clock so a fresh gen_start is strictly greater
        let mut brain = handoff_retry(&name, state.clone(), now);

        // 2.4: gen_start is now(), NOT the stale value rehydrated from state.
        assert_eq!(
            brain.gen_start_ms(),
            now,
            "gen_start must be now(), not rehydrated"
        );
        assert_ne!(
            brain.gen_start_ms(),
            state.gen_start_ms,
            "gen_start must not carry the prior generation's stale value"
        );
        assert_eq!(
            brain.generation(),
            state.generation + 1,
            "generation increments"
        );
        gen_starts.push(brain.gen_start_ms());
        generations.push(brain.generation());

        // The child and its PTY survived the restart untouched.
        assert_eq!(
            broker.session_count(),
            1,
            "exactly one session across restarts"
        );
        assert_eq!(
            broker.session_pid(sid),
            child_pid,
            "child pid stable across restart"
        );

        // Gapless resume: the marker the previous brain injected (and died before
        // reading) must arrive via the replayed buffer.
        let marker = format!("MARKER_{k}_DONE");
        brain
            .read_output_until(marker.as_bytes(), &mut buf)
            .unwrap_or_else(|e| panic!("gapless resume must replay {marker}: {e}"));

        // Hand the next marker to our successor, then crash without reading it.
        if k < RESTARTS {
            brain
                .send_input(format!("MARKER_{}_DONE\r", k + 1).as_bytes())
                .expect("inject next marker");
        }
        state = brain.snapshot();
        drop(brain);
    }

    // gen_start strictly advanced every handoff; generations are 0,1,2,...
    for w in gen_starts.windows(2) {
        assert!(
            w[1] > w[0],
            "gen_start must advance each handoff: {gen_starts:?}"
        );
    }
    for (i, g) in generations.iter().enumerate() {
        assert_eq!(
            *g, i as u64,
            "generation counter contiguous: {generations:?}"
        );
    }

    // Final generation: kill the child and confirm the exit event + reaping.
    now += 1;
    let mut brain = handoff_retry(&name, state, now);
    brain.kill_session().expect("kill session");
    loop {
        if let BrokerEvent::Exit { session_id, .. } = brain.read_event().expect("read until exit") {
            assert_eq!(session_id, sid);
            break;
        }
    }
    drop(brain);

    // The dead session is reaped from the broker table.
    let mut reaped = false;
    for _ in 0..200 {
        if broker.session_count() == 0 {
            reaped = true;
            break;
        }
        thread::sleep(Duration::from_millis(10));
    }
    assert!(
        reaped,
        "child exit must reap the session from the broker table"
    );
}


        endpoint: String::new(),
        cwd: None,
    }


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


        endpoint: String::new(),
        cwd: None,
    }