Acked. Now soften the cfg(unix) assertion to informational.


Edit

    // PLATFORM-DEPENDENT FINDING (W1 repro, 2026-06-19):
    // - Windows ConPTY ABSORBS a large inject into a stdin-ignoring child without
    //   blocking write_all — `inject_acked` is true, park (b) does NOT fire. So on
    //   the operator's platform the input-side write is BENIGN; the operator wedge
    //   is park (a) (controller output delivery), reached on injection via the
    //   injection-INDUCED output backing up the controller — not the inject write.
    // - Unix forkpty has a small kernel input queue, so the same inject is
    //   EXPECTED to park write_all (park b real there). Confirmed by CI on Linux.
    // The W1 fix still bounds the PTY write defensively (Unix + W2's atomic-write
    // coordination), but this test records WHICH park is the Windows root.
    // [int->REQ-HAZARD-INJECT-CONTROL-COEXIST]
    #[cfg(windows)]
    assert!(
        inject_acked,
        "Windows ConPTY is expected to ABSORB the inject (park b benign here); \
         if this flips, the input buffer began blocking — re-examine park (b)."
    );
    #[cfg(unix)]
    assert!(
        !inject_acked,
        "REPRO (park b, Unix forkpty): injecting into a child that never reads \
         stdin parks write_input's write_all on the full kernel input queue — the \
         inject is never acked. The W1 fix (bounded / fail-fast PTY write) flips this."
    );
}

    // PLATFORM-DEPENDENT FINDING (W1 repro, 2026-06-19):
    // - Windows ConPTY ABSORBS a large inject into a stdin-ignoring child without
    //   blocking write_all — `inject_acked` is true, park (b) does NOT fire. So on
    //   the operator's platform the input-side write is BENIGN; the operator wedge
    //   is park (a) (controller output delivery), reached on injection via the
    //   injection-INDUCED output backing up the controller — not the inject write.
    // - Unix forkpty has a small kernel input queue, so the same inject is
    //   HYPOTHESIZED to park write_input's write_all (park b). That is UNVERIFIED
    //   (no Linux run yet) and absorbing would be BENIGN, not a regression — so we
    //   do NOT assert it (asserting a maybe-wrong bug whose failure mode is good =
    //   a false-red). cfg(unix) only CAPTURES + prints the observation; gravity-linux
    //   CI is the observation venue. W2 converts it to a real pre-fix repro assert
    //   (and bounds the park) once CI actually shows it. The only hard invariant
    //   here is broker liveness (asserted above for both platforms).
    // [int->REQ-HAZARD-INJECT-CONTROL-COEXIST]
    #[cfg(windows)]
    assert!(
        inject_acked,
        "Windows ConPTY is expected to ABSORB the inject (park b benign here); \
         if this flips, the input buffer began blocking — re-examine park (b)."
    );
    #[cfg(unix)]
    eprintln!(
        "=== W1 PARK-B (Unix forkpty observation, not asserted): inject_acked={inject_acked} \
         (absorbed=true is benign; park=false would be the hypothesized forkpty park) ==="
    );
}


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

    // PLATFORM-DEPENDENT FINDING (W1 repro, 2026-06-19):
    // - Windows ConPTY ABSORBS a large inject into a stdin-ignoring child without
    //   blocking write_all — `inject_acked` is true, park (b) does NOT fire. So on
    //   the operator's platform the input-side write is BENIGN; the operator wedge
    //   is park (a) (controller output delivery), reached on injection via the
    //   injection-INDUCED output backing up the controller — not the inject write.
    // - Unix forkpty has a small kernel input queue, so the same inject is
    //   EXPECTED to park write_all (park b real there). Confirmed by CI on Linux.
    // The W1 fix still bounds the PTY write defensively (Unix + W2's atomic-write
    // coordination), but this test records WHICH park is the Windows root.
    // [int->REQ-HAZARD-INJECT-CONTROL-COEXIST]
    #[cfg(windows)]
    assert!(
        inject_acked,
        "Windows ConPTY is expected to ABSORB the inject (park b benign here); \
         if this flips, the input buffer began blocking — re-examine park (b)."
    );
    #[cfg(unix)]
    assert!(
        !inject_acked,
        "REPRO (park b, Unix forkpty): injecting into a child that never reads \
         stdin parks write_input's write_all on the full kernel input queue — the \
         inject is never acked. The W1 fix (bounded / fail-fast PTY write) flips this."
    );
}

    // PLATFORM-DEPENDENT FINDING (W1 repro, 2026-06-19):
    // - Windows ConPTY ABSORBS a large inject into a stdin-ignoring child without
    //   blocking write_all — `inject_acked` is true, park (b) does NOT fire. So on
    //   the operator's platform the input-side write is BENIGN; the operator wedge
    //   is park (a) (controller output delivery), reached on injection via the
    //   injection-INDUCED output backing up the controller — not the inject write.
    // - Unix forkpty has a small kernel input queue, so the same inject is
    //   HYPOTHESIZED to park write_input's write_all (park b). That is UNVERIFIED
    //   (no Linux run yet) and absorbing would be BENIGN, not a regression — so we
    //   do NOT assert it (asserting a maybe-wrong bug whose failure mode is good =
    //   a false-red). cfg(unix) only CAPTURES + prints the observation; gravity-linux
    //   CI is the observation venue. W2 converts it to a real pre-fix repro assert
    //   (and bounds the park) once CI actually shows it. The only hard invariant
    //   here is broker liveness (asserted above for both platforms).
    // [int->REQ-HAZARD-INJECT-CONTROL-COEXIST]
    #[cfg(windows)]
    assert!(
        inject_acked,
        "Windows ConPTY is expected to ABSORB the inject (park b benign here); \
         if this flips, the input buffer began blocking — re-examine park (b)."
    );
    #[cfg(unix)]
    eprintln!(
        "=== W1 PARK-B (Unix forkpty observation, not asserted): inject_acked={inject_acked} \
         (absorbed=true is benign; park=false would be the hypothesized forkpty park) ==="
    );
}

-    //   EXPECTED to park write_all (park b real there). Confirmed by CI on Linux.

-    // The W1 fix still bounds the PTY write defensively (Unix + W2's atomic-write

-    // coordination), but this test records WHICH park is the Windows root.

+    //   HYPOTHESIZED to park write_input's write_all (park b). That is UNVERIFIED

+    //   (no Linux run yet) and absorbing would be BENIGN, not a regression — so we

+    //   do NOT assert it (asserting a maybe-wrong bug whose failure mode is good =

+    //   a false-red). cfg(unix) only CAPTURES + prints the observation; gravity-linux

+    //   CI is the observation venue. W2 converts it to a real pre-fix repro assert

+    //   (and bounds the park) once CI actually shows it. The only hard invariant

+    //   here is broker liveness (asserted above for both platforms).

-        "REPRO (park b, Unix forkpty): injecting into a child that never reads \

-         stdin parks write_input's write_all on the full kernel input queue — the \

-         inject is never acked. The W1 fix (bounded / fail-fast PTY write) flips this."

+        "=== W1 PARK-B (Unix forkpty observation, not asserted): inject_acked={inject_acked} \

+         (absorbed=true is benign; park=false would be the hypothesized forkpty park) ==="


Verify the softened test still passes (Windows), then commit.


Bash

cargo test -p spt-daemon --test inject_control_wedge -- --test-threads=1 2>&1 | tail -12

Run softened W1 repro test


Command running in background with ID: bc9x58160. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\73618172-412e-4954-b8aa-e2c8955dfd49\tasks\bc9x58160.output. You will be notified when it completes. To check interim output, use Read on that file path.


W3 prep — locate the bind-cwd surface while the test runs.


Grep

fn establish_perch|fn cmd_bind|fn bind_from_seed|\.cwd\b|cwd:|seed\.cwd|current_dir


Found 2 files
crates\spt-runtime\src\runtime.rs
crates\spt-runtime\src\manifest.rs


<task-notification>
<task-id>bc9x58160</task-id>
<tool-use-id>toolu_01TDjwFHJkKvAowu9hmbrM5V</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\73618172-412e-4954-b8aa-e2c8955dfd49\tasks\bc9x58160.output</output-file>
<status>completed</status>
<summary>Background command "Run softened W1 repro test" completed (exit code 0)</summary>
</task-notification>


fn establish_perch|fn cmd_bind|fn bind_from_seed


crates\spt\src\api\startup.rs:119:pub fn bind_from_seed(
crates\spt\src\api\startup.rs:175:fn establish_perch(
crates\spt\src\api\startup.rs:408:pub fn cmd_bind(


Prompt:
<task-notification>
<task-id>bc9x58160</task-id>
<tool-use-id>toolu_01TDjwFHJkKvAowu9hmbrM5V</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\73618172-412e-4954-b8aa-e2c8955dfd49\tasks\bc9x58160.output</output-file>
<status>completed</status>
<summary>Background command "Run softened W1 repro test" completed (exit code 0)</summary>
</task-notification>


when you need to write tests, please use subagent profile `C:\Users\decid\Documents\projects\.claude\agents\spt-test-engineer.md`


Configure test engineer subagent profile


1	   Compiling spt-daemon v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
2	    Finished `test` profile [unoptimized + debuginfo] target(s) in 4.25s
3	     Running tests\inject_control_wedge.rs (target\debug\deps\inject_control_wedge-237ab1e3f78e76cc.exe)
4
5	running 2 tests
6	test a_backed_up_controller_does_not_wedge_the_session ... ok
7	test injecting_a_large_payload_into_a_stdin_ignoring_child ... ok
8
9	test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 8.73s
10
11

   Compiling spt-daemon v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 4.25s
     Running tests\inject_control_wedge.rs (target\debug\deps\inject_control_wedge-237ab1e3f78e76cc.exe)

running 2 tests
test a_backed_up_controller_does_not_wedge_the_session ... ok
test injecting_a_large_payload_into_a_stdin_ignoring_child ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 8.73s


108	/// Consume the seed for `parent_pid` and bind/revive the perch skeleton→live.
109	/// Pure of any listener I/O so it is unit-testable without spawning processes.
110	/// Returns the perch's capability token on success.
111	///
112	/// REQ-START-3: the seed is TAKEn (consume-once) from the daemon's in-memory map
113	/// — the take itself consumes it, so a stale/conflicting seed is spent and the
114	/// harness re-seeds on the next SessionStart rather than leaving it to re-bind.
115	// [impl->REQ-START-2]
116	// [impl->REQ-START-3]
117	// [impl->REQ-HAZARD-WINDOWS-PID-RECYCLE]
118	// [impl->REQ-START-5]
119	pub fn bind_from_seed(
120	    id: &str,
121	    parent_pid: u32,
122	    subnet: Option<&str>,
123	    adapter: Option<&str>,
124	) -> Result<String, BindError> {
125	    let seed = take_seed_from_daemon(parent_pid)?.ok_or(BindError::NoSeed(parent_pid))?;
126	    if seed.session_id.is_empty() {
127	        return Err(BindError::EmptySession);
128	    }
129	    // Pid-recycle defense (5.1): anchor liveness on the stable parent pid. A
130	    // dead anchor means the harness that wrote the seed is gone — a recycled or
131	    // abandoned seed must never bind.
132	    if !proc::is_process_alive(seed.parent_pid) {
133	        return Err(BindError::StaleSeed(seed.parent_pid));
134	    }
135	    // The seed is adapter-agnostic (ADR-0021): the creating adapter is the one
136	    // the caller resolved at bind (explicit --adapter override, else the
137	    // host_binaries/pointer resolver), not a seed-time snapshot. An empty option
138	    // stamps no adapter — the same backward-compatible degrade as before.
139	    let adapter = adapter.filter(|a| !a.is_empty());
140	    // The relay/listen path is the agent host — always an agent-family type.
141	    // (A non-agent endpoint, e.g. a Gateway, establishes via `api bind --type`,
142	    // not the live-agent relay loop.) Harness-hosted: the harness owns the process,
143	    // so there is no broker PTY → not controllable (REQ-PICKER-1).
144	    establish_perch(
145	        id,
146	        &seed.session_id,
147	        seed.parent_pid,
148	        adapter,
149	        subnet,
150	        "live_agent",
151	        Some(false),
152	    )
153	}
154
155	/// TAKE (consume) the seed for `parent_pid` from the daemon's in-memory map,
156	/// auto-starting the daemon if absent (REQ-DAEMON-3 / REQ-START-3).
157	fn take_seed_from_daemon(parent_pid: u32) -> Result<Option<Seed>, BindError> {
158	    spt_daemon::ensure_running().map_err(|e| BindError::Io(format!("daemon unavailable: {e}")))?;
159	    spt_daemon::take_seed(&spt_daemon::seed_socket_name(), parent_pid)
160	        .map_err(|e| BindError::Io(e.to_string()))
161	}
162
163	/// Write the perch record (skeleton→live) + ready marker + capability token.
164	/// Reviving an existing perch keeps its token; a live perch held under a
165	/// different session id is a conflict (pid-recycle / id collision guard).
166	///
167	/// Creation-time fields ride the shared seam
168	/// ([`spt_store::home::stamp_creation_fields`], REQ-INST-15): a NEW perch
169	/// assigns its immutable home (refuse-and-qualify on a multi-subnet node) +
170	/// seeds `sync_subnets = [home]`; a revive **carries forward** home, the
171	/// adapter (an explicit one wins — resume-under-new), the resource blurb,
172	/// and the auto-suspend override — a re-bind rewrites `info.json` and must
173	/// not wipe them.
174	// [impl->REQ-INST-15]
175	fn establish_perch(
176	    id: &str,
177	    session_id: &str,
178	    parent_pid: u32,
179	    adapter: Option<&str>,
180	    subnet: Option<&str>,
181	    endpoint_type: &str,
182	    controllable: Option<bool>,
183	) -> Result<String, BindError> {
184	    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
185
186	    let prior = info::read_info(&perch_path);
187	    if let Some(existing) = &prior {
188	        let owner_alive = info::read_pid(&perch_path)
189	            .map(proc::is_process_alive)
190	            .unwrap_or(false);
191	        if owner_alive && !existing.session_id.is_empty() && existing.session_id != session_id {
192	            return Err(BindError::Conflict {
193	                id: id.to_string(),
194	                held: existing.session_id.clone(),
195	            });
196	        }
197	    }
198
199	    std::fs::create_dir_all(&perch_path).map_err(|e| BindError::Io(e.to_string()))?;
200	    // The endpoint type tag (info.json `state`). Defaults to the agent path
201	    // (`live_agent`); a non-agent endpoint (e.g. a `gateway` — the open type
202	    // system, REQ-EP-1/REQ-EP-6) binds with its own tag via `api bind --type`.
203	    // [impl->REQ-EP-6]
204	    // Open by construction: any tag round-trips through EndpointType::from_tag,
205	    // and downstream type-keyed logic (the user-msg identity gate, REQ-MSG-5)
206	    // reads it back verbatim. A revive keeps the prior type unless overridden.
207	    let endpoint_type = prior
208	        .as_ref()
209	        .map(|p| p.state.clone())
210	        .filter(|s| endpoint_type == "live_agent" && !s.is_empty())
211	        .unwrap_or_else(|| endpoint_type.to_string());
212	    let mut rec = InfoJson::new(
213	        id,
214	        &now_stamp(),
215	        std::process::id(),
216	        session_id,
217	        &endpoint_type,
218	    );
219	    rec.parent_pid = Some(parent_pid);
220	    let subnets = spt_store::subnet::SubnetStore::load();
221	    let mut vis = spt_store::visibility::VisibilityStore::load();
222	    let dirty = spt_store::home::stamp_creation_fields(
223	        &mut rec,
224	        prior.as_ref(),
225	        &subnets,
226	        &mut vis,
227	        subnet,
228	        adapter,
229	    )
230	    .map_err(|e| BindError::Home(e.to_string()))?;
231	    if dirty {
232	        let _ = vis.save();
233	    }
234	    // Broker-PTY-controllability stamp (REQ-PICKER-1): the caller knows the hosting
235	    // mode — `listen` is harness-hosted (no broker PTY → Some(false)); `bind` of a
236	    // live_agent is spt-hosted (broker PTY → Some(true)); a non-live bind is None.
237	    // Explicit wins; a revive that omits it carries the prior value forward (like

/// Consume the seed for `parent_pid` and bind/revive the perch skeleton→live.
/// Pure of any listener I/O so it is unit-testable without spawning processes.
/// Returns the perch's capability token on success.
///
/// REQ-START-3: the seed is TAKEn (consume-once) from the daemon's in-memory map
/// — the take itself consumes it, so a stale/conflicting seed is spent and the
/// harness re-seeds on the next SessionStart rather than leaving it to re-bind.
// [impl->REQ-START-2]
// [impl->REQ-START-3]
// [impl->REQ-HAZARD-WINDOWS-PID-RECYCLE]
// [impl->REQ-START-5]
pub fn bind_from_seed(
    id: &str,
    parent_pid: u32,
    subnet: Option<&str>,
    adapter: Option<&str>,
) -> Result<String, BindError> {
    let seed = take_seed_from_daemon(parent_pid)?.ok_or(BindError::NoSeed(parent_pid))?;
    if seed.session_id.is_empty() {
        return Err(BindError::EmptySession);
    }
    // Pid-recycle defense (5.1): anchor liveness on the stable parent pid. A
    // dead anchor means the harness that wrote the seed is gone — a recycled or
    // abandoned seed must never bind.
    if !proc::is_process_alive(seed.parent_pid) {
        return Err(BindError::StaleSeed(seed.parent_pid));
    }
    // The seed is adapter-agnostic (ADR-0021): the creating adapter is the one
    // the caller resolved at bind (explicit --adapter override, else the
    // host_binaries/pointer resolver), not a seed-time snapshot. An empty option
    // stamps no adapter — the same backward-compatible degrade as before.
    let adapter = adapter.filter(|a| !a.is_empty());
    // The relay/listen path is the agent host — always an agent-family type.
    // (A non-agent endpoint, e.g. a Gateway, establishes via `api bind --type`,
    // not the live-agent relay loop.) Harness-hosted: the harness owns the process,
    // so there is no broker PTY → not controllable (REQ-PICKER-1).
    establish_perch(
        id,
        &seed.session_id,
        seed.parent_pid,
        adapter,
        subnet,
        "live_agent",
        Some(false),
    )
}

/// TAKE (consume) the seed for `parent_pid` from the daemon's in-memory map,
/// auto-starting the daemon if absent (REQ-DAEMON-3 / REQ-START-3).
fn take_seed_from_daemon(parent_pid: u32) -> Result<Option<Seed>, BindError> {
    spt_daemon::ensure_running().map_err(|e| BindError::Io(format!("daemon unavailable: {e}")))?;
    spt_daemon::take_seed(&spt_daemon::seed_socket_name(), parent_pid)
        .map_err(|e| BindError::Io(e.to_string()))
}

/// Write the perch record (skeleton→live) + ready marker + capability token.
/// Reviving an existing perch keeps its token; a live perch held under a
/// different session id is a conflict (pid-recycle / id collision guard).
///
/// Creation-time fields ride the shared seam
/// ([`spt_store::home::stamp_creation_fields`], REQ-INST-15): a NEW perch
/// assigns its immutable home (refuse-and-qualify on a multi-subnet node) +
/// seeds `sync_subnets = [home]`; a revive **carries forward** home, the
/// adapter (an explicit one wins — resume-under-new), the resource blurb,
/// and the auto-suspend override — a re-bind rewrites `info.json` and must
/// not wipe them.
// [impl->REQ-INST-15]
fn establish_perch(
    id: &str,
    session_id: &str,
    parent_pid: u32,
    adapter: Option<&str>,
    subnet: Option<&str>,
    endpoint_type: &str,
    controllable: Option<bool>,
) -> Result<String, BindError> {
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);

    let prior = info::read_info(&perch_path);
    if let Some(existing) = &prior {
        let owner_alive = info::read_pid(&perch_path)
            .map(proc::is_process_alive)
            .unwrap_or(false);
        if owner_alive && !existing.session_id.is_empty() && existing.session_id != session_id {
            return Err(BindError::Conflict {
                id: id.to_string(),
                held: existing.session_id.clone(),
            });
        }
    }

    std::fs::create_dir_all(&perch_path).map_err(|e| BindError::Io(e.to_string()))?;
    // The endpoint type tag (info.json `state`). Defaults to the agent path
    // (`live_agent`); a non-agent endpoint (e.g. a `gateway` — the open type
    // system, REQ-EP-1/REQ-EP-6) binds with its own tag via `api bind --type`.
    // [impl->REQ-EP-6]
    // Open by construction: any tag round-trips through EndpointType::from_tag,
    // and downstream type-keyed logic (the user-msg identity gate, REQ-MSG-5)
    // reads it back verbatim. A revive keeps the prior type unless overridden.
    let endpoint_type = prior
        .as_ref()
        .map(|p| p.state.clone())
        .filter(|s| endpoint_type == "live_agent" && !s.is_empty())
        .unwrap_or_else(|| endpoint_type.to_string());
    let mut rec = InfoJson::new(
        id,
        &now_stamp(),
        std::process::id(),
        session_id,
        &endpoint_type,
    );
    rec.parent_pid = Some(parent_pid);
    let subnets = spt_store::subnet::SubnetStore::load();
    let mut vis = spt_store::visibility::VisibilityStore::load();
    let dirty = spt_store::home::stamp_creation_fields(
        &mut rec,
        prior.as_ref(),
        &subnets,
        &mut vis,
        subnet,
        adapter,
    )
    .map_err(|e| BindError::Home(e.to_string()))?;
    if dirty {
        let _ = vis.save();
    }
    // Broker-PTY-controllability stamp (REQ-PICKER-1): the caller knows the hosting
    // mode — `listen` is harness-hosted (no broker PTY → Some(false)); `bind` of a
    // live_agent is spt-hosted (broker PTY → Some(true)); a non-live bind is None.
    // Explicit wins; a revive that omits it carries the prior value forward (like


Softened test green (2 pass). Commit the softening (W1 follow-up).


Bash

cd C:/Users/decid/Documents/projects/spt-core && git add crates/spt-daemon/tests/inject_control_wedge.rs && git commit -q -m "test(broker): W1 — cfg(unix) park-(b) is informational, not asserted

doyle ruling: a hard assert!(!inject_acked) on Unix encodes 'forkpty MUST
park' as a gate, but absorbing (no park) is BENIGN — gating on an unverified
hypothesis whose failure mode is good behavior = a false-red waiting to fire.
cfg(unix) now CAPTURES + eprintln's inject_acked; only broker liveness is
asserted. gravity-linux CI is the observation venue; W2 converts it to a real
pre-fix repro assert (and bounds the park) once CI actually shows it.

Co-authored by: todlando
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NVVByRoZnz85acTkVJ1uAP" && git log --oneline -1

Commit (b) assertion softening


warning: in the working copy of 'crates/spt-daemon/tests/inject_control_wedge.rs', LF will be replaced by CRLF the next time Git touches it
bab7d44 test(broker): W1 — cfg(unix) park-(b) is informational, not asserted


W1 fully banked (`bab7d44`). On to **W3** (bind-cwd). Read the rest of `establish_perch` + `cmd_bind`, and check the `InfoJson`/`Seed` cwd fields.


238	    // home/adapter — a re-bind must not wipe it).
239	    // [impl->REQ-PICKER-1]
240	    rec.controllable = controllable.or_else(|| prior.as_ref().and_then(|p| p.controllable));
241	    info::write_info(&perch_path, &rec).map_err(|e| BindError::Io(e.to_string()))?;
242	    std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), "")
243	        .map_err(|e| BindError::Io(e.to_string()))?;
244
245	    // Session ledger (REQ-TERM-6): record this session as active so the digest
246	    // can span a later `/clear`|`/compact` boundary. The boot row dedups against
247	    // a same-session revive; a missing/unstamped session is not ledgered (the
248	    // digest spans only when there is a session to enumerate). Best-effort — a
249	    // ledger problem must never fail the bind.
250	    if !session_id.is_empty() {
251	        let _ = spt_store::sessions::append(
252	            &perch_path,
253	            &spt_store::sessions::SessionEntry {
254	                ts: spt_store::timefmt::now_rfc3339(),
255	                session_id: session_id.to_string(),
256	                trigger: spt_store::sessions::SessionTrigger::Boot,
257	            },
258	        );
259	    }
260
261	    // Keep an existing token across a revive; mint one for a fresh perch.
262	    let token = auth::read_token(&perch_path).unwrap_or_else(auth::mint_token);
263	    auth::write_token(&perch_path, &token).map_err(|e| BindError::Io(e.to_string()))?;
264	    Ok(token)
265	}
266
267	/// `api listen <id>` — consume the seed, bind the perch, then run the relay
268	/// loop (heir to `$LIVE start`'s Monitor-bound poll loop). `--once` drains and
269	/// exits (testability); otherwise blocks streaming messages to stdout.
270	///
271	/// When the manifest is **live-capable** (declares `[session.psyche_init]`), the
272	/// loop also hosts the LiveAgent lifecycle over the M2b `spt-live` seams: sweep
273	/// stale signoff on start (3.2), spawn the Psyche (T2), and run a pulse tick
274	/// each cycle (drop ingest + echo fires — T3/T4/T5). The graceful signoff (T6)
275	/// is driven by the shutdown path; M3's daemon re-hosts all of it.
276	// [impl->REQ-START-2]
277	// [impl->REQ-SEAM-PSYCHE]
278	pub fn cmd_listen(
279	    id: &str,
280	    parent_pid_override: Option<u32>,
281	    once: bool,
282	    adapter: Option<&str>,
283	    manifest: Option<&spt_runtime::Manifest>,
284	    subnet: Option<&str>,
285	) -> i32 {
286	    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
287	    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
288	        eprintln!("INVALID_ID:{id}: {e}");
289	        return EXIT_REFUSED;
290	    }
291	    let parent_pid = match parent_pid_override.or_else(proc::parent_pid) {
292	        Some(p) => p,
293	        None => {
294	            eprintln!("NO_PARENT_PID: could not resolve the parent pid anchor");
295	            return EXIT_REFUSED;
296	        }
297	    };
298
299	    // Bind-time adapter/profile resolution (ADR-0021 / REQ-START-5): an explicit
300	    // `--adapter` is the override; omitted, resolve the owning adapter from the
301	    // seed's parent pid (exe basename → host_binaries candidates → active-profile
302	    // pointer / registered_at_ms). A zero-match is a friendly refusal naming the
303	    // binary + the `--adapter` escape — never a silent bind without an adapter
304	    // (which would drop the Psyche, the very /sptc:live bug class).
305	    // [impl->REQ-START-5]
306	    let adapters_dir = spt_store::perch::adapters_dir();
307	    let adapter_option: Option<String> = match adapter {
308	        Some(a) => Some(a.to_string()),
309	        None => match spt_runtime::resolve::resolve_bind_adapter(&adapters_dir, parent_pid) {
310	            Ok(opt) => Some(opt),
311	            Err(e) => {
312	                eprintln!("ADAPTER_UNRESOLVED:{id}: {e}");
313	                return EXIT_REFUSED;
314	            }
315	        },
316	    };
317	    // The effective manifest: an explicit one (resolved in `run` from
318	    // `--adapter`/`--manifest`), else the resolved adapter's registered manifest —
319	    // the source of `live_capable` (psyche_init) + endpoint-native notif render.
320	    let resolved_manifest: Option<spt_runtime::Manifest> = match manifest {
321	        Some(m) => Some(m.clone()),
322	        None => adapter_option.as_deref().and_then(|opt| {
323	            spt_runtime::registry::resolve_option(&adapters_dir, opt)
324	                .ok()
325	                .map(|(_, m)| m)
326	        }),
327	    };

    // home/adapter — a re-bind must not wipe it).
    // [impl->REQ-PICKER-1]
    rec.controllable = controllable.or_else(|| prior.as_ref().and_then(|p| p.controllable));
    info::write_info(&perch_path, &rec).map_err(|e| BindError::Io(e.to_string()))?;
    std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), "")
        .map_err(|e| BindError::Io(e.to_string()))?;

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

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

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

    // Bind-time adapter/profile resolution (ADR-0021 / REQ-START-5): an explicit
    // `--adapter` is the override; omitted, resolve the owning adapter from the
    // seed's parent pid (exe basename → host_binaries candidates → active-profile
    // pointer / registered_at_ms). A zero-match is a friendly refusal naming the
    // binary + the `--adapter` escape — never a silent bind without an adapter
    // (which would drop the Psyche, the very /sptc:live bug class).
    // [impl->REQ-START-5]
    let adapters_dir = spt_store::perch::adapters_dir();
    let adapter_option: Option<String> = match adapter {
        Some(a) => Some(a.to_string()),
        None => match spt_runtime::resolve::resolve_bind_adapter(&adapters_dir, parent_pid) {
            Ok(opt) => Some(opt),
            Err(e) => {
                eprintln!("ADAPTER_UNRESOLVED:{id}: {e}");
                return EXIT_REFUSED;
            }
        },
    };
    // The effective manifest: an explicit one (resolved in `run` from
    // `--adapter`/`--manifest`), else the resolved adapter's registered manifest —
    // the source of `live_capable` (psyche_init) + endpoint-native notif render.
    let resolved_manifest: Option<spt_runtime::Manifest> = match manifest {
        Some(m) => Some(m.clone()),
        None => adapter_option.as_deref().and_then(|opt| {
            spt_runtime::registry::resolve_option(&adapters_dir, opt)
                .ok()
                .map(|(_, m)| m)
        }),
    };


400	    0
401	}
402
403	/// `api bind <id>` — post-spawn boot bind (REQ-SEAM-POSTSPAWN). Establishes the
404	/// live perch on first contact, or flips an existing skeleton, keyed to the
405	/// session id the binary reports. Auth is intrinsic: an existing *live* perch
406	/// under a different session id is refused (you can only bind your own).
407	// [impl->REQ-SEAM-POSTSPAWN]
408	pub fn cmd_bind(
409	    id: &str,
410	    set_session_id: Option<String>,
411	    proof_session: Option<String>,
412	    adapter: Option<&str>,
413	    subnet: Option<&str>,
414	    endpoint_type: &str,
415	    manifest: Option<&spt_runtime::Manifest>,
416	) -> i32 {
417	    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
418	    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
419	        eprintln!("INVALID_ID:{id}: {e}");
420	        return EXIT_REFUSED;
421	    }
422	    let session_id = set_session_id
423	        .or(proof_session)
424	        .or_else(|| std::env::var("OWL_SESSION_ID").ok())
425	        .filter(|s| !s.is_empty());
426	    let Some(session_id) = session_id else {
427	        eprintln!("USAGE: bind needs --set-session-id (or $OWL_SESSION_ID)");
428	        return EXIT_REFUSED;
429	    };
430	    // The session's own anchor: the process that fired this hook (the session).
431	    let parent_pid = proc::parent_pid().unwrap_or_else(std::process::id);
432	    // spt-hosted: the daemon spawned a live_agent into a broker-owned PTY → it IS
433	    // broker-PTY-controllable (REQ-PICKER-1). A non-live bind (a gateway/shell — not
434	    // a PTY-attachable agent) stamps None; the picker's type-gated derivation never
435	    // amber-paints it regardless.
436	    let controllable = if endpoint_type == "live_agent" {
437	        Some(true)
438	    } else {
439	        None
440	    };
441	    match establish_perch(
442	        id,
443	        &session_id,
444	        parent_pid,
445	        adapter,
446	        subnet,
447	        endpoint_type,
448	        controllable,
449	    ) {
450	        Ok(token) => {
451	            eprintln!("BOUND:{id} token={token}");
452	            // Live-agent FIRST-HOST handoff for the spt-hosted (bind, no listen)
453	            // topology (M11-W0.3, REQ-DAEMON-1) — the mirror of W0.2's cmd_listen
454	            // mark. A live_agent endpoint whose manifest declares
455	            // `[session.psyche_init]` is marked status=online at establish; the
456	            // brain's livehost reconcile then hosts the Psyche (the SAME single host
457	            // path — bind never spawns/hosts/calls livehost). Closes the
458	            // bind-no-Psyche gap: the Psyche spawn was previously cmd_listen-only, so
459	            // a bind-only spt-hosted live agent never got one (spt-hosted uses direct
460	            // PTY, no relay — CONTEXT:39). Gated on state==live_agent ∧ psyche_init so
461	            // the establish-online signal == the reconcile host predicate (no
462	            // online-but-unhostable perch). status is daemon-authoritative (CONTEXT:194).
463	            // [impl->REQ-DAEMON-1]
464	            let live_capable = endpoint_type == "live_agent"
465	                && manifest.is_some_and(|m| m.session.psyche_init.is_some());
466	            if live_capable {
467	                let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
468	                if let Err(e) = info::set_status(&perch_path, spt_store::liveness::STATUS_ONLINE) {
469	                    eprintln!("LIVE_ONLINE_FAIL:{id}: {e}");
470	                }
471	            }
472	            // A post-spawn bind is a new-session-start — a reported resurface
473	            // boundary (ADR-0007/D8b). Best-effort.
474	            super::reporting::resurface_notifs(id);
475	            0
476	        }
477	        Err(e) => {
478	            report_bind_error(&e);
479	            EXIT_REFUSED
480	        }
481	    }
482	}
483
484	fn report_bind_error(e: &BindError) {
485	    match e {
486	        BindError::NoSeed(p) => eprintln!("NO_SEED:{p} (no seed for this parent pid)"),
487	        BindError::StaleSeed(p) => eprintln!("STALE_SEED:{p} (anchor process is dead)"),
488	        BindError::EmptySession => eprintln!("EMPTY_SESSION: seed carried no session id"),
489	        BindError::Conflict { id, held } => {

    0
}

/// `api bind <id>` — post-spawn boot bind (REQ-SEAM-POSTSPAWN). Establishes the
/// live perch on first contact, or flips an existing skeleton, keyed to the
/// session id the binary reports. Auth is intrinsic: an existing *live* perch
/// under a different session id is refused (you can only bind your own).
// [impl->REQ-SEAM-POSTSPAWN]
pub fn cmd_bind(
    id: &str,
    set_session_id: Option<String>,
    proof_session: Option<String>,
    adapter: Option<&str>,
    subnet: Option<&str>,
    endpoint_type: &str,
    manifest: Option<&spt_runtime::Manifest>,
) -> i32 {
    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
        eprintln!("INVALID_ID:{id}: {e}");
        return EXIT_REFUSED;
    }
    let session_id = set_session_id
        .or(proof_session)
        .or_else(|| std::env::var("OWL_SESSION_ID").ok())
        .filter(|s| !s.is_empty());
    let Some(session_id) = session_id else {
        eprintln!("USAGE: bind needs --set-session-id (or $OWL_SESSION_ID)");
        return EXIT_REFUSED;
    };
    // The session's own anchor: the process that fired this hook (the session).
    let parent_pid = proc::parent_pid().unwrap_or_else(std::process::id);
    // spt-hosted: the daemon spawned a live_agent into a broker-owned PTY → it IS
    // broker-PTY-controllable (REQ-PICKER-1). A non-live bind (a gateway/shell — not
    // a PTY-attachable agent) stamps None; the picker's type-gated derivation never
    // amber-paints it regardless.
    let controllable = if endpoint_type == "live_agent" {
        Some(true)
    } else {
        None
    };
    match establish_perch(
        id,
        &session_id,
        parent_pid,
        adapter,
        subnet,
        endpoint_type,
        controllable,
    ) {
        Ok(token) => {
            eprintln!("BOUND:{id} token={token}");
            // Live-agent FIRST-HOST handoff for the spt-hosted (bind, no listen)
            // topology (M11-W0.3, REQ-DAEMON-1) — the mirror of W0.2's cmd_listen
            // mark. A live_agent endpoint whose manifest declares
            // `[session.psyche_init]` is marked status=online at establish; the
            // brain's livehost reconcile then hosts the Psyche (the SAME single host
            // path — bind never spawns/hosts/calls livehost). Closes the
            // bind-no-Psyche gap: the Psyche spawn was previously cmd_listen-only, so
            // a bind-only spt-hosted live agent never got one (spt-hosted uses direct
            // PTY, no relay — CONTEXT:39). Gated on state==live_agent ∧ psyche_init so
            // the establish-online signal == the reconcile host predicate (no
            // online-but-unhostable perch). status is daemon-authoritative (CONTEXT:194).
            // [impl->REQ-DAEMON-1]
            let live_capable = endpoint_type == "live_agent"
                && manifest.is_some_and(|m| m.session.psyche_init.is_some());
            if live_capable {
                let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
                if let Err(e) = info::set_status(&perch_path, spt_store::liveness::STATUS_ONLINE) {
                    eprintln!("LIVE_ONLINE_FAIL:{id}: {e}");
                }
            }
            // A post-spawn bind is a new-session-start — a reported resurface
            // boundary (ADR-0007/D8b). Best-effort.
            super::reporting::resurface_notifs(id);
            0
        }
        Err(e) => {
            report_bind_error(&e);
            EXIT_REFUSED
        }
    }
}

fn report_bind_error(e: &BindError) {
    match e {
        BindError::NoSeed(p) => eprintln!("NO_SEED:{p} (no seed for this parent pid)"),
        BindError::StaleSeed(p) => eprintln!("STALE_SEED:{p} (anchor process is dead)"),
        BindError::EmptySession => eprintln!("EMPTY_SESSION: seed carried no session id"),
        BindError::Conflict { id, held } => {


Read the bind path. Now check `InfoJson.cwd` + `Seed.cwd` + the project-grouping that consumes cwd.


Grep

cwd|pub struct InfoJson|pub struct Seed|merge_origin_project|fn project


62:pub struct InfoJson {
77:    pub cwd: Option<String>,
189:            cwd: None,
401:        info.cwd = Some("C:/repo/spt-core".to_string());
428:            cwd: None,
826:                // Vary length (long cwd) so a torn read would be detectable.
828:                info.cwd = Some("x".repeat((n % 500) as usize));


crates\spt-daemon\src\broker.rs:1066:        // REQ-HOST-RUN-2: spawn in the requested project cwd when carried;
crates\spt-daemon\src\broker.rs:1067:        // `None` inherits the broker's cwd (legacy/test + W1 bringup shortcut).
crates\spt-daemon\src\broker.rs:1074:            req.cwd.as_deref(),
crates\spt-daemon\src\applyhost.rs:575:            cwd: None,
crates\spt-daemon\src\daemon.rs:724:            cwd: *const u16,
crates\spt-daemon\src\deelevate.rs:474:            cwd: *const u16,
crates\spt-daemon\src\msg.rs:192:    /// inherits the broker's cwd — the legacy/test behavior and the W1 bringup
crates\spt-daemon\src\msg.rs:193:    /// shortcut. `Some(dir)` lands the child in a PROJECT-scoped cwd: the
crates\spt-daemon\src\msg.rs:195:    /// source relative to cwd, so a project-scoped launcher MUST place the
crates\spt-daemon\src\msg.rs:197:    /// serde-defaulted: an N-1 brain/broker omits it and gets the inherited cwd.
crates\spt-daemon\src\msg.rs:199:    pub cwd: Option<String>,
crates\spt-daemon\src\seedmap.rs:65:pub struct SeedResult {
crates\spt-daemon\src\seedmap.rs:73:pub struct SeedRegistry {
crates\spt-daemon\src\seedmap.rs:333:            cwd: None,
crates\spt-daemon\src\livehost.rs:766:             [session.psyche_init]\ncommand='{NOOP_CMD}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
crates\spt-daemon\src\livehost.rs:1106:                    cwd: None,
crates\spt-daemon\src\harnesshost.rs:138:/// `cwd`: the project-scoped working directory the harness PTY lands in
crates\spt-daemon\src\harnesshost.rs:139:/// (REQ-HOST-RUN-2). `None` inherits the broker's cwd (the W1 bringup shortcut);
crates\spt-daemon\src\harnesshost.rs:150:    cwd: Option<&str>,
crates\spt-daemon\src\harnesshost.rs:164:            cwd: cwd.map(str::to_string),
crates\spt-daemon\src\lifecycle.rs:67:    /// is the project root — falling back to the daemon's cwd for a manifest
crates\spt-daemon\src\lifecycle.rs:109:        // the commune drop dir's parent is the project root; cwd is the
crates\spt-daemon\src\lifecycle.rs:612:             [session.psyche_init]\ncommand='{NOOP_CMD}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
crates\spt-daemon\src\lifecycle.rs:632:             [session.psyche_init]\ncommand='{NOOP_CMD}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n\n\
crates\spt-daemon\src\shellhost.rs:233:            // Shells inherit the broker's cwd (unchanged); project-scoped cwd is
crates\spt-daemon\src\shellhost.rs:235:            cwd: None,


`InfoJson.cwd: Option<String>` exists (info.rs:77). Find the `Seed` struct + its cwd field, and how the picker derives the project tab from cwd.


<tool_use_error>Path does not exist: C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\seed.rs. Note: your current working directory is C:\Users\decid\Documents\projects\spt-core.</tool_use_error>

Error: Path does not exist: C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\seed.rs. Note: your current working directory is C:\Users\decid\Documents\projects\spt-core.


crates\spt-store\src\seed.rs:32:pub struct Seed {


1	//! The ephemeral harness-hosted startup [`Seed`] — the api-bridging data the
2	//! harness hands spt-core at `SessionStart`. A `SessionStart`-equivalent hook
3	//! fires `api seed --pid <parent_pid> --session-id <sid>`; a later
4	//! `api listen <id>` consumes it (matched by the listener's *own* parent pid),
5	//! then binds the perch.
6	//!
7	//! **The transport is the daemon's in-memory map, not a file (M3b / B6).** The
8	//! interim no-daemon model (M1/M2a) bridged the two `api` invocations through a
9	//! seed *file* under `<spt_home>/seeds/`; M3b retires that file bridge
10	//! (REQ-START-3). The seed now lives in the per-machine daemon's memory —
11	//! `api seed` PUTs it and `api listen` TAKEs it over the seed-control IPC
12	//! ([`spt_daemon::seedmap`]). This module keeps only the **shared data
13	//! contract** ([`Seed`]); the put/take/consume mechanics live in the daemon.
14	//! No seed file is ever written.
15	//
16	// [impl->REQ-START-1]
17
18	use serde::{Deserialize, Serialize};
19
20	/// One ephemeral seed entry: the session details the spt-hosted topology would
21	/// share directly, minus the not-yet-chosen `live_id`. The harness fills it at
22	/// `api seed`; the daemon holds it in memory keyed by [`Seed::parent_pid`] until
23	/// `api listen` consumes it.
24	///
25	/// **Adapter-agnostic** (ADR-0021 / REQ-START-5): the seed carries *no*
26	/// adapter_name — it is a pure "a harness session exists at this pid" record.
27	/// *Which* adapter/profile owns the session is resolved at bind, as a read
28	/// against the live registry (`parent_pid` → exe basename → `host_binaries`
29	/// candidates → active-profile pointer / `registered_at_ms`), never a seed-time
30	/// snapshot that could drift past a later `adapter add`.
31	#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32	pub struct Seed {
33	    /// The stable session-binding anchor (the harness process pid).
34	    pub parent_pid: u32,
35	    /// The harness session id — the pid-recycle discriminator (5.1).
36	    pub session_id: String,
37	    /// Optional working directory the session runs in.
38	    #[serde(skip_serializing_if = "Option::is_none")]
39	    pub cwd: Option<String>,
40	    /// Creation stamp (opaque producer string).
41	    pub created: String,
42	}
43
44	#[cfg(test)]
45	mod tests {
46	    use super::*;
47
48	    // [unit->REQ-START-1] the seed data contract round-trips through serde — the
49	    // shape the harness writes at `api seed` and the daemon hands back at
50	    // `api listen` is stable across the JSON the seed-control IPC carries.
51	    #[test]
52	    fn seed_round_trips_through_json() {
53	        let seed = Seed {
54	            parent_pid: 4321,
55	            session_id: "sid-xyz".into(),
56	            cwd: Some("C:/repo".into()),
57	            created: "1700000000".into(),
58	        };
59	        let json = serde_json::to_string(&seed).unwrap();
60	        assert_eq!(serde_json::from_str::<Seed>(&json).unwrap(), seed);
61	    }
62
63	    // [unit->REQ-START-1] an absent cwd is omitted on the wire (skip_serializing)
64	    // and decodes back to None — the optional working dir stays optional.
65	    #[test]
66	    fn absent_cwd_is_omitted() {
67	        let seed = Seed {
68	            parent_pid: 999,
69	            session_id: "s".into(),
70	            cwd: None,
71	            created: "0".into(),
72	        };
73	        let json = serde_json::to_string(&seed).unwrap();
74	        assert!(!json.contains("cwd"), "None cwd is skipped: {json}");
75	        assert_eq!(serde_json::from_str::<Seed>(&json).unwrap(), seed);
76	    }
77	}
78

//! The ephemeral harness-hosted startup [`Seed`] — the api-bridging data the
//! harness hands spt-core at `SessionStart`. A `SessionStart`-equivalent hook
//! fires `api seed --pid <parent_pid> --session-id <sid>`; a later
//! `api listen <id>` consumes it (matched by the listener's *own* parent pid),
//! then binds the perch.
//!
//! **The transport is the daemon's in-memory map, not a file (M3b / B6).** The
//! interim no-daemon model (M1/M2a) bridged the two `api` invocations through a
//! seed *file* under `<spt_home>/seeds/`; M3b retires that file bridge
//! (REQ-START-3). The seed now lives in the per-machine daemon's memory —
//! `api seed` PUTs it and `api listen` TAKEs it over the seed-control IPC
//! ([`spt_daemon::seedmap`]). This module keeps only the **shared data
//! contract** ([`Seed`]); the put/take/consume mechanics live in the daemon.
//! No seed file is ever written.
//
// [impl->REQ-START-1]

use serde::{Deserialize, Serialize};

/// One ephemeral seed entry: the session details the spt-hosted topology would
/// share directly, minus the not-yet-chosen `live_id`. The harness fills it at
/// `api seed`; the daemon holds it in memory keyed by [`Seed::parent_pid`] until
/// `api listen` consumes it.
///
/// **Adapter-agnostic** (ADR-0021 / REQ-START-5): the seed carries *no*
/// adapter_name — it is a pure "a harness session exists at this pid" record.
/// *Which* adapter/profile owns the session is resolved at bind, as a read
/// against the live registry (`parent_pid` → exe basename → `host_binaries`
/// candidates → active-profile pointer / `registered_at_ms`), never a seed-time
/// snapshot that could drift past a later `adapter add`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Seed {
    /// The stable session-binding anchor (the harness process pid).
    pub parent_pid: u32,
    /// The harness session id — the pid-recycle discriminator (5.1).
    pub session_id: String,
    /// Optional working directory the session runs in.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cwd: Option<String>,
    /// Creation stamp (opaque producer string).
    pub created: String,
}

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

    // [unit->REQ-START-1] the seed data contract round-trips through serde — the
    // shape the harness writes at `api seed` and the daemon hands back at
    // `api listen` is stable across the JSON the seed-control IPC carries.
    #[test]
    fn seed_round_trips_through_json() {
        let seed = Seed {
            parent_pid: 4321,
            session_id: "sid-xyz".into(),
            cwd: Some("C:/repo".into()),
            created: "1700000000".into(),
        };
        let json = serde_json::to_string(&seed).unwrap();
        assert_eq!(serde_json::from_str::<Seed>(&json).unwrap(), seed);
    }

    // [unit->REQ-START-1] an absent cwd is omitted on the wire (skip_serializing)
    // and decodes back to None — the optional working dir stays optional.
    #[test]
    fn absent_cwd_is_omitted() {
        let seed = Seed {
            parent_pid: 999,
            session_id: "s".into(),
            cwd: None,
            created: "0".into(),
        };
        let json = serde_json::to_string(&seed).unwrap();
        assert!(!json.contains("cwd"), "None cwd is skipped: {json}");
        assert_eq!(serde_json::from_str::<Seed>(&json).unwrap(), seed);
    }
}


`Seed.cwd` exists (captured at `api seed`, discarded by `bind_from_seed`). Find all `establish_perch` callers + the project-from-cwd consumer.


crates\spt-daemon\tests\attach.rs:610:        .expect("spawn cwd-printing child in the project cwd");
crates\spt-daemon\tests\attach.rs:619:        "the child's cwd must be the requested project dir (leaf {leaf:?}), got: {:?}",
crates\spt\src\cli.rs:1305:    // daemon's cwd. A project-scoped harness (Claude Code) reads its `.claude`,
crates\spt\src\api\startup.rs:144:    establish_perch(
crates\spt\src\api\startup.rs:175:fn establish_perch(
crates\spt\src\api\startup.rs:441:    match establish_perch(
crates\spt\src\picker\model.rs:168:    /// Endpoints scoped to the cwd project.
crates\spt\src\picker\model.rs:177:    /// Display label (the project category carries the cwd project name).
crates\spt\src\picker\model.rs:350:    /// The cwd project name (the Project category label).
crates\spt\src\picker\model.rs:522:        // Project = local endpoints whose history names the cwd project; Local =
crates\spt\src\picker\data.rs:18:/// The cwd project name — the Project category label + the project-history match
crates\spt\src\picker\data.rs:138:                .map(|cwd| project::project_id_for_dir(Path::new(cwd)));
crates\spt\src\picker\data.rs:140:                merge_origin_project(project_history_for(&p.id), origin_project);
crates\spt\src\picker\data.rs:226:/// the cwd-derived `project_id` (`None` when the perch has no recorded/readable
crates\spt\src\picker\data.rs:227:/// cwd). A fresh endpoint with no committed context still belongs to the project
crates\spt\src\picker\data.rs:234:fn merge_origin_project(mut history: Vec<String>, origin: Option<String>) -> Vec<String> {
crates\spt\src\picker\data.rs:362:        assert_eq!(merge_origin_project(vec![], Some("x".into())), vec!["x"]);
crates\spt\src\picker\data.rs:365:            merge_origin_project(vec!["y".into()], Some("x".into())),
crates\spt\src\picker\data.rs:368:        // 2b. cwd==committed project ⇒ deduped, not doubled.
crates\spt\src\picker\data.rs:369:        assert_eq!(merge_origin_project(vec!["x".into()], Some("x".into())), vec!["x"]);
crates\spt\src\picker\data.rs:370:        // 3. cwd=X only ⇒ membership is [X]; a merely-viewed project Z is NOT a
crates\spt\src\picker\data.rs:372:        assert!(!merge_origin_project(vec![], Some("x".into())).contains(&"z".to_string()));
crates\spt\src\picker\data.rs:374:        assert_eq!(merge_origin_project(vec!["y".into()], None), vec!["y"]);
crates\spt\src\picker\data.rs:377:            merge_origin_project(vec![], Some(String::new())),
crates\spt-daemon\src\msg.rs:195:    /// source relative to cwd, so a project-scoped launcher MUST place the
crates\spt-daemon\src\harnesshost.rs:138:/// `cwd`: the project-scoped working directory the harness PTY lands in
crates\spt-daemon\src\shellhost.rs:233:            // Shells inherit the broker's cwd (unchanged); project-scoped cwd is


Consumer confirmed: picker/data.rs:138 reads `p.cwd` → `project_id_for_dir`. Fix = set `info.cwd` at both bind callers. Check `project_id_for_dir` + how `api seed` captures cwd (to match format).


Bash

cd C:/Users/decid/Documents/projects/spt-core && grep -rn "fn project_id_for_dir" crates/ && echo "---SEED CWD CAPTURE---" && grep -rn "cwd" crates/spt/src/cli.rs | grep -i seed; grep -rn "Seed {" crates/spt/src crates/spt-daemon/src | head; echo "---api seed builds cwd---" && grep -rn "current_dir\|cwd:" crates/spt/src/api/*.rs | head -20

Find project_id_for_dir + seed cwd capture format


<owl_messages>
<owl_message from="doyle" priority="high">W7 BYTE CONFIRMED — operator capture (real WT, target\debug\spt.exe):
Backspace = 0x08 (^H). Ctrl+Backspace = 0x7f (DEL). (Full: "20 74 68 65 72 65 20 79 6f 75 | 08 | 7f | 02 64" = " there you", BS, ctrl+BS, ctrl-b d.)

ROOT PROVEN: it's the INVERTED-from-VT mapping = the ENABLE_VIRTUAL_TERMINAL_INPUT-OFF signature. Legacy console emits 0x08 for Backspace; Claude Code maps ^H → backward-kill-word = the whole-word delete. VT standard is the opposite (Backspace=0x7f, Ctrl+Backspace=0x08).

FIX = (a) enable ENABLE_VIRTUAL_TERMINAL_INPUT on the rc stdin console (Windows), NOT the 0x08→0x7f normalize. Decisive reason: under VT input Backspace→0x7f (char-delete, correct) AND Ctrl+Backspace→0x08 (CC kill-word, correct) — both keys keep distinct correct bytes. A blind normalize 0x08→0x7f would CONFLATE them (Ctrl+Backspace currently 0x7f → both become char-delete, kill-word lost). Plus VT input fixes the whole key map (arrows/Home/End → proper CSI), not just Backspace.

IMPL NOTES for the fix:
- Save the original console input mode + RESTORE on exit (mirror RawGuard's enable/disable_raw_mode). Set ENABLE_VIRTUAL_TERMINAL_INPUT on stdin's handle alongside raw mode.
- Windows-only (cfg(windows)); Unix passthrough unchanged (already VT).
- Verify post-fix on the real terminal: Backspace→0x7f, Ctrl+Backspace→0x08, ctrl-b (0x02) detach STILL works (control byte, unaffected by VT input), arrows now arrive as ESC[ sequences forwarded verbatim.
- Activate REQ-HAZARD-RC-INPUT-KEY-ENCODING [impl,unit] + KNOWN-HAZARDS.md entry. unit can drive the mode-bit decision seam purely; the keypress itself stays HITL operator-confirm (re-capture post-fix).

Land it. Ping me + I'll have the operator re-capture to confirm the flip.</owl_message>
</owl_messages>

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


crates/spt-store/src/project.rs:64:pub fn project_id_for_dir(dir: &Path) -> String {
---SEED CWD CAPTURE---
crates/spt/src/api/mod.rs:57:    Seed {
crates/spt/src/api/mod.rs:336:        ApiCmd::Seed { pid, session_id } => startup::cmd_seed(pid, session_id),
crates/spt/src/api/startup.rs:44:    let seed = Seed {
crates/spt/src/api/startup.rs:551:        reg.put(Seed {
crates/spt-daemon/src/seedmap.rs:329:    fn a_seed(pid: u32, sid: &str) -> Seed {
crates/spt-daemon/src/seedmap.rs:330:        Seed {
---api seed builds cwd---
crates/spt/src/api/startup.rs:47:        cwd: std::env::current_dir()
crates/spt/src/api/startup.rs:554:            cwd: None,


W7 byte confirmed: Backspace=`0x08`, Ctrl+Backspace=`0x7f` — inverted-from-VT = `ENABLE_VIRTUAL_TERMINAL_INPUT` OFF. Fix = enable VT input (not normalize). Acking.


$OWL send doyle <<'EOF'
ACK W7 byte — Backspace=0x08, Ctrl+Backspace=0x7f = the VT-input-OFF signature, exactly your code-traced root. Agreed on (a) enable ENABLE_VIRTUAL_TERMINAL_INPUT, NOT normalize — a blind 0x08→0x7f conflates Backspace and Ctrl+Backspace (both → char-delete, kill-word lost) and misses arrows/Home/End. VT input gives both keys distinct correct bytes + fixes the whole CSI key map.

Landing now per your impl notes:
- Save original console input mode, set ENABLE_VIRTUAL_TERMINAL_INPUT on stdin alongside raw mode, RESTORE on exit (mirror the RawGuard enable/disable pattern). cfg(windows) only; Unix passthrough unchanged.
- ctrl-b (0x02) detach unaffected (control byte). Keep the SPT_RC_DEBUG_KEYS hexdump for the re-capture.
- Activate REQ-HAZARD-RC-INPUT-KEY-ENCODING [impl,unit]; unit drives the mode-bit decision seam purely (HITL keypress = your operator re-capture). KNOWN-HAZARDS.md entry on landing.

Pausing W3 mid-flight (bind-cwd, no commit yet) to land W7 first since the operator's in the re-capture loop. Will ping you when W7 is up → you trigger the re-capture; then I finish W3 + W5.
EOF

Ack doyle W7 byte + fix plan


➜ SENT:doyle


W7 fix. Read rc.rs raw-mode setup (RawGuard / enable_raw_mode) to mirror the enable/restore pattern.


72:struct RawGuard(bool);
73:impl RawGuard {
75:        RawGuard(crossterm::terminal::enable_raw_mode().is_ok())
78:impl Drop for RawGuard {
81:            let _ = crossterm::terminal::disable_raw_mode();
192:pub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {
263:    let _raw = RawGuard::enable();
275:        if let Ok((cols, rows)) = crossterm::terminal::size() {
432:    // hooking SIGWINCH / the crossterm event loop, which would conflict with the
446:            if let Ok((cols, rows)) = crossterm::terminal::size() {
515:                            match crossterm::terminal::size() {


68	    }
69	}
70
71	/// Restore the terminal out of raw mode no matter how the pump returns.
72	struct RawGuard(bool);
73	impl RawGuard {
74	    fn enable() -> Self {
75	        RawGuard(crossterm::terminal::enable_raw_mode().is_ok())
76	    }
77	}
78	impl Drop for RawGuard {
79	    fn drop(&mut self) {
80	        if self.0 {
81	            let _ = crossterm::terminal::disable_raw_mode();
82	        }
83	    }
84	}
85
86	/// Resolve a live endpoint id to its broker session id via the broker's hosted
87	/// session table (the spawn's endpoint label, brain.sessions()). `None` ⇒ no
88	/// live session under that endpoint.
89	fn resolve_session(brain: &mut Brain, endpoint_id: &str) -> std::io::Result<Option<u64>> {
90	    let reply = brain.sessions()?;
91	    Ok(reply
92	        .sessions
93	        .into_iter()
94	        .find(|s| s.endpoint == endpoint_id)
95	        .map(|s| s.session_id))
96	}
97

    }
}

/// Restore the terminal out of raw mode no matter how the pump returns.
struct RawGuard(bool);
impl RawGuard {
    fn enable() -> Self {
        RawGuard(crossterm::terminal::enable_raw_mode().is_ok())
    }
}
impl Drop for RawGuard {
    fn drop(&mut self) {
        if self.0 {
            let _ = crossterm::terminal::disable_raw_mode();
        }
    }
}

/// Resolve a live endpoint id to its broker session id via the broker's hosted
/// session table (the spawn's endpoint label, brain.sessions()). `None` ⇒ no
/// live session under that endpoint.
fn resolve_session(brain: &mut Brain, endpoint_id: &str) -> std::io::Result<Option<u64>> {
    let reply = brain.sessions()?;
    Ok(reply
        .sessions
        .into_iter()
        .find(|s| s.endpoint == endpoint_id)
        .map(|s| s.session_id))
}


VT-input root proven. Implement W7: extend `RawGuard` to enable `ENABLE_VIRTUAL_TERMINAL_INPUT` on Windows stdin (with save/restore), plus a pure `with_vt_input` decision seam.


Edit

/// Restore the terminal out of raw mode no matter how the pump returns.
struct RawGuard(bool);
impl RawGuard {
    fn enable() -> Self {
        RawGuard(crossterm::terminal::enable_raw_mode().is_ok())
    }
}
impl Drop for RawGuard {
    fn drop(&mut self) {
        if self.0 {
            let _ = crossterm::terminal::disable_raw_mode();
        }
    }
}

/// The Windows console-input flag that makes the legacy console deliver VT key
/// encodings (REQ-HAZARD-RC-INPUT-KEY-ENCODING). windows-sys gates this const
/// behind a feature we don't pull, so define the documented value locally.
#[cfg(windows)]
const ENABLE_VIRTUAL_TERMINAL_INPUT: u32 = 0x0200;

/// Pure decision seam (W7 / REQ-HAZARD-RC-INPUT-KEY-ENCODING): OR the
/// VT-input bit into a console input mode, leaving every other flag untouched.
///
/// Without it, Windows crossterm raw mode leaves `ENABLE_VIRTUAL_TERMINAL_INPUT`
/// OFF, so the LEGACY console delivers `^H` (0x08) for Backspace — and Claude
/// Code maps `^H` → backward-kill-word, i.e. the operator's whole-word delete.
/// With it, Backspace arrives as VT DEL (0x7f, char-delete) and Ctrl+Backspace
/// as 0x08 (CC kill-word) — distinct, correct bytes — and arrows/Home/End arrive
/// as CSI sequences. Idempotent + pure so it unit-tests without a console.
// [impl->REQ-HAZARD-RC-INPUT-KEY-ENCODING]
#[cfg(windows)]
fn with_vt_input(mode: u32) -> u32 {
    mode | ENABLE_VIRTUAL_TERMINAL_INPUT
}

/// Set `ENABLE_VIRTUAL_TERMINAL_INPUT` on the stdin console (on top of the raw
/// mode crossterm just applied) and return the prior mode to restore on exit.
/// `None` if there is no console / the calls fail (best-effort, like raw mode).
// [impl->REQ-HAZARD-RC-INPUT-KEY-ENCODING]
#[cfg(windows)]
fn enable_vt_input() -> Option<u32> {
    use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
    use windows_sys::Win32::System::Console::{
        GetConsoleMode, GetStdHandle, SetConsoleMode, STD_INPUT_HANDLE,
    };
    unsafe {
        let h = GetStdHandle(STD_INPUT_HANDLE);
        if h.is_null() || h == INVALID_HANDLE_VALUE {
            return None;
        }
        let mut mode: u32 = 0;
        if GetConsoleMode(h, &mut mode) == 0 {
            return None;
        }
        let want = with_vt_input(mode);
        if want != mode && SetConsoleMode(h, want) == 0 {
            return None;
        }
        Some(mode)
    }
}

/// Restore a saved stdin console input mode (W7 exit path).
#[cfg(windows)]
fn restore_in_mode(prev: u32) {
    use windows_sys::Win32::System::Console::{GetStdHandle, SetConsoleMode, STD_INPUT_HANDLE};
    unsafe {
        let h = GetStdHandle(STD_INPUT_HANDLE);
        if !h.is_null() {
            let _ = SetConsoleMode(h, prev);
        }
    }
}

/// Restore the terminal out of raw mode no matter how the pump returns. On
/// Windows it ALSO enables `ENABLE_VIRTUAL_TERMINAL_INPUT` for the attach so the
/// hosted TUI gets a VT key map (W7), restoring the prior console mode on drop.
struct RawGuard {
    raw_ok: bool,
    /// The pre-VT stdin console mode to restore on exit (Windows only).
    #[cfg(windows)]
    prev_in_mode: Option<u32>,
}
impl RawGuard {
    fn enable() -> Self {
        let raw_ok = crossterm::terminal::enable_raw_mode().is_ok();
        #[cfg(windows)]
        let prev_in_mode = if raw_ok { enable_vt_input() } else { None };
        RawGuard {
            raw_ok,
            #[cfg(windows)]
            prev_in_mode,
        }
    }
}
impl Drop for RawGuard {
    fn drop(&mut self) {
        #[cfg(windows)]
        if let Some(prev) = self.prev_in_mode {
            restore_in_mode(prev);
        }
        if self.raw_ok {
            let _ = crossterm::terminal::disable_raw_mode();
        }
    }
}


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

/// Restore the terminal out of raw mode no matter how the pump returns.
struct RawGuard(bool);
impl RawGuard {
    fn enable() -> Self {
        RawGuard(crossterm::terminal::enable_raw_mode().is_ok())
    }
}
impl Drop for RawGuard {
    fn drop(&mut self) {
        if self.0 {
            let _ = crossterm::terminal::disable_raw_mode();
        }
    }
}

/// The Windows console-input flag that makes the legacy console deliver VT key
/// encodings (REQ-HAZARD-RC-INPUT-KEY-ENCODING). windows-sys gates this const
/// behind a feature we don't pull, so define the documented value locally.
#[cfg(windows)]
const ENABLE_VIRTUAL_TERMINAL_INPUT: u32 = 0x0200;

/// Pure decision seam (W7 / REQ-HAZARD-RC-INPUT-KEY-ENCODING): OR the
/// VT-input bit into a console input mode, leaving every other flag untouched.
///
/// Without it, Windows crossterm raw mode leaves `ENABLE_VIRTUAL_TERMINAL_INPUT`
/// OFF, so the LEGACY console delivers `^H` (0x08) for Backspace — and Claude
/// Code maps `^H` → backward-kill-word, i.e. the operator's whole-word delete.
/// With it, Backspace arrives as VT DEL (0x7f, char-delete) and Ctrl+Backspace
/// as 0x08 (CC kill-word) — distinct, correct bytes — and arrows/Home/End arrive
/// as CSI sequences. Idempotent + pure so it unit-tests without a console.
// [impl->REQ-HAZARD-RC-INPUT-KEY-ENCODING]
#[cfg(windows)]
fn with_vt_input(mode: u32) -> u32 {
    mode | ENABLE_VIRTUAL_TERMINAL_INPUT
}

/// Set `ENABLE_VIRTUAL_TERMINAL_INPUT` on the stdin console (on top of the raw
/// mode crossterm just applied) and return the prior mode to restore on exit.
/// `None` if there is no console / the calls fail (best-effort, like raw mode).
// [impl->REQ-HAZARD-RC-INPUT-KEY-ENCODING]
#[cfg(windows)]
fn enable_vt_input() -> Option<u32> {
    use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
    use windows_sys::Win32::System::Console::{
        GetConsoleMode, GetStdHandle, SetConsoleMode, STD_INPUT_HANDLE,
    };
    unsafe {
        let h = GetStdHandle(STD_INPUT_HANDLE);
        if h.is_null() || h == INVALID_HANDLE_VALUE {
            return None;
        }
        let mut mode: u32 = 0;
        if GetConsoleMode(h, &mut mode) == 0 {
            return None;
        }
        let want = with_vt_input(mode);
        if want != mode && SetConsoleMode(h, want) == 0 {
            return None;
        }
        Some(mode)
    }
}

/// Restore a saved stdin console input mode (W7 exit path).
#[cfg(windows)]
fn restore_in_mode(prev: u32) {
    use windows_sys::Win32::System::Console::{GetStdHandle, SetConsoleMode, STD_INPUT_HANDLE};
    unsafe {
        let h = GetStdHandle(STD_INPUT_HANDLE);
        if !h.is_null() {
            let _ = SetConsoleMode(h, prev);
        }
    }
}

/// Restore the terminal out of raw mode no matter how the pump returns. On
/// Windows it ALSO enables `ENABLE_VIRTUAL_TERMINAL_INPUT` for the attach so the
/// hosted TUI gets a VT key map (W7), restoring the prior console mode on drop.
struct RawGuard {
    raw_ok: bool,
    /// The pre-VT stdin console mode to restore on exit (Windows only).
    #[cfg(windows)]
    prev_in_mode: Option<u32>,
}
impl RawGuard {
    fn enable() -> Self {
        let raw_ok = crossterm::terminal::enable_raw_mode().is_ok();
        #[cfg(windows)]
        let prev_in_mode = if raw_ok { enable_vt_input() } else { None };
        RawGuard {
            raw_ok,
            #[cfg(windows)]
            prev_in_mode,
        }
    }
}
impl Drop for RawGuard {
    fn drop(&mut self) {
        #[cfg(windows)]
        if let Some(prev) = self.prev_in_mode {
            restore_in_mode(prev);
        }
        if self.raw_ok {
            let _ = crossterm::terminal::disable_raw_mode();
        }
    }
}

-/// Restore the terminal out of raw mode no matter how the pump returns.

+/// The Windows console-input flag that makes the legacy console deliver VT key

+/// encodings (REQ-HAZARD-RC-INPUT-KEY-ENCODING). windows-sys gates this const

+/// behind a feature we don't pull, so define the documented value locally.

+const ENABLE_VIRTUAL_TERMINAL_INPUT: u32 = 0x0200;

+/// Pure decision seam (W7 / REQ-HAZARD-RC-INPUT-KEY-ENCODING): OR the

+/// VT-input bit into a console input mode, leaving every other flag untouched.

+/// Without it, Windows crossterm raw mode leaves `ENABLE_VIRTUAL_TERMINAL_INPUT`

+/// OFF, so the LEGACY console delivers `^H` (0x08) for Backspace — and Claude

+/// Code maps `^H` → backward-kill-word, i.e. the operator's whole-word delete.

+/// With it, Backspace arrives as VT DEL (0x7f, char-delete) and Ctrl+Backspace

+/// as 0x08 (CC kill-word) — distinct, correct bytes — and arrows/Home/End arrive

+/// as CSI sequences. Idempotent + pure so it unit-tests without a console.

+    mode | ENABLE_VIRTUAL_TERMINAL_INPUT

+/// Set `ENABLE_VIRTUAL_TERMINAL_INPUT` on the stdin console (on top of the raw

+/// mode crossterm just applied) and return the prior mode to restore on exit.

+/// `None` if there is no console / the calls fail (best-effort, like raw mode).

+    use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;

+        GetConsoleMode, GetStdHandle, SetConsoleMode, STD_INPUT_HANDLE,

+        let h = GetStdHandle(STD_INPUT_HANDLE);

+        if h.is_null() || h == INVALID_HANDLE_VALUE {

+        if GetConsoleMode(h, &mut mode) == 0 {

+        let want = with_vt_input(mode);

+        if want != mode && SetConsoleMode(h, want) == 0 {

+/// Restore a saved stdin console input mode (W7 exit path).

+    use windows_sys::Win32::System::Console::{GetStdHandle, SetConsoleMode, STD_INPUT_HANDLE};

+            let _ = SetConsoleMode(h, prev);

+/// Restore the terminal out of raw mode no matter how the pump returns. On

+/// Windows it ALSO enables `ENABLE_VIRTUAL_TERMINAL_INPUT` for the attach so the

+/// hosted TUI gets a VT key map (W7), restoring the prior console mode on drop.

+    /// The pre-VT stdin console mode to restore on exit (Windows only).

-        RawGuard(crossterm::terminal::enable_raw_mode().is_ok())

+        let raw_ok = crossterm::terminal::enable_raw_mode().is_ok();

+        let prev_in_mode = if raw_ok { enable_vt_input() } else { None };

+        if let Some(prev) = self.prev_in_mode {


Now the W7 unit test — per your directive, delegating to the `spt-test-engineer` subagent (background, so I stay reachable). Meanwhile I'll do the KNOWN-HAZARDS entry + REQ activation.


Agent

W7 unit test for VT-input seam

Prompt:
Write a focused unit test in the spt-core repo for v0.13.0 wave W7 (REQ-HAZARD-RC-INPUT-KEY-ENCODING).

CONTEXT: I just landed the impl in `crates/spt/src/rc.rs`. The pure decision seam is:

```rust
#[cfg(windows)]
const ENABLE_VIRTUAL_TERMINAL_INPUT: u32 = 0x0200;

#[cfg(windows)]
fn with_vt_input(mode: u32) -> u32 {
    mode | ENABLE_VIRTUAL_TERMINAL_INPUT
}
```

This is the W7 root fix: on Windows, `spt rc`'s raw-mode stdin left `ENABLE_VIRTUAL_TERMINAL_INPUT` OFF, so the legacy console emitted `^H` (0x08) for Backspace and Claude Code mapped `^H` → backward-kill-word (whole-word delete). Enabling VT input makes Backspace arrive as VT DEL (0x7f) and Ctrl+Backspace as 0x08 — distinct, correct bytes — and arrows/Home/End as CSI. Confirmed byte capture (operator, real Windows Terminal): Backspace=0x08, Ctrl+Backspace=0x7f pre-fix.

TASK: Add a Windows-only (`#[cfg(windows)]`) unit test for `with_vt_input` in `crates/spt/src/rc.rs`. It is the pure mode-bit decision seam (the actual GetStdHandle/SetConsoleMode is the impure shell, exercised by the operator's HITL re-capture — do NOT try to test the FFI). Assert:
1. The VT-input bit (0x0200) is SET in the result.
2. Every OTHER bit in an arbitrary input mode is PRESERVED (e.g. raw-mode-ish flags) — OR-in only, no clobber.
3. Idempotent: with_vt_input(with_vt_input(x)) == with_vt_input(x).
4. When the bit is already set, the value is unchanged.

REQUIREMENTS:
- First READ `crates/spt/src/rc.rs` to match the existing test conventions exactly: find the existing `#[cfg(test)] mod tests` (parse_stdin_chunk is tested there) and its assertion style, naming, and the `// [unit->REQ-...]` evidence-tag convention used throughout this repo (tag goes ON or immediately above the test fn). Match them.
- Tag the test `// [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING]`.
- The test fn (and any test-only helpers) must be `#[cfg(windows)]` since `with_vt_input` is Windows-only (it must not break the Unix build — confirm `with_vt_input` is `#[cfg(windows)]` and gate the test the same way; if the existing tests mod is not cfg-gated, gate just this test fn).
- Deterministic, no FFI, no console. Name states scenario + expectation.
- Do NOT modify any non-test code. Do NOT touch the impl. Only add the test (and, if needed, place it in the existing tests module).

After writing, run ONLY this test to confirm it compiles + passes:
`cargo test -p spt --lib rc:: -- --nocapture` (or the narrowest filter that hits your new test). Report: the exact test code you added (with file:line), the command you ran, and the pass/fail result. If `cargo test -p spt --lib` is too broad/slow, you may build-check with `cargo test -p spt --lib --no-run` plus a targeted run. Keep it tight.


<owl_reachability_notice>
You have an active owl perch. Launch this Agent with run_in_background: true so you stay reachable for incoming messages while it works.
</owl_reachability_notice>


Error: Agent type 'spt-test-engineer' not found. Available agents: claude, claude-code-guide, Explore, general-purpose, gsd-advisor-researcher, gsd-ai-researcher, gsd-assumptions-analyzer, gsd-code-fixer, gsd-code-reviewer, gsd-codebase-mapper, gsd-debug-session-manager, gsd-debugger, gsd-doc-classifier, gsd-doc-synthesizer, gsd-doc-verifier, gsd-doc-writer, gsd-domain-researcher, gsd-eval-auditor, gsd-eval-planner, gsd-executor, gsd-framework-selector, gsd-integration-checker, gsd-intel-updater, gsd-nyquist-auditor, gsd-pattern-mapper, gsd-phase-researcher, gsd-plan-checker, gsd-planner, gsd-project-researcher, gsd-research-synthesizer, gsd-roadmapper, gsd-security-auditor, gsd-ui-auditor, gsd-ui-checker, gsd-ui-researcher, gsd-user-profiler, gsd-verifier, Plan, statusline-setup


The `spt-test-engineer` type isn't registered (profile lives in `projects/.claude/agents/`, not discovered for this cwd/session). For this trivial 4-line pure-fn test I'll write inline to keep W7 moving, and sort the agent wiring with you for the substantive W3/W4 suites. Check rc.rs test conventions.


201:fn parse_stdin_chunk(armed: &mut bool, input: &[u8]) -> DetachParse {
617:#[cfg(test)]
618:mod tests {
626:    // [unit->REQ-HAZARD-RC-ATTACH-FAILFAST] path (a): attaching to a perch stamped
645:    // [unit->REQ-HAZARD-RC-ATTACH-FAILFAST] path (b): the generous first-event
661:    // [unit->REQ-HAZARD-RC-EOF] a severed-broker-stream read error (UnexpectedEof
682:    // [unit->REQ-RC-1] the detach keybind state machine: `ctrl-b d` detaches
711:    // [unit->REQ-RC-1] the prefix may straddle a read boundary: a chunk ending
724:    // [unit->REQ-RC-1] operator op ids are non-colliding across sessions: two


617	#[cfg(test)]
618	mod tests {
619	    use super::*;
620
621	    fn parse(armed: &mut bool, input: &[u8]) -> (Vec<u8>, bool) {
622	        let p = parse_stdin_chunk(armed, input);
623	        (p.forward, p.detach)
624	    }
625
626	    // [unit->REQ-HAZARD-RC-ATTACH-FAILFAST] path (a): attaching to a perch stamped
627	    // status=offline (the B2-reconcile / endpoint-stop state) SHORT-CIRCUITS with
628	    // clean guidance — it returns Ok WITHOUT any broker dance (no daemon needed),
629	    // so it can never hang on a dead session.
630	    #[test]
631	    fn attach_offline_endpoint_short_circuits_clean() {
632	        let _home = crate::testutil::isolated_home();
633	        let id = "stopped-ep";
634	        let perch = spt_store::perch::resolve_perch_path(id, spt_store::perch::ParentHint::Infer);
635	        std::fs::create_dir_all(&perch).unwrap();
636	        let rec = spt_store::info::InfoJson::new(id, "t", std::process::id(), "sid", "live_agent");
637	        spt_store::info::write_info(&perch, &rec).unwrap();
638	        spt_store::info::set_status(&perch, spt_store::liveness::STATUS_OFFLINE).unwrap();
639
640	        // Offline status → the gate returns early (no broker), both intents.
641	        assert!(run_attach(id, AttachIntent::Control).is_ok());
642	        assert!(run_attach(id, AttachIntent::Viewer).is_ok());
643	    }
644
645	    // [unit->REQ-HAZARD-RC-ATTACH-FAILFAST] path (b): the generous first-event
646	    // backstop fires ONLY when NOTHING has arrived past the grace — never for a
647	    // session that produced an event (a fresh/mid-init alive attach is safe), and
648	    // never before the grace elapses.
649	    #[test]
650	    fn first_event_stall_decision() {
651	        // No event + past the generous grace → stalled.
652	        assert!(first_event_stalled(false, FIRST_EVENT_GRACE));
653	        assert!(first_event_stalled(false, FIRST_EVENT_GRACE + Duration::from_secs(1)));
654	        // Any event seen → never stalled (the working/mid-init attach guard).
655	        assert!(!first_event_stalled(true, FIRST_EVENT_GRACE + Duration::from_secs(60)));
656	        // Before the grace → not yet (a slow-painting session still has time).

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

    fn parse(armed: &mut bool, input: &[u8]) -> (Vec<u8>, bool) {
        let p = parse_stdin_chunk(armed, input);
        (p.forward, p.detach)
    }

    // [unit->REQ-HAZARD-RC-ATTACH-FAILFAST] path (a): attaching to a perch stamped
    // status=offline (the B2-reconcile / endpoint-stop state) SHORT-CIRCUITS with
    // clean guidance — it returns Ok WITHOUT any broker dance (no daemon needed),
    // so it can never hang on a dead session.
    #[test]
    fn attach_offline_endpoint_short_circuits_clean() {
        let _home = crate::testutil::isolated_home();
        let id = "stopped-ep";
        let perch = spt_store::perch::resolve_perch_path(id, spt_store::perch::ParentHint::Infer);
        std::fs::create_dir_all(&perch).unwrap();
        let rec = spt_store::info::InfoJson::new(id, "t", std::process::id(), "sid", "live_agent");
        spt_store::info::write_info(&perch, &rec).unwrap();
        spt_store::info::set_status(&perch, spt_store::liveness::STATUS_OFFLINE).unwrap();

        // Offline status → the gate returns early (no broker), both intents.
        assert!(run_attach(id, AttachIntent::Control).is_ok());
        assert!(run_attach(id, AttachIntent::Viewer).is_ok());
    }

    // [unit->REQ-HAZARD-RC-ATTACH-FAILFAST] path (b): the generous first-event
    // backstop fires ONLY when NOTHING has arrived past the grace — never for a
    // session that produced an event (a fresh/mid-init alive attach is safe), and
    // never before the grace elapses.
    #[test]
    fn first_event_stall_decision() {
        // No event + past the generous grace → stalled.
        assert!(first_event_stalled(false, FIRST_EVENT_GRACE));
        assert!(first_event_stalled(false, FIRST_EVENT_GRACE + Duration::from_secs(1)));
        // Any event seen → never stalled (the working/mid-init attach guard).
        assert!(!first_event_stalled(true, FIRST_EVENT_GRACE + Duration::from_secs(60)));
        // Before the grace → not yet (a slow-painting session still has time).


Add the W7 unit test inline (Windows-gated, matching the repo's `[unit->REQ-...]` tag convention).


Edit

        // Offline status → the gate returns early (no broker), both intents.
        assert!(run_attach(id, AttachIntent::Control).is_ok());
        assert!(run_attach(id, AttachIntent::Viewer).is_ok());
    }

        // Offline status → the gate returns early (no broker), both intents.
        assert!(run_attach(id, AttachIntent::Control).is_ok());
        assert!(run_attach(id, AttachIntent::Viewer).is_ok());
    }

    // [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING] the VT-input mode-bit decision
    // seam: OR-in the ENABLE_VIRTUAL_TERMINAL_INPUT bit, preserve every other
    // flag, idempotent. This is what flips the legacy console from emitting ^H
    // (0x08, → CC kill-word) to VT DEL (0x7f) for Backspace. The impure
    // GetStdHandle/Get/SetConsoleMode shell is HITL-verified by the operator key
    // re-capture, not here.
    #[cfg(windows)]
    #[test]
    fn with_vt_input_sets_the_bit_and_preserves_other_flags() {
        // Sets the VT-input bit from a clean slate.
        assert_eq!(
            with_vt_input(0) & ENABLE_VIRTUAL_TERMINAL_INPUT,
            ENABLE_VIRTUAL_TERMINAL_INPUT
        );
        // OR-in only: every pre-existing (raw-mode-ish) flag survives untouched.
        let raw = 0x0001 | 0x0040 | 0x0080;
        let got = with_vt_input(raw);
        assert_eq!(got & raw, raw, "no existing console flag is cleared");
        assert_eq!(got, raw | ENABLE_VIRTUAL_TERMINAL_INPUT);
        // Idempotent — already-set is a no-op (the restore-on-exit relies on this
        // being a pure superset so the saved prior mode round-trips).
        assert_eq!(with_vt_input(got), got);
    }


        // Offline status → the gate returns early (no broker), both intents.
        assert!(run_attach(id, AttachIntent::Control).is_ok());
        assert!(run_attach(id, AttachIntent::Viewer).is_ok());
    }

    // [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING] the VT-input mode-bit decision
    // seam: OR-in the ENABLE_VIRTUAL_TERMINAL_INPUT bit, preserve every other
    // flag, idempotent. This is what flips the legacy console from emitting ^H
    // (0x08, → CC kill-word) to VT DEL (0x7f) for Backspace. The impure
    // GetStdHandle/Get/SetConsoleMode shell is HITL-verified by the operator key
    // re-capture, not here.
    #[cfg(windows)]
    #[test]
    fn with_vt_input_sets_the_bit_and_preserves_other_flags() {
        // Sets the VT-input bit from a clean slate.
        assert_eq!(
            with_vt_input(0) & ENABLE_VIRTUAL_TERMINAL_INPUT,
            ENABLE_VIRTUAL_TERMINAL_INPUT
        );
        // OR-in only: every pre-existing (raw-mode-ish) flag survives untouched.
        let raw = 0x0001 | 0x0040 | 0x0080;
        let got = with_vt_input(raw);
        assert_eq!(got & raw, raw, "no existing console flag is cleared");
        assert_eq!(got, raw | ENABLE_VIRTUAL_TERMINAL_INPUT);
        // Idempotent — already-set is a no-op (the restore-on-exit relies on this
        // being a pure superset so the saved prior mode round-trips).
        assert_eq!(with_vt_input(got), got);
    }

+    // [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING] the VT-input mode-bit decision

+    // seam: OR-in the ENABLE_VIRTUAL_TERMINAL_INPUT bit, preserve every other

+    // flag, idempotent. This is what flips the legacy console from emitting ^H

+    // (0x08, → CC kill-word) to VT DEL (0x7f) for Backspace. The impure

+    // GetStdHandle/Get/SetConsoleMode shell is HITL-verified by the operator key

+    fn with_vt_input_sets_the_bit_and_preserves_other_flags() {

+        // Sets the VT-input bit from a clean slate.

+            with_vt_input(0) & ENABLE_VIRTUAL_TERMINAL_INPUT,

+            ENABLE_VIRTUAL_TERMINAL_INPUT

+        // OR-in only: every pre-existing (raw-mode-ish) flag survives untouched.

+        assert_eq!(got & raw, raw, "no existing console flag is cleared");

+        assert_eq!(got, raw | ENABLE_VIRTUAL_TERMINAL_INPUT);

+        // Idempotent — already-set is a no-op (the restore-on-exit relies on this

+        // being a pure superset so the saved prior mode round-trips).

+        assert_eq!(with_vt_input(got), got);

// [unit->REQ-HAZARD-RC-ATTACH-FAILFAST] path (b): the generous first-event

// backstop fires ONLY when NOTHING has arrived past the grace — never for a

// session that produced an event (a fresh/mid-init alive attach is safe), and


Activate the W7 REQ + add the KNOWN-HAZARDS entry. Read the current REQ line.


1219	id = "REQ-HAZARD-RC-INPUT-KEY-ENCODING"
1220	title = "An `spt rc` session forwards the Backspace key as the VT DEL byte (0x7f), so the hosted TUI (Claude Code) deletes ONE character — never a whole word. SYMPTOM (operator dogfooding): Backspace in an rc session always behaves like ctrl+Backspace — deletes the entire last word. ROOT (doyle /diagnose, code-grounded, byte PENDING HITL confirm): rc is a RAW VERBATIM byte pump — spawn_stdin_reader (rc.rs:152) reads std::io::stdin() bytes under crossterm raw mode and forwards them unchanged (parse_stdin_chunk only intercepts the ctrl-b detach prefix); there is NO key-event encoding and NO 0x08↔0x7f normalization ANYWHERE in the tree (grep: zero SetConsoleMode / ENABLE_VIRTUAL_TERMINAL_INPUT). On Windows, crossterm enable_raw_mode does NOT set ENABLE_VIRTUAL_TERMINAL_INPUT, so the LEGACY console delivers ^H (0x08, ctrl+h) for Backspace instead of VT DEL (0x7f); Claude Code maps ^H → backward-kill-word → the observed whole-word delete. CONFIRM-FIRST (build the loop): an env-gated hexdump in spawn_stdin_reader (SPT_RC_DEBUG_KEYS) prints the forwarded byte; operator presses Backspace + ctrl+Backspace in a real rc session. FIX CANDIDATES: (a) enable ENABLE_VIRTUAL_TERMINAL_INPUT on the rc stdin console on Windows so the console emits proper VT (Backspace→0x7f, arrows/Home/End as CSI) — cleanest, fixes the whole key map not just Backspace; (b) narrow normalize bare 0x08→0x7f in the rc input path (riskier — a real ctrl+h is also 0x08). Prefer (a) unless it regresses other keys. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)"
1221	required_stages = []  # SEED — activate [impl,unit] at W7 build (Windows-specific; unit can drive the byte-normalization/console-mode decision seam purely; the end-to-end keypress is HITL operator-confirm). doc = KNOWN-HAZARDS.md entry. Confirm the forwarded byte via the SPT_RC_DEBUG_KEYS hexdump BEFORE the fix (gate the root, not the patch).

id = "REQ-HAZARD-RC-INPUT-KEY-ENCODING"
title = "An `spt rc` session forwards the Backspace key as the VT DEL byte (0x7f), so the hosted TUI (Claude Code) deletes ONE character — never a whole word. SYMPTOM (operator dogfooding): Backspace in an rc session always behaves like ctrl+Backspace — deletes the entire last word. ROOT (doyle /diagnose, code-grounded, byte PENDING HITL confirm): rc is a RAW VERBATIM byte pump — spawn_stdin_reader (rc.rs:152) reads std::io::stdin() bytes under crossterm raw mode and forwards them unchanged (parse_stdin_chunk only intercepts the ctrl-b detach prefix); there is NO key-event encoding and NO 0x08↔0x7f normalization ANYWHERE in the tree (grep: zero SetConsoleMode / ENABLE_VIRTUAL_TERMINAL_INPUT). On Windows, crossterm enable_raw_mode does NOT set ENABLE_VIRTUAL_TERMINAL_INPUT, so the LEGACY console delivers ^H (0x08, ctrl+h) for Backspace instead of VT DEL (0x7f); Claude Code maps ^H → backward-kill-word → the observed whole-word delete. CONFIRM-FIRST (build the loop): an env-gated hexdump in spawn_stdin_reader (SPT_RC_DEBUG_KEYS) prints the forwarded byte; operator presses Backspace + ctrl+Backspace in a real rc session. FIX CANDIDATES: (a) enable ENABLE_VIRTUAL_TERMINAL_INPUT on the rc stdin console on Windows so the console emits proper VT (Backspace→0x7f, arrows/Home/End as CSI) — cleanest, fixes the whole key map not just Backspace; (b) narrow normalize bare 0x08→0x7f in the rc input path (riskier — a real ctrl+h is also 0x08). Prefer (a) unless it regresses other keys. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)"
required_stages = []  # SEED — activate [impl,unit] at W7 build (Windows-specific; unit can drive the byte-normalization/console-mode decision seam purely; the end-to-end keypress is HITL operator-confirm). doc = KNOWN-HAZARDS.md entry. Confirm the forwarded byte via the SPT_RC_DEBUG_KEYS hexdump BEFORE the fix (gate the root, not the patch).


required_stages = []  # SEED — activate [impl,unit] at W7 build (Windows-specific; unit can drive the byte-normalization/console-mode decision seam purely; the end-to-end keypress is HITL operator-confirm). doc = KNOWN-HAZARDS.md entry. Confirm the forwarded byte via the SPT_RC_DEBUG_KEYS hexdump BEFORE the fix (gate the root, not the patch).

required_stages = ["impl", "unit"]  # ACTIVATED v0.13.0 W7 (todlando). BYTE CONFIRMED (operator HITL re-capture via SPT_RC_DEBUG_KEYS on the fresh debug binary, real Windows Terminal): pre-fix Backspace=0x08 (^H), Ctrl+Backspace=0x7f (DEL) — the INVERTED-from-VT signature of ENABLE_VIRTUAL_TERMINAL_INPUT being OFF (CC maps ^H → backward-kill-word = the whole-word delete). FIX (a, doyle ruling, NOT the 0x08→0x7f normalize which would conflate Backspace/Ctrl+Backspace): RawGuard now enables ENABLE_VIRTUAL_TERMINAL_INPUT on the stdin console alongside crossterm raw mode and restores the prior mode on drop (cfg(windows); Unix passthrough unchanged) — Backspace→0x7f, Ctrl+Backspace→0x08, arrows/Home/End→CSI, ctrl-b(0x02) detach unaffected. impl = rc.rs with_vt_input/enable_vt_input/restore_in_mode + RawGuard. unit = with_vt_input_sets_the_bit_and_preserves_other_flags (the pure mode-bit seam; the FFI shell + keypress are HITL operator re-capture). doc = KNOWN-HAZARDS.md entry. The SPT_RC_DEBUG_KEYS hexdump (committed f05566b) confirmed the root before the patch.


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

title = "An `spt rc` session forwards the Backspace key as the VT DEL byte (0x7f), so the hosted TUI (Claude Code) deletes ONE character — never a whole word. SYMPTOM (operator dogfooding): Backspace in an rc session always behaves like ctrl+Backspace — deletes the entire last word. ROOT (doyle /diagnose, code-grounded, byte PENDING HITL confirm): rc is a RAW VERBATIM byte pump — spawn_stdin_reader (rc.rs:152) reads std::io::stdin() bytes under crossterm raw mode and forwards them unchanged (parse_stdin_chunk only intercepts the ctrl-b detach prefix); there is NO key-event encoding and NO 0x08↔0x7f normalization ANYWHERE in the tree (grep: zero SetConsoleMode / ENABLE_VIRTUAL_TERMINAL_INPUT). On Windows, crossterm enable_raw_mode does NOT set ENABLE_VIRTUAL_TERMINAL_INPUT, so the LEGACY console delivers ^H (0x08, ctrl+h) for Backspace instead of VT DEL (0x7f); Claude Code maps ^H → backward-kill-word → the observed whole-word delete. CONFIRM-FIRST (build the loop): an env-gated hexdump in spawn_stdin_reader (SPT_RC_DEBUG_KEYS) prints the forwarded byte; operator presses Backspace + ctrl+Backspace in a real rc session. FIX CANDIDATES: (a) enable ENABLE_VIRTUAL_TERMINAL_INPUT on the rc stdin console on Windows so the console emits proper VT (Backspace→0x7f, arrows/Home/End as CSI) — cleanest, fixes the whole key map not just Backspace; (b) narrow normalize bare 0x08→0x7f in the rc input path (riskier — a real ctrl+h is also 0x08). Prefer (a) unless it regresses other keys. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)"

-required_stages = []  # SEED — activate [impl,unit] at W7 build (Windows-specific; unit can drive the byte-normalization/console-mode decision seam purely; the end-to-end keypress is HITL operator-confirm). doc = KNOWN-HAZARDS.md entry. Confirm the forwarded byte via the SPT_RC_DEBUG_KEYS hexdump BEFORE the fix (gate the root, not the patch).

+required_stages = ["impl", "unit"]  # ACTIVATED v0.13.0 W7 (todlando). BYTE CONFIRMED (operator HITL re-capture via SPT_RC_DEBUG_KEYS on the fresh debug binary, real Windows Terminal): pre-fix Backspace=0x08 (^H), Ctrl+Backspace=0x7f (DEL) — the INVERTED-from-VT signature of ENABLE_VIRTUAL_TERMINAL_INPUT being OFF (CC maps ^H → backward-kill-word = the whole-word delete). FIX (a, doyle ruling, NOT the 0x08→0x7f normalize which would conflate Backspace/Ctrl+Backspace): RawGuard now enables ENABLE_VIRTUAL_TERMINAL_INPUT on the stdin console alongside crossterm raw mode and restores the prior mode on drop (cfg(windows); Unix passthrough unchanged) — Backspace→0x7f, Ctrl+Backspace→0x08, arrows/Home/End→CSI, ctrl-b(0x02) detach unaffected. impl = rc.rs with_vt_input/enable_vt_input/restore_in_mode + RawGuard. unit = with_vt_input_sets_the_bit_and_preserves_other_flags (the pure mode-bit seam; the FFI shell + keypress are HITL operator re-capture). doc = KNOWN-HAZARDS.md entry. The SPT_RC_DEBUG_KEYS hexdump (committed f05566b) confirmed the root before the patch.


Add the KNOWN-HAZARDS entry. Check the doc's structure/numbering.


Bash

cd C:/Users/decid/Documents/projects/spt-core && grep -nE "^#{1,3} |^### 7\.|RC-INPUT|VIRTUAL_TERMINAL" docs/KNOWN-HAZARDS.md | tail -30

Inspect KNOWN-HAZARDS structure + numbering


221:### 5.8 Console children of the console-less daemon flash visible windows  `[REQ-HAZARD-CHILD-CONSOLE-FLASH]`
228:### 5.9 `Instant - Duration` underflow-panics on a freshly-booted host  `[REQ-HAZARD-INSTANT-UNDERFLOW]`
235:### 5.10 `sudo spt` dead-ends on a user-local install (secure_path)  `[REQ-HAZARD-SUDO-SECURE-PATH]`
243:### 5.11 Self-elevating re-launch must re-run verbatim, never widen / inject / loop  `[REQ-HAZARD-SELF-ELEVATE]`
251:### 5.12 Native-PTY spawn of a bare program runs the wrong (non-PE) file on Windows  `[REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE]`
259:## 6. Documented regressions (non-obvious invariants)
261:### 6.1 No flat/nested perch siblings; resolver-routed paths
267:### 6.2 Soft-cleanup preserves state, removes `ready`
273:### 6.3 Cascade-wipe guard: never delete a parent hosting non-empty children
279:### 6.4 Drop files are single-writer (supervisor-owned), read-only for the mind
285:### 6.5 Direct-write precedence guard against stale LLM overwrites
291:### 6.6 Surfaced context conflicts preserve both versions until dominated
297:### 6.7 Broker and brain MUST be separate processes (in-process collapse silently breaks no-endpoint-drop update) `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]`
306:### 6.8 No irreversible durable-state migration before update ready-promotion `[REQ-HAZARD-ROLLBACK-STATE-COMPAT]`
316:### 6.9 Resume-mode brain: a blocking spawn/command wait silently discards OTHER sessions' output
322:### 6.10 Phase-significant loop timing must be a durable absolute-deadline grid, not phase-relative sleep `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]`
328:### 6.11 Brain respawn must exec the APPLIED bytes, not the renamed old binary (Linux `current_exe` follows the apply-rename; readiness ≠ new-bytes) `[REQ-HAZARD-BRAIN-RESPAWN-PATH]`
337:## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)
341:### 7.1 Local `api` mutation auth  `[REQ-HAZARD-LOCAL-API-AUTH]`
347:### 7.2 Idempotent delivery across brain restart  `[REQ-HAZARD-RESTART-IDEMPOTENT]`
353:### 7.3 Psyche outbound capture + sanitization  `[REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]`
359:### 7.4 Per-agent pulse/psyche/echo scheduling must not serialize across agents  `[REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]`
365:### 7.5 WAN-inbound origin is transport truth, never payload  `[REQ-HAZARD-WAN-ORIGIN-AUTH]`
373:### 7.6 Pump brain-IPC reads must be deadline-bounded (a blocked read wedges the whole pump)  `[REQ-HAZARD-PUMP-IPC-DEADLINE]`
380:### 7.7 A slow/dead/hostile remote VIEWER must never stall the controller, child, or drain  `[REQ-HAZARD-VIEWER-ISOLATION]`
387:### 7.8 The broker must never make a brain wait UNBOUNDED on a QUIC op (the pump-IPC-deadline B-half)  `[REQ-HAZARD-BROKER-QUIC-DEADLINE]`
394:### 7.9 A daemon-state wire change needs a deliberate BROKER restart (the broker is resident across a brain self-update)  `[REQ-HAZARD-BROKER-SEED-WIRE-SKEW]`
401:### 7.10 A VIEW is independent from the endpoint — closing the launching tab must NOT reap the daemon-hosted harness  `[REQ-HAZARD-VIEWER-CLOSE-DETACH]`
408:### 7.11 A dead PTY child + a dropped operator pump must NOT wedge the broker for other clients  `[REQ-HAZARD-ATTACH-WEDGE]`
416:## Conformance checklist (condensed)


Read the 7.11 entry format + the conformance checklist to match.