837	pub(crate) fn event_body_escape(s: &str) -> String {
838	    s.replace('&', "&")
839	        .replace('<', "<")
840	        .replace('>', ">")
841	        .replace('"', """)
842	        .replace('\n', "
")
843	}
844
845	/// Inverse of `event_body_escape`. Decode order MUST be `<br>` FIRST, then
846	/// tag-shaped entities, with `&`→`&` LAST.
847	///
848	/// 25.3-03 Defect C — body-side counterpart to the attr-side inverse
849	/// (`parse_event_from_attr`). Call-site invariant: invoke ONLY after EVENT
850	/// framing has been parsed and the body substring extracted; NEVER apply to
851	/// the full envelope string globally — that would unescape outer framing
852	/// tokens too and reintroduce the asymmetric site Defect C documented.
853	///
854	/// Amp-last invariant: if `&` were decoded first, a body like
855	/// `&lt;EVENT&gt;` (user-prose literal entity reference) would
856	/// double-decode into `<EVENT>` and become structurally significant.
857	///
858	/// Invariant: at most ONE body decode per transport traversal.
859	pub(crate) fn event_body_unescape(s: &str) -> String {
860	    s.replace("
", "\n")
861	        .replace("<", "<")
862	        .replace(">", ">")
863	        .replace(""", "\"")
864	        .replace("&", "&") // amp-last (Pitfall 3 inverse)
865	}
866
867	/// HTML-escape for attribute values. Same order as event_body_escape but omit
868	/// the `\n â†’ <br>` step â€” attribute values never carry newlines in this
869	/// protocol (from-id and ISO-8601 timestamps are line-safe by construction).
870	///
871	/// Promoted to `pub(crate)` per Phase 29 Plan 01 Task 1 so the new
872	/// `compose_echo_commune_payload` helper in `crate::owl::echo_commune` can
873	/// reuse the canonical escape pair instead of duplicating it.
874	pub(crate) fn event_attr_escape(s: &str) -> String {
875	    s.replace('&', "&")
876	        .replace('<', "<")
877	        .replace('>', ">")
878	        .replace('"', """)
879	}
880
881	/// Extract the `from=` attribute value from an `<EVENT ... from="ATTR" ...>`
882	/// envelope. Inverse pair of `event_attr_escape` (above) â€” unescapes
883	/// `< > " &`. Returns None on: no `<EVENT ` tag, no `from=`
884	/// attribute, empty `from=""`, or a malformed envelope with no closing quote.
885	///
886	/// CRITICAL ORDER (inverse of the event_body_escape Pitfall 3 at poll.rs:508-510):
887	/// `&` MUST be unescaped LAST. If `&` were decoded first, a sender
888	/// id like `&lt;` would double-decode into `<`. Each non-amp entity is
889	/// unique so their relative order is free; only the amp-last invariant matters.
890	///
891	/// Used by the wrapper REPLY arm (src/live/wrapper/mod.rs:223-238) to route
892	/// replies back to the original EVENT sender post-aba13d9 envelope rollout
893	/// (quick-260513-vzw HIGH-1).
894	pub(crate) fn parse_event_from_attr(msg: &str) -> Option<String> {
895	    let event_start = msg.find("<EVENT ")?;
896	    let after_event = &msg[event_start..];
897	    let from_marker = "from=\"";
898	    let from_pos = after_event.find(from_marker)?;
899	    let value_start = from_pos + from_marker.len();
900	    let rest = &after_event[value_start..];
901	    let close_quote = rest.find('"')?;
902	    let escaped = &rest[..close_quote];
903	    if escaped.is_empty() {
904	        return None;
905	    }
906	    // Order: non-amp entities first, & LAST (see doc comment above).
907	    let unescaped = escaped
908	        .replace("<", "<")
909	        .replace(">", ">")
910	        .replace(""", "\"")
911	        .replace("&", "&");

pub(crate) fn event_body_escape(s: &str) -> String {
    s.replace('&', "&")
        .replace('<', "<")
        .replace('>', ">")
        .replace('"', """)
        .replace('\n', "
")
}

/// Inverse of `event_body_escape`. Decode order MUST be `<br>` FIRST, then
/// tag-shaped entities, with `&`→`&` LAST.
///
/// 25.3-03 Defect C — body-side counterpart to the attr-side inverse
/// (`parse_event_from_attr`). Call-site invariant: invoke ONLY after EVENT
/// framing has been parsed and the body substring extracted; NEVER apply to
/// the full envelope string globally — that would unescape outer framing
/// tokens too and reintroduce the asymmetric site Defect C documented.
///
/// Amp-last invariant: if `&` were decoded first, a body like
/// `&lt;EVENT&gt;` (user-prose literal entity reference) would
/// double-decode into `<EVENT>` and become structurally significant.
///
/// Invariant: at most ONE body decode per transport traversal.
pub(crate) fn event_body_unescape(s: &str) -> String {
    s.replace("
", "\n")
        .replace("<", "<")
        .replace(">", ">")
        .replace(""", "\"")
        .replace("&", "&") // amp-last (Pitfall 3 inverse)
}

/// HTML-escape for attribute values. Same order as event_body_escape but omit
/// the `\n â†’ <br>` step â€” attribute values never carry newlines in this
/// protocol (from-id and ISO-8601 timestamps are line-safe by construction).
///
/// Promoted to `pub(crate)` per Phase 29 Plan 01 Task 1 so the new
/// `compose_echo_commune_payload` helper in `crate::owl::echo_commune` can
/// reuse the canonical escape pair instead of duplicating it.
pub(crate) fn event_attr_escape(s: &str) -> String {
    s.replace('&', "&")
        .replace('<', "<")
        .replace('>', ">")
        .replace('"', """)
}

/// Extract the `from=` attribute value from an `<EVENT ... from="ATTR" ...>`
/// envelope. Inverse pair of `event_attr_escape` (above) â€” unescapes
/// `< > " &`. Returns None on: no `<EVENT ` tag, no `from=`
/// attribute, empty `from=""`, or a malformed envelope with no closing quote.
///
/// CRITICAL ORDER (inverse of the event_body_escape Pitfall 3 at poll.rs:508-510):
/// `&` MUST be unescaped LAST. If `&` were decoded first, a sender
/// id like `&lt;` would double-decode into `<`. Each non-amp entity is
/// unique so their relative order is free; only the amp-last invariant matters.
///
/// Used by the wrapper REPLY arm (src/live/wrapper/mod.rs:223-238) to route
/// replies back to the original EVENT sender post-aba13d9 envelope rollout
/// (quick-260513-vzw HIGH-1).
pub(crate) fn parse_event_from_attr(msg: &str) -> Option<String> {
    let event_start = msg.find("<EVENT ")?;
    let after_event = &msg[event_start..];
    let from_marker = "from=\"";
    let from_pos = after_event.find(from_marker)?;
    let value_start = from_pos + from_marker.len();
    let rest = &after_event[value_start..];
    let close_quote = rest.find('"')?;
    let escaped = &rest[..close_quote];
    if escaped.is_empty() {
        return None;
    }
    // Order: non-amp entities first, & LAST (see doc comment above).
    let unescaped = escaped
        .replace("<", "<")
        .replace(">", ">")
        .replace(""", "\"")
        .replace("&", "&");


Got the codec verbatim (amp-last decode confirmed). Building the first M0 brick: cargo workspace + `spt-proto` with the envelope codec + two-slice parser copied verbatim, both KNOWN-HAZARDS tagged and unit-tested.


Write

# spt-core cargo workspace (M0). Acyclically-layered crates per R-ARCH-1:
#   spt-proto -> spt-store -> spt-msg -> {spt-net, spt-term, spt-runtime} -> spt-live -> spt-daemon -> spt
# Only spt-proto exists so far (M0, first brick).
[workspace]
resolver = "2"
members = ["crates/spt-proto"]


proceed however you recommend using best judgment


File created successfully at: C:\Users\decid\Documents\projects\spt-core\Cargo.toml (file state is current in your context — no need to Read it back)

# spt-core cargo workspace (M0). Acyclically-layered crates per R-ARCH-1:
#   spt-proto -> spt-store -> spt-msg -> {spt-net, spt-term, spt-runtime} -> spt-live -> spt-daemon -> spt
# Only spt-proto exists so far (M0, first brick).
[workspace]
resolver = "2"
members = ["crates/spt-proto"]


[package]
name = "spt-proto"
version = "0.0.0"
edition = "2021"
publish = false
description = "spt-core wire protocol: envelope grammar + HTML-entity codec + two-slice parser. Public SDK (R-ARCH-2)."

[dependencies]


File created successfully at: C:\Users\decid\Documents\projects\spt-core\crates\spt-proto\Cargo.toml (file state is current in your context — no need to Read it back)

[package]
name = "spt-proto"
version = "0.0.0"
edition = "2021"
publish = false
description = "spt-core wire protocol: envelope grammar + HTML-entity codec + two-slice parser. Public SDK (R-ARCH-2)."

[dependencies]


//! `spt-proto` — the wire-protocol commodity layer for spt-core.
//!
//! Per ADR-0001, the stable wire formats are **copied verbatim** from the
//! sister project (`claude_skill_owl`); only architecture/lifecycle is
//! clean-room. This crate owns the envelope grammar and is part of the public,
//! semver-committed SDK (R-ARCH-2), carrying its own wire-protocol version
//! independent of crate semver (R-ARCH-3).

pub mod envelope;


File created successfully at: C:\Users\decid\Documents\projects\spt-core\crates\spt-proto\src\lib.rs (file state is current in your context — no need to Read it back)

//! `spt-proto` — the wire-protocol commodity layer for spt-core.
//!
//! Per ADR-0001, the stable wire formats are **copied verbatim** from the
//! sister project (`claude_skill_owl`); only architecture/lifecycle is
//! clean-room. This crate owns the envelope grammar and is part of the public,
//! semver-committed SDK (R-ARCH-2), carrying its own wire-protocol version
//! independent of crate semver (R-ARCH-3).

pub mod envelope;


//! Envelope grammar: the HTML-entity transport codec and the two-slice
//! Psyche-context parser. **Copied verbatim** from `claude_skill_owl`
//! (`src/owl/poll.rs` codec + `src/common/envelope.rs` parser) per ADR-0001 —
//! this is the stable, public wire contract; do not clean-room it.
//!
//! ## Encoding contract (the one written-down rule)
//! Producer encodes once at the source; body stays encoded through transport
//! (spool / TCP / relay); consumer decodes once after framing-parse.
//!
//! ENCODE order: `&`→`&` FIRST, then `<` `>` `"`, then `\n`→`<br>` LAST.
//! DECODE order: `<br>`→`\n` FIRST, then `<` `>` `"`, then
//! `&`→`&` **LAST** — the amp-last invariant. If `&` decoded first, a
//! user-prose literal like `&lt;EVENT&gt;` would double-decode into
//! `<EVENT>` and become structurally significant. (KNOWN-HAZARDS 4.1.)

// ---------------------------------------------------------------------------
// Transport codec (KNOWN-HAZARDS 4.1 — `&` decoded LAST)
// ---------------------------------------------------------------------------

/// HTML-escape an EVENT body. Order: amp-first, `<br>`-last.
pub fn event_body_escape(s: &str) -> String {
    s.replace('&', "&")
        .replace('<', "<")
        .replace('>', ">")
        .replace('"', """)
        .replace('\n', "
")
}

/// Inverse of [`event_body_escape`]. Decode order MUST be `<br>` FIRST, then
/// tag-shaped entities, with `&`→`&` LAST. Call ONLY after EVENT framing
/// has been parsed and the body substring extracted; never on the full
/// envelope string (that would unescape outer framing tokens too).
///
/// Amp-last invariant: if `&` were decoded first, a body like
/// `&lt;EVENT&gt;` would double-decode into `<EVENT>`.
// [impl->REQ-HAZARD-ENVELOPE-DECODE-ORDER]
pub fn event_body_unescape(s: &str) -> String {
    s.replace("
", "\n")
        .replace("<", "<")
        .replace(">", ">")
        .replace(""", "\"")
        .replace("&", "&") // amp-last (Pitfall 3 inverse)
}

/// HTML-escape for attribute values. Same order as [`event_body_escape`] but
/// omits `\n`→`<br>` (attr values are line-safe by construction).
pub fn event_attr_escape(s: &str) -> String {
    s.replace('&', "&")
        .replace('<', "<")
        .replace('>', ">")
        .replace('"', """)
}

/// Extract the `from="..."` attribute from an `<EVENT ... from="ATTR" ...>`
/// envelope, unescaping with the amp-last invariant. Returns `None` on: no
/// `<EVENT ` tag, no `from=` attribute, empty `from=""`, or no closing quote.
// [impl->REQ-HAZARD-ENVELOPE-DECODE-ORDER]
pub fn parse_event_from_attr(msg: &str) -> Option<String> {
    let event_start = msg.find("<EVENT ")?;
    let after_event = &msg[event_start..];
    let from_marker = "from=\"";
    let from_pos = after_event.find(from_marker)?;
    let value_start = from_pos + from_marker.len();
    let rest = &after_event[value_start..];
    let close_quote = rest.find('"')?;
    let escaped = &rest[..close_quote];
    if escaped.is_empty() {
        return None;
    }
    // Order: non-amp entities first, & LAST.
    let unescaped = escaped
        .replace("<", "<")
        .replace(">", ">")
        .replace(""", "\"")
        .replace("&", "&");
    Some(unescaped)
}

// ---------------------------------------------------------------------------
// Two-slice payload parser (KNOWN-HAZARDS 4.2 — panic-free + tolerant)
// ---------------------------------------------------------------------------

/// The two-slice payload produced by [`parse_two_slice`]. `live` is the body
/// of `<live-context>` if present; `project` is the body of `<project-context>`
/// if present. Either or both may be `None`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TwoSlicePayload {
    pub live: Option<String>,
    pub project: Option<String>,
}

/// Parse a raw Psyche output string into the two-slice taxonomy. Never panics:
/// every extraction is `Option`-based. Robustness rules: both/either/neither
/// tag, unclosed tags, out-of-order tags, empty bodies, surrounding whitespace,
/// and nested unknown tags preserved verbatim.
// [impl->REQ-HAZARD-ENVELOPE-PARSER-SAFE]
pub fn parse_two_slice(raw: &str) -> TwoSlicePayload {
    let live = extract_tag(raw, "live-context");
    let project = extract_tag(raw, "project-context");

    // Fallback: no tags at all → whole payload routes to the live slot (trimmed).
    if live.is_none() && project.is_none() {
        let trimmed = raw.trim();
        return TwoSlicePayload {
            live: Some(trimmed.to_string()),
            project: None,
        };
    }

    TwoSlicePayload { live, project }
}

/// Find the first `<{tag}>...</{tag}>` and return the trimmed body. `None` when
/// either delimiter is absent or the close does not follow the open.
fn extract_tag(haystack: &str, tag: &str) -> Option<String> {
    let open = format!("<{}>", tag);
    let close = format!("</{}>", tag);
    let open_idx = haystack.find(&open)?;
    let body_start = open_idx + open.len();
    let after_open = haystack.get(body_start..)?;
    let close_off = after_open.find(&close)?;
    let body = &after_open[..close_off];
    Some(body.trim().to_string())
}

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

    // ---- codec: amp-last invariant (KNOWN-HAZARDS 4.1) ----

    // [unit->REQ-HAZARD-ENVELOPE-DECODE-ORDER]
    #[test]
    fn decode_is_amp_last_so_literal_entities_do_not_double_decode() {
        // User prose containing a literal entity reference for `<EVENT>`.
        // Encoded once, it must round-trip back to the literal text, NOT to a
        // live `<EVENT>` framing token.
        let user_prose = "<EVENT>"; // literally the five chars ampersand-l-t...
        let encoded = event_body_escape(user_prose);
        // `&` -> `&` first, so the ampersands become `&`:
        assert_eq!(encoded, "&lt;EVENT&gt;");
        let decoded = event_body_unescape(&encoded);
        assert_eq!(
            decoded, user_prose,
            "amp-last decode must NOT collapse &lt; into <",
        );
    }

    // [unit->REQ-HAZARD-ENVELOPE-DECODE-ORDER]
    #[test]
    fn body_escape_unescape_round_trips_html_and_newlines() {
        let original = "a  c & d \"e\"\nsecond line";
        let round = event_body_unescape(&event_body_escape(original));
        assert_eq!(round, original);
    }

    // [unit->REQ-HAZARD-ENVELOPE-DECODE-ORDER]
    #[test]
    fn from_attr_unescape_is_amp_last() {
        let msg = "<EVENT from=\"&lt;weird&gt;\" type=\"msg\">body</EVENT>";
        assert_eq!(parse_event_from_attr(msg).as_deref(), Some("<weird>"));
    }

    #[test]
    fn from_attr_returns_none_on_garbage_or_empty() {
        assert_eq!(parse_event_from_attr("no event here"), None);
        assert_eq!(parse_event_from_attr("<EVENT from=\"\">x</EVENT>"), None);
        assert_eq!(parse_event_from_attr("<EVENT from=\"unterminated"), None);
    }

    // ---- parser: panic-free + tolerant (KNOWN-HAZARDS 4.2) ----

    // [unit->REQ-HAZARD-ENVELOPE-PARSER-SAFE]
    #[test]
    fn parser_both_tags_present() {
        let raw = "<live-context>\nagent slice\n</live-context>\n<project-context>\nproject slice\n</project-context>";
        let p = parse_two_slice(raw);
        assert_eq!(p.live.as_deref(), Some("agent slice"));
        assert_eq!(p.project.as_deref(), Some("project slice"));
    }

    // [unit->REQ-HAZARD-ENVELOPE-PARSER-SAFE]
    #[test]
    fn parser_no_tags_falls_back_whole_payload_to_live() {
        let p = parse_two_slice("  plain prose, no tags\n");
        assert_eq!(p.live.as_deref(), Some("plain prose, no tags"));
        assert_eq!(p.project, None);
    }

    // [unit->REQ-HAZARD-ENVELOPE-PARSER-SAFE]
    #[test]
    fn parser_unclosed_live_tag_does_not_scoop_project() {
        let raw = "<live-context>\noops no close\n<project-context>\nproject body\n</project-context>";
        let p = parse_two_slice(raw);
        assert_eq!(p.live, None);
        assert_eq!(p.project.as_deref(), Some("project body"));
    }

    // [unit->REQ-HAZARD-ENVELOPE-PARSER-SAFE]
    #[test]
    fn parser_out_of_order_tags_both_extracted() {
        let raw = "<project-context>P first</project-context>\n<live-context>L after</live-context>";
        let p = parse_two_slice(raw);
        assert_eq!(p.live.as_deref(), Some("L after"));
        assert_eq!(p.project.as_deref(), Some("P first"));
    }

    // [unit->REQ-HAZARD-ENVELOPE-PARSER-SAFE]
    #[test]
    fn parser_empty_body_is_some_empty_and_nested_tags_preserved() {
        let p = parse_two_slice("<live-context></live-context><project-context></project-context>");
        assert_eq!(p.live.as_deref(), Some(""));
        assert_eq!(p.project.as_deref(), Some(""));

        let nested = "<live-context>\nrole: x\nfoo\n</live-context>";
        let p2 = parse_two_slice(nested);
        assert_eq!(p2.live.as_deref(), Some("role: x\nfoo"));
    }
}


File created successfully at: C:\Users\decid\Documents\projects\spt-core\crates\spt-proto\src\envelope.rs (file state is current in your context — no need to Read it back)

//! Envelope grammar: the HTML-entity transport codec and the two-slice
//! Psyche-context parser. **Copied verbatim** from `claude_skill_owl`
//! (`src/owl/poll.rs` codec + `src/common/envelope.rs` parser) per ADR-0001 —
//! this is the stable, public wire contract; do not clean-room it.
//!
//! ## Encoding contract (the one written-down rule)
//! Producer encodes once at the source; body stays encoded through transport
//! (spool / TCP / relay); consumer decodes once after framing-parse.
//!
//! ENCODE order: `&`→`&` FIRST, then `<` `>` `"`, then `\n`→`<br>` LAST.
//! DECODE order: `<br>`→`\n` FIRST, then `<` `>` `"`, then
//! `&`→`&` **LAST** — the amp-last invariant. If `&` decoded first, a
//! user-prose literal like `&lt;EVENT&gt;` would double-decode into
//! `<EVENT>` and become structurally significant. (KNOWN-HAZARDS 4.1.)

// ---------------------------------------------------------------------------
// Transport codec (KNOWN-HAZARDS 4.1 — `&` decoded LAST)
// ---------------------------------------------------------------------------

/// HTML-escape an EVENT body. Order: amp-first, `<br>`-last.
pub fn event_body_escape(s: &str) -> String {
    s.replace('&', "&")
        .replace('<', "<")
        .replace('>', ">")
        .replace('"', """)
        .replace('\n', "
")
}

/// Inverse of [`event_body_escape`]. Decode order MUST be `<br>` FIRST, then
/// tag-shaped entities, with `&`→`&` LAST. Call ONLY after EVENT framing
/// has been parsed and the body substring extracted; never on the full
/// envelope string (that would unescape outer framing tokens too).
///
/// Amp-last invariant: if `&` were decoded first, a body like
/// `&lt;EVENT&gt;` would double-decode into `<EVENT>`.
// [impl->REQ-HAZARD-ENVELOPE-DECODE-ORDER]
pub fn event_body_unescape(s: &str) -> String {
    s.replace("
", "\n")
        .replace("<", "<")
        .replace(">", ">")
        .replace(""", "\"")
        .replace("&", "&") // amp-last (Pitfall 3 inverse)
}

/// HTML-escape for attribute values. Same order as [`event_body_escape`] but
/// omits `\n`→`<br>` (attr values are line-safe by construction).
pub fn event_attr_escape(s: &str) -> String {
    s.replace('&', "&")
        .replace('<', "<")
        .replace('>', ">")
        .replace('"', """)
}

/// Extract the `from="..."` attribute from an `<EVENT ... from="ATTR" ...>`
/// envelope, unescaping with the amp-last invariant. Returns `None` on: no
/// `<EVENT ` tag, no `from=` attribute, empty `from=""`, or no closing quote.
// [impl->REQ-HAZARD-ENVELOPE-DECODE-ORDER]
pub fn parse_event_from_attr(msg: &str) -> Option<String> {
    let event_start = msg.find("<EVENT ")?;
    let after_event = &msg[event_start..];
    let from_marker = "from=\"";
    let from_pos = after_event.find(from_marker)?;
    let value_start = from_pos + from_marker.len();
    let rest = &after_event[value_start..];
    let close_quote = rest.find('"')?;
    let escaped = &rest[..close_quote];
    if escaped.is_empty() {
        return None;
    }
    // Order: non-amp entities first, & LAST.
    let unescaped = escaped
        .replace("<", "<")
        .replace(">", ">")
        .replace(""", "\"")
        .replace("&", "&");
    Some(unescaped)
}

// ---------------------------------------------------------------------------
// Two-slice payload parser (KNOWN-HAZARDS 4.2 — panic-free + tolerant)
// ---------------------------------------------------------------------------

/// The two-slice payload produced by [`parse_two_slice`]. `live` is the body
/// of `<live-context>` if present; `project` is the body of `<project-context>`
/// if present. Either or both may be `None`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TwoSlicePayload {
    pub live: Option<String>,
    pub project: Option<String>,
}

/// Parse a raw Psyche output string into the two-slice taxonomy. Never panics:
/// every extraction is `Option`-based. Robustness rules: both/either/neither
/// tag, unclosed tags, out-of-order tags, empty bodies, surrounding whitespace,
/// and nested unknown tags preserved verbatim.
// [impl->REQ-HAZARD-ENVELOPE-PARSER-SAFE]
pub fn parse_two_slice(raw: &str) -> TwoSlicePayload {
    let live = extract_tag(raw, "live-context");
    let project = extract_tag(raw, "project-context");

    // Fallback: no tags at all → whole payload routes to the live slot (trimmed).
    if live.is_none() && project.is_none() {
        let trimmed = raw.trim();
        return TwoSlicePayload {
            live: Some(trimmed.to_string()),
            project: None,
        };
    }

    TwoSlicePayload { live, project }
}

/// Find the first `<{tag}>...</{tag}>` and return the trimmed body. `None` when
/// either delimiter is absent or the close does not follow the open.
fn extract_tag(haystack: &str, tag: &str) -> Option<String> {
    let open = format!("<{}>", tag);
    let close = format!("</{}>", tag);
    let open_idx = haystack.find(&open)?;
    let body_start = open_idx + open.len();
    let after_open = haystack.get(body_start..)?;
    let close_off = after_open.find(&close)?;
    let body = &after_open[..close_off];
    Some(body.trim().to_string())
}

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

    // ---- codec: amp-last invariant (KNOWN-HAZARDS 4.1) ----

    // [unit->REQ-HAZARD-ENVELOPE-DECODE-ORDER]
    #[test]
    fn decode_is_amp_last_so_literal_entities_do_not_double_decode() {
        // User prose containing a literal entity reference for `<EVENT>`.
        // Encoded once, it must round-trip back to the literal text, NOT to a
        // live `<EVENT>` framing token.
        let user_prose = "<EVENT>"; // literally the five chars ampersand-l-t...
        let encoded = event_body_escape(user_prose);
        // `&` -> `&` first, so the ampersands become `&`:
        assert_eq!(encoded, "&lt;EVENT&gt;");
        let decoded = event_body_unescape(&encoded);
        assert_eq!(
            decoded, user_prose,
            "amp-last decode must NOT collapse &lt; into <",
        );
    }

    // [unit->REQ-HAZARD-ENVELOPE-DECODE-ORDER]
    #[test]
    fn body_escape_unescape_round_trips_html_and_newlines() {
        let original = "a  c & d \"e\"\nsecond line";
        let round = event_body_unescape(&event_body_escape(original));
        assert_eq!(round, original);
    }

    // [unit->REQ-HAZARD-ENVELOPE-DECODE-ORDER]
    #[test]
    fn from_attr_unescape_is_amp_last() {
        let msg = "<EVENT from=\"&lt;weird&gt;\" type=\"msg\">body</EVENT>";
        assert_eq!(parse_event_from_attr(msg).as_deref(), Some("<weird>"));
    }

    #[test]
    fn from_attr_returns_none_on_garbage_or_empty() {
        assert_eq!(parse_event_from_attr("no event here"), None);
        assert_eq!(parse_event_from_attr("<EVENT from=\"\">x</EVENT>"), None);
        assert_eq!(parse_event_from_attr("<EVENT from=\"unterminated"), None);
    }

    // ---- parser: panic-free + tolerant (KNOWN-HAZARDS 4.2) ----

    // [unit->REQ-HAZARD-ENVELOPE-PARSER-SAFE]
    #[test]
    fn parser_both_tags_present() {
        let raw = "<live-context>\nagent slice\n</live-context>\n<project-context>\nproject slice\n</project-context>";
        let p = parse_two_slice(raw);
        assert_eq!(p.live.as_deref(), Some("agent slice"));
        assert_eq!(p.project.as_deref(), Some("project slice"));
    }

    // [unit->REQ-HAZARD-ENVELOPE-PARSER-SAFE]
    #[test]
    fn parser_no_tags_falls_back_whole_payload_to_live() {
        let p = parse_two_slice("  plain prose, no tags\n");
        assert_eq!(p.live.as_deref(), Some("plain prose, no tags"));
        assert_eq!(p.project, None);
    }

    // [unit->REQ-HAZARD-ENVELOPE-PARSER-SAFE]
    #[test]
    fn parser_unclosed_live_tag_does_not_scoop_project() {
        let raw = "<live-context>\noops no close\n<project-context>\nproject body\n</project-context>";
        let p = parse_two_slice(raw);
        assert_eq!(p.live, None);
        assert_eq!(p.project.as_deref(), Some("project body"));
    }

    // [unit->REQ-HAZARD-ENVELOPE-PARSER-SAFE]
    #[test]
    fn parser_out_of_order_tags_both_extracted() {
        let raw = "<project-context>P first</project-context>\n<live-context>L after</live-context>";
        let p = parse_two_slice(raw);
        assert_eq!(p.live.as_deref(), Some("L after"));
        assert_eq!(p.project.as_deref(), Some("P first"));
    }

    // [unit->REQ-HAZARD-ENVELOPE-PARSER-SAFE]
    #[test]
    fn parser_empty_body_is_some_empty_and_nested_tags_preserved() {
        let p = parse_two_slice("<live-context></live-context><project-context></project-context>");
        assert_eq!(p.live.as_deref(), Some(""));
        assert_eq!(p.project.as_deref(), Some(""));

        let nested = "<live-context>\nrole: x\nfoo\n</live-context>";
        let p2 = parse_two_slice(nested);
        assert_eq!(p2.live.as_deref(), Some("role: x\nfoo"));
    }
}


Build + test the crate.


Bash

cd "C:/Users/decid/Documents/projects/spt-core" && cargo test -p spt-proto 2>&1 | grep -E "error|warning: unused|test result|running|passed|failed|Compiling spt-proto|Finished" | head -25

Build and test spt-proto


   Compiling spt-proto v0.0.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-proto)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.53s
running 9 tests
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


9/9 tests pass. Wiring traceability: add `crates` scan root + activate the two hazards.


11
12	[scan]
13	roots = ["src", "tests", "docs"]
14

[scan]
roots = ["src", "tests", "docs"]


Edit

[scan]
roots = ["src", "tests", "docs", "crates"]


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)

[scan]
roots = ["src", "tests", "docs", "crates"]

# installed CLI at M0 (the binary isn't installed yet).

+roots = ["src", "tests", "docs", "crates"]

# Default policy applied at activation: doc + impl + unit. Networking/lifecycle


id = "REQ-HAZARD-ENVELOPE-DECODE-ORDER"
title = "Envelope decode order, ampersand decoded last (4.1)"
required_stages = []

id = "REQ-HAZARD-ENVELOPE-DECODE-ORDER"
title = "Envelope decode order, ampersand decoded last (4.1)"
required_stages = ["impl", "unit"]  # activated M0: spt-proto envelope codec


id = "REQ-HAZARD-ENVELOPE-DECODE-ORDER"
title = "Envelope decode order, ampersand decoded last (4.1)"
required_stages = []

id = "REQ-HAZARD-ENVELOPE-DECODE-ORDER"
title = "Envelope decode order, ampersand decoded last (4.1)"
required_stages = ["impl", "unit"]  # activated M0: spt-proto envelope codec

+required_stages = ["impl", "unit"]  # activated M0: spt-proto envelope codec

title = "Two-slice envelope parser is panic-free and tolerant (4.2)"


id = "REQ-HAZARD-ENVELOPE-PARSER-SAFE"
title = "Two-slice envelope parser is panic-free and tolerant (4.2)"
required_stages = []

id = "REQ-HAZARD-ENVELOPE-PARSER-SAFE"
title = "Two-slice envelope parser is panic-free and tolerant (4.2)"
required_stages = ["impl", "unit"]  # activated M0: spt-proto two-slice parser


id = "REQ-HAZARD-ENVELOPE-PARSER-SAFE"
title = "Two-slice envelope parser is panic-free and tolerant (4.2)"
required_stages = []

id = "REQ-HAZARD-ENVELOPE-PARSER-SAFE"
title = "Two-slice envelope parser is panic-free and tolerant (4.2)"
required_stages = ["impl", "unit"]  # activated M0: spt-proto two-slice parser

+required_stages = ["impl", "unit"]  # activated M0: spt-proto two-slice parser

title = "Stale registry entries degrade to fallback, never hard-fail (4.3)"


Neutralize the stray example tags in TRACEABILITY.md so they don't count as false evidence (a `[impl->REQ-PAIR-1]` in a doc would falsely satisfy PAIR-1's impl stage later).


22	### Tag examples
23
24	```rust
25	// [impl->REQ-PAIR-1]
26	fn spake2_pair(...) { ... }
27	```
28	```rust
29	// [unit->REQ-HAZARD-ENVELOPE-DECODE-ORDER]
30	#[test] fn decodes_amp_last() { ... }
31	```
32	```markdown
33	<!-- [doc->REQ-NET-2] -->
34	## Relay dependency
35	```

### Tag examples

```rust
// [impl->REQ-PAIR-1]
fn spake2_pair(...) { ... }
```
```rust
// [unit->REQ-HAZARD-ENVELOPE-DECODE-ORDER]
#[test] fn decodes_amp_last() { ... }
```
```markdown
<!-- [doc->REQ-NET-2] -->
## Relay dependency
```


### Tag examples

```rust
// [impl->REQ-PAIR-1]
fn spake2_pair(...) { ... }
```
```rust
// [unit->REQ-HAZARD-ENVELOPE-DECODE-ORDER]
#[test] fn decodes_amp_last() { ... }
```
```markdown
<!-- [doc->REQ-NET-2] -->
## Relay dependency
```

### Tag examples

Real tags have no spaces (`[stage->REQ-ID]`). The examples below use a space
after `->` purely so the scanner does not count these illustrations as live
evidence — drop the space in real code.

```rust
// [impl-> REQ-PAIR-1]
fn spake2_pair(...) { ... }
```
```rust
// [unit-> REQ-HAZARD-ENVELOPE-DECODE-ORDER]
#[test] fn decodes_amp_last() { ... }
```
```markdown
<!-- [doc-> REQ-NET-2] -->
## Relay dependency
```


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

### Tag examples

Real tags have no spaces (`[stage->REQ-ID]`). The examples below use a space
after `->` purely so the scanner does not count these illustrations as live
evidence — drop the space in real code.

```rust
// [impl-> REQ-PAIR-1]
fn spake2_pair(...) { ... }
```
```rust
// [unit-> REQ-HAZARD-ENVELOPE-DECODE-ORDER]
#[test] fn decodes_amp_last() { ... }
```
```markdown
<!-- [doc-> REQ-NET-2] -->
## Relay dependency
```

# Traceability development contract

> How spt-core uses [`traceable-reqs`](https://github.com/BigscreenVR/traceable-reqs) to keep every requirement traced from doc → impl → test. The manifest is `traceable-reqs.toml` (seeded from PRD `R-*` + KNOWN-HAZARDS invariants). This contract makes the trace pay off instead of rotting.

## Why

The whole point of the rebuild is to *not* re-introduce the sister project's bugs and to ship the PRD's scope verifiably. A requirement that isn't traced to real evidence (a doc, an implementation, a test) is a requirement you're *hoping* is met. This contract converts that hope into a build that fails when evidence is missing.

## The contract (rules)

1. **The manifest is the authoritative requirement registry.** Every PRD `R-*` requirement and every KNOWN-HAZARDS invariant exists in `traceable-reqs.toml` as a `REQ-*` id. No work without a REQ; no REQ without intent to satisfy. The PRD holds the prose; the manifest holds the id + `required_stages`.
2. **Tag in the same change as the evidence.** When you write the function / test / doc-section that satisfies a stage, add its `[<stage>-><REQ-ID>]` tag in *that same commit*. Never "tag later" — this single rule is what stops the trace from drifting.
3. **Evidence-proximate tags, not file-tops.** A tag sits on or immediately above the real function, test, doc section, config entry, or workflow step that proves the stage. One tag = one piece of real evidence. Tags at file tops to satisfy coverage are noise and usually wrong.
4. **KNOWN-HAZARDS are first-class requirements.** Each invariant is a `REQ-HAZARD-*` requiring `unit` (and `int` where cross-process/cross-node). A hazard cannot be "covered" without a test tagging it — the anti-regression promise becomes mechanical (PRD success criterion #6).
5. **Activation, not premature failure.** Every requirement starts `required_stages = []` (inactive) so `check` stays green pre-code. A **milestone (M0–M5) activates** the requirements it delivers by setting their real `required_stages`. Deferred items (`docs/DEFERRED.md`) stay `[]` until promoted.
6. **Scan roots stay honest.** `[scan].roots` includes every evidence location (`src`, `tests`, `docs`, and later `.github/workflows`, `scripts`). Audit roots whenever a new evidence dir appears — a missing root makes evidence *silently* vanish from the trace.

### Stages

`doc` (prose/design/rustdoc) · `impl` (production code) · `unit` (unit test) · `int` (integration / e2e / cross-node). Default activation policy: `["doc","impl","unit"]`; networking/lifecycle/cross-node reqs add `int`.

### Tag examples

```rust
// [impl->REQ-PAIR-1]
fn spake2_pair(...) { ... }
```
```rust
// [unit->REQ-HAZARD-ENVELOPE-DECODE-ORDER]
#[test] fn decodes_amp_last() { ... }
```
```markdown
<!-- [doc->REQ-NET-2] -->
## Relay dependency
```

## Enforcement (four layers — defense in depth)

1. **CI gate (hard backstop).** `traceable-reqs check --json` runs on every PR/push; exit-1 (`missing_stage` / `undeclared_id` / `parse_error` / `manifest_error`) **fails the build**. A PR can't merge if it leaves an *activated* req's required stage uncovered. Runs alongside the DOCS-STRATEGY CI gates.
2. **Milestone activation gate.** Starting a milestone flips its reqs from `[]` to the real policy; the milestone **is not done until `check` is green for its reqs**. Coverage grows *with* the project instead of failing on day one.
3. **Agent-facing rule.** The repo `CLAUDE.md` instructs every dev-agent: tag `[<stage>-><REQ-ID>]` in the same change as the evidence, and run `traceable-reqs check` before declaring done. Since spt-core is built *with* agents, the contract must be machine-followable — agent discipline + CI as the net.
4. **Quality audit (anti-box-ticking).** `check` proves a tag *exists*; `traceable-reqs review` + `lint` audit whether tags sit near *real* evidence and whether titles are meaningful. Run advisory in CI (or periodically). Presence = hard gate; quality = audited.

Optional fast-feedback: a pre-push git hook running `traceable-reqs check` locally.

## CI snippet (wire at M0, once there is a Rust project + CI)

```yaml
# .github/workflows/traceability.yml
name: traceability
on: [push, pull_request]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install traceable-reqs
        run: gh release download --repo BigscreenVR/traceable-reqs --pattern '*linux-x86_64' --output traceable-reqs && chmod +x traceable-reqs
        env: { GH_TOKEN: ${{ github.token }} }
      - name: Coverage gate (hard)
        run: ./traceable-reqs check --json
      - name: Quality audit (advisory)
        run: ./traceable-reqs lint || true
```

## Lifecycle

- **Now (planning):** manifest seeded, all reqs inactive. `check` is green (nothing required yet).
- **M0:** install the CLI, validate the seed against it, wire the CI workflow, **activate M0 reqs** (`REQ-ARCH-*`, `REQ-HAZARD-*` for the commodity layer), tag as code lands.
- **M1–M5:** each milestone activates + covers its requirements before it's called done.

+Real tags have no spaces (`[stage->REQ-ID]`). The examples below use a space

+after `->` purely so the scanner does not count these illustrations as live

+evidence — drop the space in real code.


Bash

cd "C:/Users/decid/Documents/projects/spt-core" && traceable-reqs check 2>&1 | grep -E "ENVELOPE-DECODE|ENVELOPE-PARSER|PAIR-1|NET-2|requirements:|Findings|\[must\]|parse_error|error" | head -25; echo "EXIT=${PIPESTATUS[0]}"

Run traceable-reqs check after M0 activation


95 requirements: 95 complete, 0 incomplete, 4 findings
  [OK] REQ-HAZARD-ENVELOPE-DECODE-ORDER  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-ENVELOPE-PARSER-SAFE  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-NET-2  required: []  stages: -doc -impl -unit -int
  [OK] REQ-PAIR-1  required: []  stages: -doc -impl -unit -int
Findings:
  [must] parse_error docs/TRACEABILITY.md:24 — Tag-shaped token does not match [stage->REQ-ID]
  [must] parse_error docs/TRACEABILITY.md:29 — Tag-shaped token does not match [stage->REQ-ID]
  [must] parse_error docs/TRACEABILITY.md:33 — Tag-shaped token does not match [stage->REQ-ID]
  [must] parse_error docs/TRACEABILITY.md:37 — Tag-shaped token does not match [stage->REQ-ID]
EXIT=1


Space-form still reads as a malformed tag (errors). The inert form is angle-bracketing the stage (like the prose placeholders that passed). Fixing.


Edit

Real tags have no spaces (`[stage->REQ-ID]`). The examples below use a space
after `->` purely so the scanner does not count these illustrations as live
evidence — drop the space in real code.

```rust
// [impl-> REQ-PAIR-1]
fn spake2_pair(...) { ... }
```
```rust
// [unit-> REQ-HAZARD-ENVELOPE-DECODE-ORDER]
#[test] fn decodes_amp_last() { ... }
```
```markdown
<!-- [doc-> REQ-NET-2] -->
## Relay dependency
```

Real tags are `[stage->REQ-ID]` with a bare stage word. The examples below
angle-bracket the stage (`[<impl>->...]`) only so the scanner treats them as
illustrations, not live evidence — write a bare stage word in real code.

```rust
// [<impl>->REQ-PAIR-1]
fn spake2_pair(...) { ... }
```
```rust
// [<unit>->REQ-HAZARD-ENVELOPE-DECODE-ORDER]
#[test] fn decodes_amp_last() { ... }
```
```markdown
<!-- [<doc>->REQ-NET-2] -->
## Relay dependency
```


Real tags are `[stage->REQ-ID]` with a bare stage word. The examples below
angle-bracket the stage (`[<impl>->...]`) only so the scanner treats them as
illustrations, not live evidence — write a bare stage word in real code.

```rust
// [<impl>->REQ-PAIR-1]
fn spake2_pair(...) { ... }
```
```rust
// [<unit>->REQ-HAZARD-ENVELOPE-DECODE-ORDER]
#[test] fn decodes_amp_last() { ... }
```
```markdown
<!-- [<doc>->REQ-NET-2] -->
## Relay dependency
```

# Traceability development contract

> How spt-core uses [`traceable-reqs`](https://github.com/BigscreenVR/traceable-reqs) to keep every requirement traced from doc → impl → test. The manifest is `traceable-reqs.toml` (seeded from PRD `R-*` + KNOWN-HAZARDS invariants). This contract makes the trace pay off instead of rotting.

## Why

The whole point of the rebuild is to *not* re-introduce the sister project's bugs and to ship the PRD's scope verifiably. A requirement that isn't traced to real evidence (a doc, an implementation, a test) is a requirement you're *hoping* is met. This contract converts that hope into a build that fails when evidence is missing.

## The contract (rules)

1. **The manifest is the authoritative requirement registry.** Every PRD `R-*` requirement and every KNOWN-HAZARDS invariant exists in `traceable-reqs.toml` as a `REQ-*` id. No work without a REQ; no REQ without intent to satisfy. The PRD holds the prose; the manifest holds the id + `required_stages`.
2. **Tag in the same change as the evidence.** When you write the function / test / doc-section that satisfies a stage, add its `[<stage>-><REQ-ID>]` tag in *that same commit*. Never "tag later" — this single rule is what stops the trace from drifting.
3. **Evidence-proximate tags, not file-tops.** A tag sits on or immediately above the real function, test, doc section, config entry, or workflow step that proves the stage. One tag = one piece of real evidence. Tags at file tops to satisfy coverage are noise and usually wrong.
4. **KNOWN-HAZARDS are first-class requirements.** Each invariant is a `REQ-HAZARD-*` requiring `unit` (and `int` where cross-process/cross-node). A hazard cannot be "covered" without a test tagging it — the anti-regression promise becomes mechanical (PRD success criterion #6).
5. **Activation, not premature failure.** Every requirement starts `required_stages = []` (inactive) so `check` stays green pre-code. A **milestone (M0–M5) activates** the requirements it delivers by setting their real `required_stages`. Deferred items (`docs/DEFERRED.md`) stay `[]` until promoted.
6. **Scan roots stay honest.** `[scan].roots` includes every evidence location (`src`, `tests`, `docs`, and later `.github/workflows`, `scripts`). Audit roots whenever a new evidence dir appears — a missing root makes evidence *silently* vanish from the trace.

### Stages

`doc` (prose/design/rustdoc) · `impl` (production code) · `unit` (unit test) · `int` (integration / e2e / cross-node). Default activation policy: `["doc","impl","unit"]`; networking/lifecycle/cross-node reqs add `int`.

### Tag examples

Real tags have no spaces (`[stage->REQ-ID]`). The examples below use a space
after `->` purely so the scanner does not count these illustrations as live
evidence — drop the space in real code.

```rust
// [impl-> REQ-PAIR-1]
fn spake2_pair(...) { ... }
```
```rust
// [unit-> REQ-HAZARD-ENVELOPE-DECODE-ORDER]
#[test] fn decodes_amp_last() { ... }
```
```markdown
<!-- [doc-> REQ-NET-2] -->
## Relay dependency
```

## Enforcement (four layers — defense in depth)

1. **CI gate (hard backstop).** `traceable-reqs check --json` runs on every PR/push; exit-1 (`missing_stage` / `undeclared_id` / `parse_error` / `manifest_error`) **fails the build**. A PR can't merge if it leaves an *activated* req's required stage uncovered. Runs alongside the DOCS-STRATEGY CI gates.
2. **Milestone activation gate.** Starting a milestone flips its reqs from `[]` to the real policy; the milestone **is not done until `check` is green for its reqs**. Coverage grows *with* the project instead of failing on day one.
3. **Agent-facing rule.** The repo `CLAUDE.md` instructs every dev-agent: tag `[<stage>-><REQ-ID>]` in the same change as the evidence, and run `traceable-reqs check` before declaring done. Since spt-core is built *with* agents, the contract must be machine-followable — agent discipline + CI as the net.
4. **Quality audit (anti-box-ticking).** `check` proves a tag *exists*; `traceable-reqs review` + `lint` audit whether tags sit near *real* evidence and whether titles are meaningful. Run advisory in CI (or periodically). Presence = hard gate; quality = audited.

Optional fast-feedback: a pre-push git hook running `traceable-reqs check` locally.

## CI snippet (wire at M0, once there is a Rust project + CI)

```yaml
# .github/workflows/traceability.yml
name: traceability
on: [push, pull_request]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install traceable-reqs
        run: gh release download --repo BigscreenVR/traceable-reqs --pattern '*linux-x86_64' --output traceable-reqs && chmod +x traceable-reqs
        env: { GH_TOKEN: ${{ github.token }} }
      - name: Coverage gate (hard)
        run: ./traceable-reqs check --json
      - name: Quality audit (advisory)
        run: ./traceable-reqs lint || true
```

## Lifecycle

- **Now (planning):** manifest seeded, all reqs inactive. `check` is green (nothing required yet).
- **M0:** install the CLI, validate the seed against it, wire the CI workflow, **activate M0 reqs** (`REQ-ARCH-*`, `REQ-HAZARD-*` for the commodity layer), tag as code lands.
- **M1–M5:** each milestone activates + covers its requirements before it's called done.

-Real tags have no spaces (`[stage->REQ-ID]`). The examples below use a space

-after `->` purely so the scanner does not count these illustrations as live

-evidence — drop the space in real code.

+Real tags are `[stage->REQ-ID]` with a bare stage word. The examples below

+angle-bracket the stage (`[<impl>->...]`) only so the scanner treats them as

+illustrations, not live evidence — write a bare stage word in real code.


Bash

cd "C:/Users/decid/Documents/projects/spt-core" && traceable-reqs check 2>&1 | grep -E "requirements:|Findings|\[must\]|ENVELOPE-DECODE|ENVELOPE-PARSER" ; echo "EXIT=${PIPESTATUS[0]}"

Re-run check after fixing example tags


95 requirements: 95 complete, 0 incomplete, 1 findings
  [OK] REQ-HAZARD-ENVELOPE-DECODE-ORDER  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-HAZARD-ENVELOPE-PARSER-SAFE  required: [impl, unit]  stages: -doc +impl +unit -int
Findings:
  [must] parse_error docs/TRACEABILITY.md:24 — Tag-shaped token does not match [stage->REQ-ID]
EXIT=1


Line 24 is my note's own literal `[stage->REQ-ID]`. Angle-bracket it too.


Edit

Real tags are `[stage->REQ-ID]` with a bare stage word. The examples below

Real tags are `[<stage>->REQ-ID]` with a bare stage word. The examples below


# Traceability development contract

> How spt-core uses [`traceable-reqs`](https://github.com/BigscreenVR/traceable-reqs) to keep every requirement traced from doc → impl → test. The manifest is `traceable-reqs.toml` (seeded from PRD `R-*` + KNOWN-HAZARDS invariants). This contract makes the trace pay off instead of rotting.

## Why

The whole point of the rebuild is to *not* re-introduce the sister project's bugs and to ship the PRD's scope verifiably. A requirement that isn't traced to real evidence (a doc, an implementation, a test) is a requirement you're *hoping* is met. This contract converts that hope into a build that fails when evidence is missing.

## The contract (rules)

1. **The manifest is the authoritative requirement registry.** Every PRD `R-*` requirement and every KNOWN-HAZARDS invariant exists in `traceable-reqs.toml` as a `REQ-*` id. No work without a REQ; no REQ without intent to satisfy. The PRD holds the prose; the manifest holds the id + `required_stages`.
2. **Tag in the same change as the evidence.** When you write the function / test / doc-section that satisfies a stage, add its `[<stage>-><REQ-ID>]` tag in *that same commit*. Never "tag later" — this single rule is what stops the trace from drifting.
3. **Evidence-proximate tags, not file-tops.** A tag sits on or immediately above the real function, test, doc section, config entry, or workflow step that proves the stage. One tag = one piece of real evidence. Tags at file tops to satisfy coverage are noise and usually wrong.
4. **KNOWN-HAZARDS are first-class requirements.** Each invariant is a `REQ-HAZARD-*` requiring `unit` (and `int` where cross-process/cross-node). A hazard cannot be "covered" without a test tagging it — the anti-regression promise becomes mechanical (PRD success criterion #6).
5. **Activation, not premature failure.** Every requirement starts `required_stages = []` (inactive) so `check` stays green pre-code. A **milestone (M0–M5) activates** the requirements it delivers by setting their real `required_stages`. Deferred items (`docs/DEFERRED.md`) stay `[]` until promoted.
6. **Scan roots stay honest.** `[scan].roots` includes every evidence location (`src`, `tests`, `docs`, and later `.github/workflows`, `scripts`). Audit roots whenever a new evidence dir appears — a missing root makes evidence *silently* vanish from the trace.

### Stages

`doc` (prose/design/rustdoc) · `impl` (production code) · `unit` (unit test) · `int` (integration / e2e / cross-node). Default activation policy: `["doc","impl","unit"]`; networking/lifecycle/cross-node reqs add `int`.

### Tag examples

Real tags are `[stage->REQ-ID]` with a bare stage word. The examples below
angle-bracket the stage (`[<impl>->...]`) only so the scanner treats them as
illustrations, not live evidence — write a bare stage word in real code.

```rust
// [<impl>->REQ-PAIR-1]
fn spake2_pair(...) { ... }
```
```rust
// [<unit>->REQ-HAZARD-ENVELOPE-DECODE-ORDER]
#[test] fn decodes_amp_last() { ... }
```
```markdown
<!-- [<doc>->REQ-NET-2] -->
## Relay dependency
```

## Enforcement (four layers — defense in depth)

1. **CI gate (hard backstop).** `traceable-reqs check --json` runs on every PR/push; exit-1 (`missing_stage` / `undeclared_id` / `parse_error` / `manifest_error`) **fails the build**. A PR can't merge if it leaves an *activated* req's required stage uncovered. Runs alongside the DOCS-STRATEGY CI gates.
2. **Milestone activation gate.** Starting a milestone flips its reqs from `[]` to the real policy; the milestone **is not done until `check` is green for its reqs**. Coverage grows *with* the project instead of failing on day one.
3. **Agent-facing rule.** The repo `CLAUDE.md` instructs every dev-agent: tag `[<stage>-><REQ-ID>]` in the same change as the evidence, and run `traceable-reqs check` before declaring done. Since spt-core is built *with* agents, the contract must be machine-followable — agent discipline + CI as the net.
4. **Quality audit (anti-box-ticking).** `check` proves a tag *exists*; `traceable-reqs review` + `lint` audit whether tags sit near *real* evidence and whether titles are meaningful. Run advisory in CI (or periodically). Presence = hard gate; quality = audited.

Optional fast-feedback: a pre-push git hook running `traceable-reqs check` locally.

## CI snippet (wire at M0, once there is a Rust project + CI)

```yaml
# .github/workflows/traceability.yml
name: traceability
on: [push, pull_request]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install traceable-reqs
        run: gh release download --repo BigscreenVR/traceable-reqs --pattern '*linux-x86_64' --output traceable-reqs && chmod +x traceable-reqs
        env: { GH_TOKEN: ${{ github.token }} }
      - name: Coverage gate (hard)
        run: ./traceable-reqs check --json
      - name: Quality audit (advisory)
        run: ./traceable-reqs lint || true
```

## Lifecycle

- **Now (planning):** manifest seeded, all reqs inactive. `check` is green (nothing required yet).
- **M0:** install the CLI, validate the seed against it, wire the CI workflow, **activate M0 reqs** (`REQ-ARCH-*`, `REQ-HAZARD-*` for the commodity layer), tag as code lands.
- **M1–M5:** each milestone activates + covers its requirements before it's called done.

-Real tags are `[stage->REQ-ID]` with a bare stage word. The examples below

+Real tags are `[<stage>->REQ-ID]` with a bare stage word. The examples below


Bash

cd "C:/Users/decid/Documents/projects/spt-core" && traceable-reqs check 2>&1 | grep -E "requirements:|Findings|\[must\]" ; echo "EXIT=${PIPESTATUS[0]}"; echo "=== full test suite ==="; cargo test 2>&1 | grep -E "test result|error\[|warning: unused" | head

Final check + full workspace test


95 requirements: 95 complete, 0 incomplete, 0 findings
EXIT=0
=== full test suite ===
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


**Green across the board:** `traceable-reqs check` EXIT=0 (0 findings, both envelope hazards now `[impl,unit]` covered), workspace tests 9/9. First M0 brick is real. Committing.


cd "C:/Users/decid/Documents/projects/spt-core" && git add -A && git status --short && git commit -q -F - <<'EOF'
feat(spt-proto): M0 first brick — envelope codec + parser, 2 hazards covered

Start the cargo workspace and the spt-proto crate (first layer of R-ARCH-1).
Copy-verbatim the stable wire contract from claude_skill_owl per ADR-0001:

- envelope.rs: HTML-entity transport codec (event_body_escape/unescape,
  event_attr_escape, parse_event_from_attr) with the amp-last decode invariant,
  and the two-slice Psyche-context parser (parse_two_slice) — both public SDK.

Two KNOWN-HAZARDS activated and conformance-tested (impl+unit):
- REQ-HAZARD-ENVELOPE-DECODE-ORDER (4.1): amp decoded LAST so a literal
  &lt;EVENT&gt; does not double-decode into framing.
- REQ-HAZARD-ENVELOPE-PARSER-SAFE (4.2): panic-free, tolerant two-slice parser
  (both/either/neither tag, unclosed, out-of-order, empty, nested preserved).

Traceability wired:
- traceable-reqs.toml: add "crates" scan root; activate the two hazards to
  required_stages = ["impl","unit"].
- evidence tagged in-place (// [impl->...] on fns, // [unit->...] on tests).
- TRACEABILITY.md: neutralize illustrative example tags so they stop counting
  as false evidence (angle-bracket the stage).

cargo test: 9/9 pass. traceable-reqs check: green (95 reqs, 0 findings).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EOF
git log --oneline -3

Commit first M0 spt-proto brick


warning: in the working copy of 'docs/TRACEABILITY.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'traceable-reqs.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'Cargo.lock', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'Cargo.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-proto/Cargo.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-proto/src/envelope.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-proto/src/lib.rs', LF will be replaced by CRLF the next time Git touches it
A  Cargo.lock
A  Cargo.toml
A  crates/spt-proto/Cargo.toml
A  crates/spt-proto/src/envelope.rs
A  crates/spt-proto/src/lib.rs
M  docs/TRACEABILITY.md
M  traceable-reqs.toml
4fa2949 feat(spt-proto): M0 first brick — envelope codec + parser, 2 hazards covered
0dfff6a docs: Spike #2 — native iroh NAT traversal across two separate networks PASS
48ec6d4 docs: Spike #2 cross-machine result — two-host Iroh QUIC PASS


Committed `4fa2949`. Pushing.


cd "C:/Users/decid/Documents/projects/spt-core" && git push origin main 2>&1 | tail -3; echo "exit ${PIPESTATUS[0]}"

Push M0 brick to origin


To https://github.com/SaberMage/spt-core.git
   0dfff6a..4fa2949  main -> main
exit 0


Pushed → `origin/main` at `4fa2949`.

## Where we landed

Full arc this session: psyche-sync enabled → Stage A red-team → both de-risk spikes → **M0 started**.

**Build status:**
- `spt-proto` crate live: envelope HTML-entity codec (amp-last) + two-slice parser, copied verbatim per ADR-0001, public SDK.
- 2 KNOWN-HAZARDS activated + conformance-tested (4.1 decode-order, 4.2 parser-safe). `cargo test` 9/9.
- traceability enforcing real evidence: `crates` scan root, in-place tags, check **green** (95 reqs, 0 findings).

**Commits** `a7128b6 → 4fa2949` (6), all pushed:
- Stage A + Spike #1, Spike #2 ×3 (incl. native NAT traversal), M0 brick.

**De-risk verdict:** every load-bearing unknown tested. ADR-0004 split holds (ConPTY), ADR-0002 networking holds (real cross-NAT hole-punch). No architecture falsified.

## Next increment (continuing M0)

`spt-proto` is started, not done. Remaining pieces (R-ARCH-1 / PRD §7.1):
- **endpoint types** (ReadyAgent, LiveAgent, Psyche, Worker, SptNode — open type system)
- **EVENT-PART chunker** + framing (the `<EVENT>` grammar, byte-exact reassembly)
- **Ed25519 identity** (keypair, signing — copy stable format)
- **typed + binary payloads** (R-EP-3)
- then **`spt-store`** (spool DDL, perch layout, trust store, registry) — the next crate

Recommend continuing one brick at a time (next: the EVENT framing + chunker, since the codec it pairs with is already in). Or pause here — clean checkpoint, all green, all pushed.

Want me to keep going on M0, or hold? (Also can drop a `/spt:commune` to checkpoint this cross-machine if useful.)


i remember design and reqs being scoped and settled, but never a formal plan. is a plan yet to come, or were we proceeding without one? or was the roadmap enough of a plan to guide the whole development properly?


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


Quick check that I'm not forgetting an existing plan doc.


cd "C:/Users/decid/Documents/projects/spt-core" && ls *.md docs/*.md 2>/dev/null | grep -iE "plan|spec|task|milestone" ; echo "---roadmap process stmt---"; grep -niE "lightweight|GSD|not a .*plan|structure" ROADMAP.md | head

Look for any existing plan docs + roadmap process statement


---roadmap process stmt---
3:> Lightweight-but-structured path from design → fruition. Not a GSD/phase-gated process — a clear sequence with de-risk gates. Planning corpus is complete (PRD + CONTEXT + 5 ADRs + 6 design docs); this is how we build it.
13:- **Process: lightweight but structured.** GSD is too heavy. This roadmap + the design docs are the structure.


Checked — no `PLAN.md`/`SPEC`/task docs exist, and the roadmap answers your question explicitly. Here's the honest picture.

## What exists (the "plan" stack)

| Layer | Doc | Granularity |
|---|---|---|
| WHAT/WHY | PRD.md | requirements |
| Meaning | CONTEXT.md | glossary/model |
| Decisions | 5 ADRs | rationale |
| HOW (design) | 6 design docs | subsystem contracts |
| **Sequence** | **ROADMAP.md** | **milestones M0–M5 + de-risk gates** |
| Checklist | traceable-reqs.toml | 92 reqs, activate-per-milestone |

## The answer

**No formal plan is "yet to come" — the project decided against one, on purpose.** ROADMAP.md line 13: *"Process: lightweight but structured. GSD is too heavy. This roadmap + the design docs are the structure."* And line 3: *"Not a GSD/phase-gated process — a clear sequence with de-risk gates."*

So: **roadmap + design docs ARE the plan, by deliberate choice.** Not an oversight.

## My honest read on whether that's enough

For **sequencing and de-risk** — yes, plenty. The roadmap nailed Stage A/B (review + two named spikes); that part was a real plan and it worked.

For **executing a milestone** — it's thin. Each M0–M5 is *one sentence*. "M0 — Workspace + spt-proto + spt-store" is ~6 subsystems across 2 crates. So far I've been **improvising the task decomposition** (I just decided "first brick = envelope codec" on the fly). Fine for a small self-evident brick; gets riskier as M0 grows — ordering, what's copy-verbatim vs clean-room, what activates when, integration points.

The traceable-reqs registry is a **completeness checklist** (what must be true), not a **plan** (the order/how). It's a safety net, not a sequence.

## What I'd recommend

A **just-in-time, lightweight per-milestone task plan** — not heavyweight GSD. One thin `M0-PLAN.md`: ordered task list across spt-proto + spt-store, each with its acceptance + which reqs it activates. Bridges the gap between roadmap (milestones) and reqs (checklist), honors the "lightweight" intent, and I write it only for the milestone in front of us (M0 now), not all five upfront.

It's optional — we *can* keep brick-by-brick improvising, leaning on traceable-reqs to catch gaps. But entering a multi-subsystem milestone, ~30 min of M0 planning would de-risk the build the way the spikes de-risked the design.

Want me to draft `M0-PLAN.md`?


yes, and also move into a new branch called 'dev-freeform' in case we later determine this approach was too unstructured. --- separate question: you mentioned a potential "spike #4". would that still be good to do at this point?


Two things: branch to `dev-freeform`, draft `M0-PLAN.md`, and answer the Spike #4 question.

First the branch. Creating `dev-freeform` from current HEAD and switching to it (leaving `main` at `4fa2949` as the baseline — I won't rewrite pushed main history; if you want main rewound to pre-build `0dfff6a` for a truly clean baseline, that's a force-push I'll do only on explicit say-so).


cd "C:/Users/decid/Documents/projects/spt-core" && git checkout -b dev-freeform 2>&1 && git push -u origin dev-freeform 2>&1 | tail -4; echo "branch now: $(git branch --show-current)"

Create and push dev-freeform branch


Switched to a new branch 'dev-freeform'
remote:
branch 'dev-freeform' set up to track 'origin/dev-freeform'.
To https://github.com/SaberMage/spt-core.git
 * [new branch]      dev-freeform -> dev-freeform
branch now: dev-freeform


yes, and also move into a new branch called 'dev-freeform' in case we later determine this approach was too unstructured. --- separate question: you mentioned a potential "spike #4". would that still…


On `dev-freeform`, pushed. Now drafting `M0-PLAN.md`. Quick sister-source recon so the plan cites real copy-verbatim sources.


cd "C:/Users/decid/Documents/projects/claude_skill_owl" && echo "=== EVENT-PART chunker ==="; grep -rln "EVENT-PART\|event_part\|chunk" src/ | head -5; echo "=== identity / ed25519 ==="; grep -rln "ed25519\|Ed25519\|SigningKey\|keypair\|VerifyingKey" src/ | head -5; echo "=== spool schema ==="; grep -rln "CREATE TABLE\|spool" src/common/ | head -5; echo "=== endpoint/agent types ==="; grep -rln "enum.*Type\|AgentType\|endpoint" src/common/types.rs 2>/dev/null | head -3; wc -l src/common/types.rs 2>/dev/null

Locate sister sources for M0 plan citations


=== EVENT-PART chunker ===
src/live/wrapper/mod.rs
src/owl/poll.rs
src/owl/resume.rs
=== identity / ed25519 ===
=== spool schema ===
src/common/list_filter.rs
src/common/mod.rs
src/common/registry.rs
src/common/spool.rs
src/common/wrapper_state.rs
=== endpoint/agent types ===
560 src/common/types.rs
Shell cwd was reset to C:\Users\decid\Documents\projects\spt-core


Key plan input: the sister project has **no Ed25519/identity code** — so M0's signed identity is **clean-room** (new with spt-core's networking), not copy-verbatim. Everything else (envelope, EVENT-PART, spool, registry, types, atomic-write) copies verbatim. Writing the plan.


Write

# M0 Plan — Workspace + commodity layer (`spt-proto` + `spt-store`)

> **Just-in-time, lightweight.** This is the missing granularity between ROADMAP.md
> (milestone sequence) and `traceable-reqs.toml` (requirement checklist) — an ordered
> task list for **M0 only**. It honors the roadmap's "lightweight but structured, GSD
> too heavy" stance (ROADMAP §13). M1–M5 get their own JIT plans when reached.
> Branch: `dev-freeform` (so `main` can stay a baseline if this approach proves too loose).

## Goal

Ship the two bottom crates of the R-ARCH-1 stack — `spt-proto` (wire) and `spt-store`
(persistence) — as a compiling, tested foundation that M1 (local messaging + binary)
can build straight onto. Stable formats are **copied verbatim** from `claude_skill_owl`
(ADR-0001); only the new identity layer is clean-room.

**M0 done =** both crates compile · `cargo test` green · `traceable-reqs check` green with
M0 reqs *activated* (not just present) · CI runs the gate.

## Scope

In: envelope/EVENT grammar + chunker, endpoint types, Ed25519 identity primitive,
wire-version, typed+binary payloads (proto); atomic-write, spool, perch layout, registry
persistence, info.json (store). Out (later milestones): daemon/broker (M2/M3), networking
& pairing (M4), live/Psyche (M2), terminal/PTY (M3), cross-node registry *resolution* (M4 —
M0 builds local persistence only).

## Sequencing rationale

`spt-proto` before `spt-store` (store serializes proto types). Within proto: codec → EVENT
framing → chunker (depends on framing) → types → identity → version/payloads. Within store:
atomic-write first (everything else writes through it) → spool → perch layout → registry →
info.json. Each task lands its evidence tags + activates its reqs in the same commit
(TRACEABILITY rule 1), and `check` must be green before the next task.

## Tasks — `spt-proto`

| # | Task | Source | Reqs / hazards | Acceptance |
|---|------|--------|----------------|------------|
| **T1** | Envelope HTML-entity codec (amp-last) + two-slice parser | copy ✓ `poll.rs` codec + `envelope.rs` | REQ-HAZARD-ENVELOPE-DECODE-ORDER, -PARSER-SAFE | **DONE** `4fa2949` (9 tests) |
| T2 | EVENT envelope grammar: compose/parse `<EVENT attrs>body</EVENT>`, attrs (from/type/seq/id/timestamp) | copy `poll.rs` compose sites + `parse_event_from_attr` | R-ARCH-4; R-EP-3 (framing) | compose→parse round-trip; attr escaping amp-last; malformed → None, no panic |
| T3 | EVENT-PART chunker + byte-exact reassembly (long body → seq=K/M id parts; drop orphans) | copy `poll.rs`/`wrapper/mod.rs`/`resume.rs` | R-ARCH-4 (+ new REQ-HAZARD-EVENTPART-REASSEMBLY) | property test: split(N)→reassemble == identity for random bodies/splits |
| T4 | Endpoint types: ReadyAgent, LiveAgent, Psyche, Worker, SptNode + Shell/PresenceChannel **seams**; open type system | copy/adapt `types.rs` | R-EP-1, R-EP-2 | type model compiles; agent-vs-Shell split present; unknown type tolerated |
| T5 | Ed25519 identity primitive: keypair gen, sign/verify, pubkey↔string | **clean-room** (`ed25519-dalek`; sister has none) | foundation for R-NET/R-PAIR (identity only, not pairing) | sign/verify round-trip; pubkey serialize/parse stable; known-answer test |
| T6 | Wire-protocol version constant + N-1 compat window helper | clean-room | R-ARCH-3 | version exposed; N-1 accept / N-2 reject unit test |
| T7 | Payload model: typed operation commands + arbitrary binary blobs | copy text path + clean-room binary | R-EP-3 | text + binary blob round-trip through envelope encode/decode |

## Tasks — `spt-store`

| # | Task | Source | Reqs / hazards | Acceptance |
|---|------|--------|----------------|------------|
| T8 | Atomic write (tmp + rename + retry on EBUSY) + UNC-prefix strip on serialized paths | copy `owlery.rs` | REQ-HAZARD-EBUSY-RENAME, -UNC-PATH-STRIP | concurrent-writer test tolerates EBUSY; `\\?\` stripped + re-readable |
| T9 | Spool: schema + `drain_non_deferred` vs `drain_all`, deferred rows, EVENT-PART persistence | copy `spool.rs` | REQ-HAZARD-DEFERRED-DRAIN, -DEFERRED-SURVIVE-DRAIN | deferred row survives event-stream drain; non-deferred drains; ordering preserved |
| T10 | Perch layout + single path resolver (no flat/nested siblings) | copy/adapt `perch_path.rs` | REQ-HAZARD-SINGLE-PATH-SOURCE, -WORKER-PATH | all perch paths route one resolver; no divergent sibling layouts |
| T11 | Registry persistence + stale-entry cleanup (local; cross-node *resolution* deferred M4) | copy `registry.rs` | REQ-HAZARD-REGISTRY-STALE-CLEAN; R-INST-7 (partial: local) | stale (dead-PID) entry cleaned before lookup; never hard-fails on stale |
| T12 | `info.json` shape + torn-read tolerance (atomic write + read-retry) | copy verbatim | REQ-HAZARD-INFO-JSON-TORN-READ | concurrent read during write yields valid or retried, never torn |

## Tasks — infra

| # | Task | Reqs | Acceptance |
|---|------|------|------------|
| T13 | CI workflow: `cargo test` + `traceable-reqs check --json` as a hard gate | R-DOCS-5 (partial) | PR fails on red test or missing activated-req evidence |
| T14 | M0 activation sweep: confirm every M0 req has real `required_stages` + green `check` | — | `check` green with all M0 reqs activated |

## M0 requirement-activation map

Activate as each task lands (set `required_stages`, default `["impl","unit"]`; copy-verbatim
hazards that were cross-process in the sister add `int` where a store/IPC boundary exists):

- **Done:** ENVELOPE-DECODE-ORDER, ENVELOPE-PARSER-SAFE (`impl`+`unit`).
- **Proto:** R-ARCH-1/3/4, R-EP-1/2/3, + new EVENTPART-REASSEMBLY hazard.
- **Store:** EBUSY-RENAME, UNC-PATH-STRIP, DEFERRED-DRAIN, DEFERRED-SURVIVE-DRAIN,
  SINGLE-PATH-SOURCE, WORKER-PATH, REGISTRY-STALE-CLEAN, INFO-JSON-TORN-READ; R-INST-7 (partial).

**Stay `[]` (later milestones):** all R-NET/R-PAIR (M4), R-DAEMON/R-API/R-SEAM/R-START (M2),
R-TERM (M3), R-UPD (M3), R-INST-1..6/8 cross-node (M4), R-REACH (M4), the lifecycle/live hazards.

## New requirement to register before its task

- `REQ-HAZARD-EVENTPART-REASSEMBLY` — long-body EVENT-PART split/reassembly is byte-exact;
  orphan parts (seq K/M with no matching 1/M across a session boundary) are dropped silently.
  Add to `traceable-reqs.toml` when starting T3 (TRACEABILITY rule 3: new req before satisfying).

## Risks carried into M0 (from Stage A / spikes)

- None block M0 (the FATALs were ADR-0004 daemon/M3 + ADR-0005 pairing/M4). M0 is pure
  commodity-layer port + the identity primitive; lowest-risk milestone. The open ADR
  decisions (context-merge model, seed-rotation, R-UPD-3 wording) are M3/M4 and do not gate M0.


File created successfully at: C:\Users\decid\Documents\projects\spt-core\M0-PLAN.md (file state is current in your context — no need to Read it back)

# M0 Plan — Workspace + commodity layer (`spt-proto` + `spt-store`)

> **Just-in-time, lightweight.** This is the missing granularity between ROADMAP.md
> (milestone sequence) and `traceable-reqs.toml` (requirement checklist) — an ordered
> task list for **M0 only**. It honors the roadmap's "lightweight but structured, GSD
> too heavy" stance (ROADMAP §13). M1–M5 get their own JIT plans when reached.
> Branch: `dev-freeform` (so `main` can stay a baseline if this approach proves too loose).

## Goal

Ship the two bottom crates of the R-ARCH-1 stack — `spt-proto` (wire) and `spt-store`
(persistence) — as a compiling, tested foundation that M1 (local messaging + binary)
can build straight onto. Stable formats are **copied verbatim** from `claude_skill_owl`
(ADR-0001); only the new identity layer is clean-room.

**M0 done =** both crates compile · `cargo test` green · `traceable-reqs check` green with
M0 reqs *activated* (not just present) · CI runs the gate.

## Scope

In: envelope/EVENT grammar + chunker, endpoint types, Ed25519 identity primitive,
wire-version, typed+binary payloads (proto); atomic-write, spool, perch layout, registry
persistence, info.json (store). Out (later milestones): daemon/broker (M2/M3), networking
& pairing (M4), live/Psyche (M2), terminal/PTY (M3), cross-node registry *resolution* (M4 —
M0 builds local persistence only).

## Sequencing rationale

`spt-proto` before `spt-store` (store serializes proto types). Within proto: codec → EVENT
framing → chunker (depends on framing) → types → identity → version/payloads. Within store:
atomic-write first (everything else writes through it) → spool → perch layout → registry →
info.json. Each task lands its evidence tags + activates its reqs in the same commit
(TRACEABILITY rule 1), and `check` must be green before the next task.

## Tasks — `spt-proto`

| # | Task | Source | Reqs / hazards | Acceptance |
|---|------|--------|----------------|------------|
| **T1** | Envelope HTML-entity codec (amp-last) + two-slice parser | copy ✓ `poll.rs` codec + `envelope.rs` | REQ-HAZARD-ENVELOPE-DECODE-ORDER, -PARSER-SAFE | **DONE** `4fa2949` (9 tests) |
| T2 | EVENT envelope grammar: compose/parse `<EVENT attrs>body</EVENT>`, attrs (from/type/seq/id/timestamp) | copy `poll.rs` compose sites + `parse_event_from_attr` | R-ARCH-4; R-EP-3 (framing) | compose→parse round-trip; attr escaping amp-last; malformed → None, no panic |
| T3 | EVENT-PART chunker + byte-exact reassembly (long body → seq=K/M id parts; drop orphans) | copy `poll.rs`/`wrapper/mod.rs`/`resume.rs` | R-ARCH-4 (+ new REQ-HAZARD-EVENTPART-REASSEMBLY) | property test: split(N)→reassemble == identity for random bodies/splits |
| T4 | Endpoint types: ReadyAgent, LiveAgent, Psyche, Worker, SptNode + Shell/PresenceChannel **seams**; open type system | copy/adapt `types.rs` | R-EP-1, R-EP-2 | type model compiles; agent-vs-Shell split present; unknown type tolerated |
| T5 | Ed25519 identity primitive: keypair gen, sign/verify, pubkey↔string | **clean-room** (`ed25519-dalek`; sister has none) | foundation for R-NET/R-PAIR (identity only, not pairing) | sign/verify round-trip; pubkey serialize/parse stable; known-answer test |
| T6 | Wire-protocol version constant + N-1 compat window helper | clean-room | R-ARCH-3 | version exposed; N-1 accept / N-2 reject unit test |
| T7 | Payload model: typed operation commands + arbitrary binary blobs | copy text path + clean-room binary | R-EP-3 | text + binary blob round-trip through envelope encode/decode |

## Tasks — `spt-store`

| # | Task | Source | Reqs / hazards | Acceptance |
|---|------|--------|----------------|------------|
| T8 | Atomic write (tmp + rename + retry on EBUSY) + UNC-prefix strip on serialized paths | copy `owlery.rs` | REQ-HAZARD-EBUSY-RENAME, -UNC-PATH-STRIP | concurrent-writer test tolerates EBUSY; `\\?\` stripped + re-readable |
| T9 | Spool: schema + `drain_non_deferred` vs `drain_all`, deferred rows, EVENT-PART persistence | copy `spool.rs` | REQ-HAZARD-DEFERRED-DRAIN, -DEFERRED-SURVIVE-DRAIN | deferred row survives event-stream drain; non-deferred drains; ordering preserved |
| T10 | Perch layout + single path resolver (no flat/nested siblings) | copy/adapt `perch_path.rs` | REQ-HAZARD-SINGLE-PATH-SOURCE, -WORKER-PATH | all perch paths route one resolver; no divergent sibling layouts |
| T11 | Registry persistence + stale-entry cleanup (local; cross-node *resolution* deferred M4) | copy `registry.rs` | REQ-HAZARD-REGISTRY-STALE-CLEAN; R-INST-7 (partial: local) | stale (dead-PID) entry cleaned before lookup; never hard-fails on stale |
| T12 | `info.json` shape + torn-read tolerance (atomic write + read-retry) | copy verbatim | REQ-HAZARD-INFO-JSON-TORN-READ | concurrent read during write yields valid or retried, never torn |

## Tasks — infra

| # | Task | Reqs | Acceptance |
|---|------|------|------------|
| T13 | CI workflow: `cargo test` + `traceable-reqs check --json` as a hard gate | R-DOCS-5 (partial) | PR fails on red test or missing activated-req evidence |
| T14 | M0 activation sweep: confirm every M0 req has real `required_stages` + green `check` | — | `check` green with all M0 reqs activated |

## M0 requirement-activation map

Activate as each task lands (set `required_stages`, default `["impl","unit"]`; copy-verbatim
hazards that were cross-process in the sister add `int` where a store/IPC boundary exists):

- **Done:** ENVELOPE-DECODE-ORDER, ENVELOPE-PARSER-SAFE (`impl`+`unit`).
- **Proto:** R-ARCH-1/3/4, R-EP-1/2/3, + new EVENTPART-REASSEMBLY hazard.
- **Store:** EBUSY-RENAME, UNC-PATH-STRIP, DEFERRED-DRAIN, DEFERRED-SURVIVE-DRAIN,
  SINGLE-PATH-SOURCE, WORKER-PATH, REGISTRY-STALE-CLEAN, INFO-JSON-TORN-READ; R-INST-7 (partial).

**Stay `[]` (later milestones):** all R-NET/R-PAIR (M4), R-DAEMON/R-API/R-SEAM/R-START (M2),
R-TERM (M3), R-UPD (M3), R-INST-1..6/8 cross-node (M4), R-REACH (M4), the lifecycle/live hazards.

## New requirement to register before its task

- `REQ-HAZARD-EVENTPART-REASSEMBLY` — long-body EVENT-PART split/reassembly is byte-exact;
  orphan parts (seq K/M with no matching 1/M across a session boundary) are dropped silently.
  Add to `traceable-reqs.toml` when starting T3 (TRACEABILITY rule 3: new req before satisfying).

## Risks carried into M0 (from Stage A / spikes)

- None block M0 (the FATALs were ADR-0004 daemon/M3 + ADR-0005 pairing/M4). M0 is pure
  commodity-layer port + the identity primitive; lowest-risk milestone. The open ADR
  decisions (context-merge model, seed-rotation, R-UPD-3 wording) are M3/M4 and do not gate M0.


Plan written. Linking it from ROADMAP, then committing on `dev-freeform`.


Edit

4. Then start **M0** (workspace + `spt-proto` + `spt-store`).

4. **M0** (workspace + `spt-proto` + `spt-store`) — **started**; task breakdown in [`M0-PLAN.md`](./M0-PLAN.md). Each milestone gets its own just-in-time plan (this one is M0); the roadmap stays the milestone sequence, the JIT plan is the task layer.


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

# spt-core Roadmap

> Lightweight-but-structured path from design → fruition. Not a GSD/phase-gated process — a clear sequence with de-risk gates. Planning corpus is complete (PRD + CONTEXT + 5 ADRs + 6 design docs); this is how we build it.

## End goal

A harness-independent core for an agent ecosystem — messaging, live-agent lifecycle, terminal hosting, **first-class P2P networking**, and a runtime-manifest harness contract — shipped as Rust crates + a single `spt` binary. The proof: the same agent runs natively across all of a user's machines with a synced mind, reachable and operable from anywhere, with seamless self-update and zero-config cross-machine messaging. First thing built atop it: a rebuilt `spt` plugin at parity with today's modern SPT.

## Scope decisions (locked)

- **v1 = the full PRD, networking included.** The networked element is the *reason* spt-core exists; it is not deferrable to v1.1.
- **The hard freeze on `claude_skill_owl` is not a clock.** It works today and delivers value as-is while spt-core is built — no schedule pressure from it.
- **Process: lightweight but structured.** GSD is too heavy. This roadmap + the design docs are the structure.

## Path to fruition (sequence)

**Stage A — Red-team review (before building).** Pressure-test the design adversarially. Focus on the riskiest architectural decisions, not bikeshedding. Tools: `plan-eng-review` (architecture) + `codex` (adversarial second opinion). Scope-cutting is *not* a goal (v1 is locked as full PRD), but surfacing contradictions / over-engineering / unproven assumptions is.

**Stage B — De-risk spikes.** Throwaway proofs of the load-bearing unknowns — anything that, if it fails, forces a redesign. Do these before committing to the full build. See *De-risk spikes* below.

**Stage C — Walking skeleton.** Build the killer-quickstart slice end-to-end through the real crate boundaries: **two agents exchange a message** (first local; ultimately cross-node — the real proof). Validates the whole stack early; then thicken.

**Stage D — Phased build (M0–M5).** Dependency-ordered, each milestone ships something usable.

## Milestones (all v1)

- **M0 — Workspace + commodity layer.** Cargo workspace; `spt-proto` (wire envelope grammar, endpoint types, framing, Ed25519 identity, typed+binary payloads) + `spt-store` (spool, perch layout, trust store, registry persistence). Copy-verbatim the stable formats from the sister project (ADR-0001).
- **M1 — Local messaging + binary.** `spt-msg` (delivery TCP+spool, routing, send/ring/ready) + the `spt` binary + CLI. **= the killer quickstart (local): two agents exchange a message.**
- **M2 — Harness contract + lifecycle.** `spt-runtime` (AgentRuntime, ManifestRuntime, manifest schema) + `spt-live` (Psyche/pulse/commune/signoff) + the `api` subcommand surface + both startup topologies. **Rebuild the `spt` plugin to parity** (the stated first sub-project).
- **M3 — Terminal wrapper + daemon + self-update.** `spt-term` (session-surface, PTY, broker) + the consolidated `spt-daemon` (broker/brain split) + peer-propagated signed self-update.
- **M4 — Networking + instances.** `spt-net` (Iroh WAN, mDNS LAN, TOTP-SPAKE2 pairing, subnet registry) + the multi-instance model + cross-node Psyche sync (P2P, opt-in GitHub hub). **= the real proof: cross-node message + drive an agent on machine A from machine B.**
- **M5 — Shells, presence, deferred capabilities.** Shell adapters + sleep/wake, PresenceChannel, and the consent-gated capabilities (remote-exec, instantiate-anywhere) landing into the v1 consent seam.

## De-risk spikes (do early — these can invalidate the architecture)

1. **Broker/brain no-terminate handoff** — prove a PTY child + open socket survive a daemon-*logic* restart (FD-passing or stable-broker shim). The hardest invariant in the design (ADR-0004).
2. **Iroh + mDNS smoke test** — two nodes, separate NATs, one message end-to-end; measure direct-vs-relay + binary size (research brief §5). Validates the whole networking premise.
3. **ConPTY + `portable-pty`** — host and drive a real session in a broker PTY on Windows + Linux.
4. **Manifest-driven lifecycle** — a manifest drives one real Claude Code session through spawn → bind → commune → signoff, proving harness-independence works in practice.

## Immediate next steps

Progress (2026-05-31):
- ✅ **Stage A — codex adversarial pass** — done. 14 findings (5 FATAL, 9 SERIOUS) in `docs/reviews/STAGE-A-codex-redteam.md`.
- ✅ **Spike #1 (broker/brain handoff)** — PASS on Windows ConPTY. `docs/spikes/SPIKE-01-broker-handoff.md`. Found+fixed the ConPTY DSR-stall hazard. 4 open gaps carried to M3.
- ✅ **Spike #2 (Iroh smoke test)** — PASS (single-host). `docs/spikes/SPIKE-02-iroh-smoke.md`. iroh 0.98 API workable; 13.57 MB release. Cross-NAT/relay proof deferred to M4.
- ✅ **Findings folded** into ADR-0002/0003/0004/0005 + KNOWN-HAZARDS (+3) + traceable-reqs.toml (+4 REQ-HAZARD).

Remaining before M0:
1. **Stage A — `plan-eng-review`** (interactive architecture pass; second half of Stage A).
2. **Spike #4 (manifest-driven lifecycle)** — a manifest drives one real session spawn→bind→commune→signoff. (Spike #3 ConPTY hosting is largely covered by Spike #1.)
3. Resolve the open user decisions logged in the ADR amendments (context-merge model, seed-rotation, R-UPD-3 rewording).
4. Then start **M0** (workspace + `spt-proto` + `spt-store`).

## Tooling: requirement traceability (adopted)

`traceable-reqs` is adopted as the requirement-tracing gate. `traceable-reqs.toml` seeds the `REQ-*` registry from PRD `R-*` + KNOWN-HAZARDS invariants (all inactive until milestone activation). The development contract + enforcement (CI gate, milestone activation, agent rule, quality audit) is `docs/TRACEABILITY.md`; the agent rule lives in `CLAUDE.md`. **M0 task:** install the CLI, validate the seed, wire the CI workflow, activate M0 reqs.

## Open threads carried forward

- New deferred items logged this session (`docs/DEFERRED.md`): block-level OCC context merge; the rebalance / Memory-Health pass; the consent framework's gated capabilities.
- The dual-audience docs strategy (`docs/DOCS-STRATEGY.md`) governs the *shipped* docs, authored as code lands.

-4. Then start **M0** (workspace + `spt-proto` + `spt-store`).

+4. **M0** (workspace + `spt-proto` + `spt-store`) — **started**; task breakdown in [`M0-PLAN.md`](./M0-PLAN.md). Each milestone gets its own just-in-time plan (this one is M0); the roadmap stays the milestone sequence, the JIT plan is the task layer.


Bash

cd "C:/Users/decid/Documents/projects/spt-core" && git add M0-PLAN.md ROADMAP.md && git commit -q -F - <<'EOF'
docs: add M0-PLAN.md (just-in-time task plan) + roadmap pointer

Bridges the gap between ROADMAP.md (milestone sequence) and traceable-reqs.toml
(requirement checklist): an ordered, lightweight task breakdown for M0 only.
Honors the roadmap's "lightweight but structured, GSD too heavy" stance; M1-M5
get their own JIT plans when reached.

Covers spt-proto (envelope ✓, EVENT framing, EVENT-PART chunker, endpoint
types, Ed25519 identity [clean-room — sister has no crypto], wire-version,
payloads) and spt-store (atomic-write, spool, perch layout, registry, info.json),
plus CI wiring and the M0 requirement-activation map. Notes one new hazard to
register before its task (EVENTPART-REASSEMBLY).

On dev-freeform so main stays a baseline if the freeform approach proves loose.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EOF
git log --oneline -2 && git push -q origin dev-freeform && echo "pushed"

Commit and push M0 plan on dev-freeform


warning: in the working copy of 'ROADMAP.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'M0-PLAN.md', LF will be replaced by CRLF the next time Git touches it
905c6b4 docs: add M0-PLAN.md (just-in-time task plan) + roadmap pointer
4fa2949 feat(spt-proto): M0 first brick — envelope codec + parser, 2 hazards covered
pushed


Done — `dev-freeform` created & pushed, `M0-PLAN.md` committed (`905c6b4`), ROADMAP points at it. `main` untouched at `4fa2949` as your baseline.

## Spike #4 — my recommendation: **defer it, don't do it now**

Spike #4 = "a manifest drives one real session through spawn→bind→commune→signoff, proving harness-independence."

Why the earlier spikes were worth doing *now* but this one isn't:

**#1 and #2 tested novel, unproven assumptions that could force a redesign of locked ADRs** — no-terminate broker handoff (ConPTY, never done in the sister project) and iroh cross-NAT (brand-new networking). If either failed, ADR-0004/0002 break. They were genuine load-bearing unknowns.

**Spike #4's mechanism is already proven** — `claude_skill_owl` runs spawn→commune→signoff in production today. spt-core's manifest contract is a clean-room *generalization* of a working mechanism, not a leap into the unknown. Low redesign risk.

**As scoped it wouldn't even test the risky part.** The actual risk in the harness contract is **independence** — does it work for a *non-Claude* harness (Codex/Cursor)? One Claude Code session can't prove that; Claude Code is the incumbent the sister project already serves. Proving independence needs a **second adapter** — which is real M2 work (the `api` surface, seams, manifest schema, psyche loop don't exist yet).

**Doing it now = building M2's substrate as throwaway.** Out of dependency order (we're at M0), and it'd duplicate the real thing.

**Better:** fold it into M2. Build the manifest contract for real, then validate with the stated parity rebuild **plus an early second (non-Claude) adapter** — that's the test that actually de-risks independence. If you want an early signal, the right move is a *"drive the lifecycle from Codex via manifest"* check at the **start of M2**, not a throwaway now.

So: #1, #2 done (the ones that could've sunk the design). #3 (ConPTY) was absorbed into #1. #4 → M2. The design is de-risked; remaining unknowns are integration-time, not feasibility.

---

Net state: on `dev-freeform`, M0 planned + first brick in. Ready to execute **T2 (EVENT framing)** next per the plan. Want me to proceed?


<command-message>spt:commune</command-message>
<command-name>/spt:commune</command-name>
<command-args>with immediate next steps and broad summary of the project's status + end goal. we'll resume in the next session</command-args>


Base directory for this skill: C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.22\skills\commune

# /spt:commune

All commands use `$LIVE` env var, auto-injected by the plugin's SessionStart hook. If commands fail with "command not found", restart the Claude Code session so the SessionStart hook re-runs.

> **Identity auto-detection:** Your identity is auto-detected from your session. Pass your ID explicitly only if auto-detection fails.

## Flow

1. **Download Psyche's *current* context** — it shifts during the session as background echo communes fire. Re-download right before composing; the gen-N snapshot you read at session start is already stale:
   ```bash
   $LIVE psyche-download <your-id>
   ```

   > **Output truncation.** If the Bash tool reports `Output too large (N KB)` and saves the full result to a `tool-results/<id>.txt` file, use the `Read` tool on that saved path to access the full content. Do NOT re-run `psyche-download` to grep/awk out sections — repeated invocations re-download the same context, waste tokens, and may duplicate `## Pending Commune` sections in `live_context.md`.

2. **Diff your knowledge against this fresh download** — only what's missing from *it* is the real delta (new work, decisions, intentions). Anything already absorbed is not. If the download includes a `<memformat>` section, use it as a guide for what topics to cover. A `## Pending Commune (uncommitted)` section means a previous commune is still queued — do NOT re-send it.

3. **Commune only the delta** -- use the **Write tool** to create
   `.claude/{your-id}-commune.md` with the two-slice body as its contents
   (see `## Two-slice body shape (Phase 25 D-10/D-11)` below for the required
   envelope wrapping). Do not use Bash/heredoc -- Write is the canonical path:
   no shell escaping, no `EOF` collision risk, and Write auto-creates the
   `.claude/` parent directory.

   Your Self listener detects the file on its next poll iteration, notifies
   your Psyche wrapper, and the wrapper ingests the content via the existing
   Psyche session. On success (subprocess exit code 0) the wrapper deletes the
   file. On error (token rate limit, etc) the file is retained and retried on
   the next consume cycle. If you write again before the wrapper consumes, the
   latest write wins (last-write-wins overwrite semantics).

If Psyche's context is already current, no commune is needed. Say so and move on.

Read `commune.md` (in this skill directory) for the full commune protocol -- when to send, what to include, and diligence triggers.

**Agent-type guard:** Only works on live agents. If target is not live, refuses with: "Communes require a live agent with Psyche."

## Two-slice body shape (Phase 25 D-10/D-11)

The body you write in `.claude/{your-id}-commune.md` MUST itself be wrapped in two-slice envelopes per Phase 25 D-10/D-11. The taxonomy — what belongs in `<live-context>` vs `<project-context>` — is defined canonically in `psyche.md` §`<output_envelope>` (lines 282–323). Read that section as the source of truth; this skill doc teaches only when and how to apply it.

**In-project detection rule (D-25.1-04).** Before composing your commune body, run `$LIVE psyche-download <your-id>` (Step 1 of `## Flow` above) and inspect the `<current ... />` tag at the top of the output. The `<current/>` tag carries a `project="..."` attribute whenever any payload is produced — its presence means you are inside a tracked project (inside a git repo, `project` is the repo name; outside a git repo per D-11, `project` is the cwd-basename fallback, which still counts as a project context for routing purposes). A `psyche-download` invocation that returns no payload at all (NO-CONTEXT exit) means no project resolved. So: `project="..."` populated on `<current/>` → emit BOTH envelopes (apply rule 1 or 2 below); no payload at all → emit ONLY `<live-context>` (rule 3). Secondary signal: the `<project-context-resolved name="..."/>` sentinel further down the output indicates the project file already has prior content — useful for spotting first-time-in-project pairings, but NOT the routing rule.

**Three rules (D-25.1-01..03):**

- **In-project, both slices populated** (marker present, you have project-specific content this cycle): emit `<live-context>...</live-context>` AND `<project-context>...</project-context>`, both with non-empty bodies.
- **In-project, no project-specific update this cycle** (marker present, but nothing project-bound to report): emit `<live-context>...</live-context>` AND `<project-context></project-context>` (empty body — the deliberate "in-project but quiet" signal per D-25.1-02). Distinguishable from "no project resolved".
- **Outside any tracked project** (marker absent): emit ONLY `<live-context>...</live-context>`. Do NOT emit an empty `<project-context>` envelope (missing tag means "no project resolved" per D-25.1-03).

**Nested envelopes.** The Phase 23 `<EVENT type="commune">` envelope (see `## Envelope shape (Phase 23 v1.8)` below) is composed by `$LIVE commune` automatically — you NEVER write `<EVENT>` tags. You write ONLY the inner two-slice body. The two envelope layers nest: outer EVENT wraps inner two-slice body.

(See `psyche.md` §`<output_envelope>` for the canonical taxonomy and per-slice content rules.)

## Envelope shape (Phase 23 v1.8)

As of v1.8 Phase 23, commune deliveries are wrapped in a typed EVENT envelope carrying a 5-field stamp (`machine`, `project`, `branch`, `head_sha`, `head_subject`); the prose `COMMUNE (timestamp): body` form is gone.

```xml
<EVENT type="commune" timestamp="2026-05-20T06:00:00-07:00" machine="hostname" project="claude_skill_owl" branch="main" head_sha="abc123..." head_subject="subject line capped at 72 chars">body</EVENT>
```

Outside a git repo (D-11), `branch`, `head_sha`, and `head_subject` are omitted entirely (not empty-string-set):

```xml
<EVENT type="commune" timestamp="2026-05-20T06:00:00-07:00" machine="hostname" project="cwd-basename">body</EVENT>
```

`$LIVE commune` composes this envelope automatically — Self does NOT manually construct EVENT XML. The `body` placeholder in the example above is the two-slice body (see `## Two-slice body shape (Phase 25 D-10/D-11)` above); the two envelope layers nest. The wrapper-side parser ignores unknown attrs, so the stamp fields are forward-compatible with older receivers. For drift detection on resume, see the `live` skill.

ARGUMENTS: with immediate next steps and broad summary of the project's status + end goal. we'll resume in the next session


/spt:commune with immediate next steps and broad summary of the project's status + end goal. we'll resume in the next session


I'm agent doyle (live). Communing. First download current Psyche context to compute the real delta.


$LIVE psyche-download doyle 2>&1 | head -60

Download current Psyche context


<memformat>
<?xml version="1.0"?>
<memformat version="1">
<!-- Changelog: v1 - initial seed -->
<topics>
  <topic name="events">Notable or unusual events and requests</topic>
  <topic name="interactions">Agent interactions -- who, roles, contributions</topic>
  <topic name="reflections">Thoughts on recent work -- what went well, what was tricky</topic>
  <topic name="ideas">Ideas to raise with the user</topic>
  <topic name="improvements">Further improvement ideas for recent problem spaces</topic>
  <topic name="timeline">What happened when, timing, sequencing</topic>
  <topic name="blockers">Current blockers or concerns</topic>
  <topic name="focus">Current focus and immediate next steps</topic>
</topics>
</memformat>
</memformat>

<current machine="HFENDULEAM" project="spt-core" branch="dev-freeform" head_sha="905c6b438f781847bcfda59501d30e073c85a832" head_subject="docs: add M0-PLAN.md (just-in-time task plan) + roadmap pointer" commits_since="0" commits_unpulled="0"/>
Generation 10 advanced. Pushed Stage A + both spikes + **M0 brick 1** (a7128b6..4fa2949 → origin/main). **Traceability complete**: `traceable-reqs check` EXIT=0 (0 findings, both envelope hazards marked `[impl,unit]` covered). M0 increment 1 delivered: cargo workspace + `spt-proto` crate with envelope codec + two-slice parser (verbatim from sister ADR-0001), both KNOWN-HAZARDS activated + conformance-tested, all tests green (9/9). **Next M0 work**: endpoint types (ReadyAgent, LiveAgent, Psyche, Worker, SptNode), EVENT-PART chunker + framing (byte-exact reassembly), Ed25519 identity (keypair/signing), typed + binary payloads, then `spt-store` crate (spool DDL, perch layout, trust store, registry). User querying plan formality — design/reqs settled, no explicit plan document, roadmap guided dev.<project-context-resolved name="spt-core"/>
M0 execution: envelope codec + parser working, 95 req traceability passing, boundary tests passed (ConPTY, NAT hole-punch). Remaining M0 items: framing + chunker, identity layer, payload typing, store crate. User considering next move: continue M0 increment 2 or hold at clean checkpoint.


Psyche has Stage A + spikes + M0 brick 1 already. Delta to commune: the Spike #4 defer decision, the planning-formality resolution (+ M0-PLAN), `dev-freeform` isolation, the enlyzeam test rig, and the requested status/next-steps/end-goal summary for next-session resume. In-project → both slices.