735	            // Seed the dedup cursor at the broker's resume point BEFORE the
736	            // subscribe replay starts arriving, then re-attach from there.
737	            self.session_cursors
738	                .insert(info.session_id, info.resume_seq);
739	            self.subscribe(info.session_id, info.resume_seq)?;
740	            resumed.push(info.session_id);
741	        }
742	        Ok(resumed)
743	    }
744
745	    /// The per-session resume cursor (the next seq still needed for a session
746	    /// re-attached by [`Brain::resume_sessions`]) — observability / tests.
747	    pub fn session_cursor(&self, session_id: u64) -> Option<u64> {
748	        self.session_cursors.get(&session_id).copied()
749	    }
750
751	    /// Run the **PTY-digest feed** for `endpoint_id` (B8): read this session's
752	    /// output events and fold each chunk through `engine`, publishing every change
753	    /// to `hub` (and persisting completed turns when the engine opted in) via
754	    /// [`ingest_chunk`]. This is the brain-side digest projection — the parse is
755	    /// logic, so it lives here, never in the broker (ADR-0004 §B). Returns when
756	    /// the session exits, `stop` is set (checked between events), or the broker
757	    /// connection drops. A non-output event other than `exit` is ignored (the
758	    /// feed only cares about the byte stream).
759	    // [impl->REQ-TERM-4]
760	    pub fn run_digest_feed(
761	        &mut self,
762	        engine: &mut DigestEngine,
763	        hub: &DigestHub,
764	        endpoint_id: &str,
765	        stop: &AtomicBool,
766	    ) -> io::Result<()> {
767	        loop {
768	            if stop.load(Ordering::Relaxed) {
769	                return Ok(());
770	            }
771	            match self.read_event()? {
772	                BrokerEvent::Output { bytes, .. } => ingest_chunk(engine, hub, endpoint_id, &bytes),
773	                BrokerEvent::Exit { .. } => return Ok(()),
774	                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
775	                _ => {}
776	            }
777	        }
778	    }
779
780	    /// Query the broker-owned net endpoint's status (D4a): node id, dialable
781	    /// address, conn count — or `enabled: false` on a net-less broker. Reads
782	    /// until the reply (consuming interleaved events like [`Brain::spawn_session`]).
783	    // [impl->REQ-DAEMON-2]
784	    pub fn net_status(&mut self) -> io::Result<NetStatusReply> {
785	        self.send(KIND_NET_STATUS, serde_json::Value::Null)?;
786	        loop {
787	            match self.read_event()? {
788	                BrokerEvent::NetStatus(s) => return Ok(s),
789	                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
790	                _ => continue,
791	            }
792	        }
793	    }
794

            // Seed the dedup cursor at the broker's resume point BEFORE the
            // subscribe replay starts arriving, then re-attach from there.
            self.session_cursors
                .insert(info.session_id, info.resume_seq);
            self.subscribe(info.session_id, info.resume_seq)?;
            resumed.push(info.session_id);
        }
        Ok(resumed)
    }

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

    /// Run the **PTY-digest feed** for `endpoint_id` (B8): read this session's
    /// output events and fold each chunk through `engine`, publishing every change
    /// to `hub` (and persisting completed turns when the engine opted in) via
    /// [`ingest_chunk`]. This is the brain-side digest projection — the parse is
    /// logic, so it lives here, never in the broker (ADR-0004 §B). Returns when
    /// the session exits, `stop` is set (checked between events), or the broker
    /// connection drops. A non-output event other than `exit` is ignored (the
    /// feed only cares about the byte stream).
    // [impl->REQ-TERM-4]
    pub fn run_digest_feed(
        &mut self,
        engine: &mut DigestEngine,
        hub: &DigestHub,
        endpoint_id: &str,
        stop: &AtomicBool,
    ) -> io::Result<()> {
        loop {
            if stop.load(Ordering::Relaxed) {
                return Ok(());
            }
            match self.read_event()? {
                BrokerEvent::Output { bytes, .. } => ingest_chunk(engine, hub, endpoint_id, &bytes),
                BrokerEvent::Exit { .. } => return Ok(()),
                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
                _ => {}
            }
        }
    }

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


Full data-flow clear. Critical design fork: `HistoryRecord.raw` is **opaque** (history subsystem "delimits, never parses"), but digest needs source-tagging (input/agent/tool). How does the projection tag opaque records? The native test shows `{"r":"u"}`/`{"r":"a"}` — a role discriminator. Need to see how echo-commune (the sibling consumer) handles this — it's the precedent for T7a.


Found 3 files
crates\spt-live\src\history.rs
crates\spt-live\src\lib.rs
crates\spt-live\src\echo.rs


25-// [impl->REQ-HAZARD-DROP-FILE-SINGLE-WRITER]
26-
27:use crate::history::HistoryRecord;
28-use spt_runtime::{AgentRuntime, RuntimeError};
29-use spt_store::atomic::atomic_write_string;
--
93-    id: &str,
94-    keys: &BTreeMap<String, String>,
95:    history: &[HistoryRecord],
96-    commune_dir: &Path,
97-    timeout: Duration,
--
100-    let mut stdin = String::new();
101-    for r in history {
102:        stdin.push_str(&r.raw);
103-        stdin.push('\n');
104-    }
--
145-    const FAIL: &str = "sh -c \"exit 1\"";
146-
147:    fn echo_manifest(cmd: &str, guard: bool) -> Manifest {
148-        let guard_line = if guard {
149-            "recursion_guard_env=\"SPT_ECHO_COMMUNE\"\n"
--
158-    }
159-
160:    fn recs(lines: &[&str]) -> Vec<HistoryRecord> {
161-        lines
162-            .iter()
163:            .map(|l| HistoryRecord { raw: l.to_string() })
164-            .collect()
165-    }
166-
167-    #[test] // [unit->REQ-SEAM-HISTORY] echo consumes history + emits a single-writer commune drop.
168:    fn echo_consumes_history_and_writes_commune() {
169-        let dir = tempfile::tempdir().unwrap();
170-        let rt = ManifestRuntime::new(echo_manifest(PASSTHROUGH, false));
--
195-
196-    #[test] // [unit->REQ-SEAM-HISTORY] the echo child runs with the recursion guard set (won't re-echo).
197:    fn echo_child_is_recursion_guarded() {
198-        let dir = tempfile::tempdir().unwrap();
199-        let rt = ManifestRuntime::new(echo_manifest(GUARD_ECHO, true));
--
215-
216-    #[test] // [unit->REQ-SEAM-HISTORY] a failing summarizer surfaces NonZero, no half-written drop.
217:    fn echo_nonzero_errors() {
218-        let dir = tempfile::tempdir().unwrap();
219-        let rt = ManifestRuntime::new(echo_manifest(FAIL, false));


Decisive finding: echo-commune **never parses** records — it concatenates `r.raw` and pipes to an adapter-owned summarizer command (echo.rs:100-104). spt-core stays opaque. But the digest is a *structured* projection computed by spt-core — it can't reuse that one-shot-summarizer pattern. This is **the central T7a design fork**: how does the projection source-tag opaque records? The native fixture `{"r":"u"}`/`{"r":"a"}` shows a role discriminator already exists. M9-PLAN:162 ("digest needs no manifest seam — rides `[history]`") rules out an adapter digest-normalize command. The CC-fixture E2E (Path A JSONL → digest) implies the normalized line IS directly projectable.

I have the full terrain. Writing the JIT plan now.


408	[[requirements]]
409	id = "REQ-TERM-3"
410	title = "Byte-stream remote terminal streaming for v1"
411	required_stages = ["impl", "unit"]
412	# ── Live activity buffer / PTY digest (ADR-0008, 2026-06-01; activate M3) ──
413	[[requirements]]
414	id = "REQ-TERM-4"
415	title = "Live activity buffer (PTY digest): adapter-supplied patterns over broker PTY, spt digest pull + delta-stream, opt-in Path-B log"
416	required_stages = ["impl", "unit", "int"]
417	[[requirements]]
418	id = "REQ-FRONT-1"
419	title = "Day-one launcher/manager frontend (list/launch/attach/init)"
420	required_stages = []  # rule 5: GUI milestone — no frontend work in M0–M4
421

[[requirements]]
id = "REQ-TERM-3"
title = "Byte-stream remote terminal streaming for v1"
required_stages = ["impl", "unit"]
# ── Live activity buffer / PTY digest (ADR-0008, 2026-06-01; activate M3) ──
[[requirements]]
id = "REQ-TERM-4"
title = "Live activity buffer (PTY digest): adapter-supplied patterns over broker PTY, spt digest pull + delta-stream, opt-in Path-B log"
required_stages = ["impl", "unit", "int"]
[[requirements]]
id = "REQ-FRONT-1"
title = "Day-one launcher/manager frontend (list/launch/attach/init)"
required_stages = []  # rule 5: GUI milestone — no frontend work in M0–M4


1	# M9 — Adapter customization & session surfaces (task plan)
2
3	> **STATUS: PLANNED (2026-06-12, doyle — direct product of the two-day Gateway
4	> grill with the operator; every feature here is operator-ratified and already
5	> normative in CONTEXT.md @ `bd469b9`). Executor: todlando (standing loop) on
6	> operator greenlight. Reviewer: doyle (three gates, §Review gates).** No
7	> design forks remain — the grill closed them; deviations from the CONTEXT.md
8	> definitions are STOP-and-ping, not code-around.
9
10	> Shell-free milestone: nothing here touches the M5 shell machinery (that's
11	> M10). Seven features, each independently shippable, clustered into three
12	> PR-sized waves. The CONTEXT.md entries are the authoritative spec for every
13	> feature — this plan adds only sequencing, evidence choreography, and the
14	> code-survey notes an executor needs.
15
16	## Goal
17
18	Adapters become **customizable without forking** (profiles/strings/hints),
19	the messaging surface gains **user authority** (`user-msg` + Gateway
20	acceptance), agents gain a **durable role** (`live-role.md`), and the session
21	digest is **re-founded on session logs** (the ADR-0008 amendment made real,
22	its REQ evidence honestly re-pointed). Concretely at M9-close:
23
24	- `spt adapter list` shows `claude-spt`, `claude-spt:work` (a local profile
25	  that survived an adapter re-add), and a shipped profile — all spawnable as
26	  distinct adapter options; a loosening overlay was REFUSED at registration.
27	- A hook script fetches per-profile context via `spt adapter get-string` and
28	  a keyword in a user message fires its hint exactly once per session.
29	- A message sent from a Gateway-typed endpoint arrives `user-msg`; the same
30	  payload from a LiveAgent arrives re-stamped `msg`.
31	- `spt endpoint role --overwrite` sets a role that renders FIRST in the next
32	  start-transition injection; nothing automated ever wrote it.
33	- `spt endpoint digest <id>` renders a digest for a **Claude-Code-hosted**
34	  endpoint (topology-independent — impossible under the PTY shape), built
35	  from normalized history records; the PTY-parse engine is GONE; REQ-TERM-4's
36	  title and evidence match reality.
37
38	## What is already satisfied (don't re-build)
39
40	- **Manifest parse + registration** (`spt adapter add/remove`, manifest-first,

# M9 — Adapter customization & session surfaces (task plan)

> **STATUS: PLANNED (2026-06-12, doyle — direct product of the two-day Gateway
> grill with the operator; every feature here is operator-ratified and already
> normative in CONTEXT.md @ `bd469b9`). Executor: todlando (standing loop) on
> operator greenlight. Reviewer: doyle (three gates, §Review gates).** No
> design forks remain — the grill closed them; deviations from the CONTEXT.md
> definitions are STOP-and-ping, not code-around.

> Shell-free milestone: nothing here touches the M5 shell machinery (that's
> M10). Seven features, each independently shippable, clustered into three
> PR-sized waves. The CONTEXT.md entries are the authoritative spec for every
> feature — this plan adds only sequencing, evidence choreography, and the
> code-survey notes an executor needs.

## Goal

Adapters become **customizable without forking** (profiles/strings/hints),
the messaging surface gains **user authority** (`user-msg` + Gateway
acceptance), agents gain a **durable role** (`live-role.md`), and the session
digest is **re-founded on session logs** (the ADR-0008 amendment made real,
its REQ evidence honestly re-pointed). Concretely at M9-close:

- `spt adapter list` shows `claude-spt`, `claude-spt:work` (a local profile
  that survived an adapter re-add), and a shipped profile — all spawnable as
  distinct adapter options; a loosening overlay was REFUSED at registration.
- A hook script fetches per-profile context via `spt adapter get-string` and
  a keyword in a user message fires its hint exactly once per session.
- A message sent from a Gateway-typed endpoint arrives `user-msg`; the same
  payload from a LiveAgent arrives re-stamped `msg`.
- `spt endpoint role --overwrite` sets a role that renders FIRST in the next
  start-transition injection; nothing automated ever wrote it.
- `spt endpoint digest <id>` renders a digest for a **Claude-Code-hosted**
  endpoint (topology-independent — impossible under the PTY shape), built
  from normalized history records; the PTY-parse engine is GONE; REQ-TERM-4's
  title and evidence match reality.

## What is already satisfied (don't re-build)

- **Manifest parse + registration** (`spt adapter add/remove`, manifest-first,


M9-PLAN says "no design forks remain — the grill closed them; deviations from CONTEXT.md are STOP-and-ping." So the source-tagging mechanism may already be specified in CONTEXT.md. Must ground the plan against ratified spec, not invent a fork. Checking CONTEXT.md digest entry.


113:[Omitted long matching line]
153:- **`api history-log`** — Path B: ingest normalized records into spt-core's native history store.
208:[Omitted long matching line]
334:**live activity buffer (session digest)**:
337:[Omitted long matching line]
338:- **Topology-independent.** Because the source is logs, the digest works for harness-hosted endpoints too — Claude Code's per-session JSONL is Path A's worked example. Freshness = file-watch + adapter hook nudges; harnesses with no usable logs push entries directly via **`api digest-entry`** (the Path-B-style door). Near-realtime tailing needs an *incremental* normalize story (design-phase concern: Path A's normalize-command was shaped for occasional pulls).
339:[Omitted long matching line]
340:- **"Option (C)" logging surface:** the digest may also be *persisted* (opt-in per-endpoint) as a coarse activity log appended to the native history store (Path B), on-boundary or periodic — off by default.
341:- **Implementation note:** `spt-daemon::digest` (the `DigestEngine` + manifest `PtyDigest` pattern seam) implements the superseded PTY-parse shape; reconciliation onto the log source is pending milestone work, as is the `docs/MANIFEST.md` pattern-seam schema revision.
343:_Avoid_: "PTY digest" (the superseded source mechanism); parsing agent content out of PTY bytes anywhere.
380:**live role** (`live-role.md`, ratified 2026-06-12 — core milestone A):
381:[Omitted long matching line]
550:**joiner / seed-holder (ceremony roles)**:
633:- **(a) harness-bootstrapped** — a harness plugin installs spt-core as its bootstrap step (today's cplugs model: `/plugin` pulls the plugin, which fetches the spt-core binary). The plugin's role shrinks to this bootstrap; spt-core self-updates thereafter.


334	**live activity buffer (session digest)**:
335	A rolling, human-glanceable view of an endpoint's **recent session activity** — the last ~N user turns (default ~3) plus the agent output between them, with **tool-usage sprints collapsed** to a single entry (e.g. `<endpoint_id> used: **Write** `dir/file.txt` · **Bash** `first ~25 cmd chars` · […]`). Distinct from both the **raw scrollback ring** (unparsed bytes) and full **transcript history** (the conversation behind echo communes/resume): its job is *"what is this agent doing right now, at a glance,"* not conversation replay.
336
337	- **Source = normalized session logs, never PTY parsing** (revised 2026-06-12, Gateway grill — supersedes the ADR-0008 source mechanism). A TUI's PTY stream is a *rendering protocol*, not a content log: scroll/resize trigger full repaints (duplicate content), and semantic turn boundaries drown in repaint soup. The split: **render surfaces read the PTY** (the R-TERM raw byte ring, unchanged); **content surfaces read the logs**. The digest is a rolling tail-projection of the **same normalized history the echo-commune consumes** (history subsystem Path A locate+normalize / Path B native store) — one pipeline, two consumers. The presentation formula (N-turn window, collapsed sprints, ~25-char arg truncation as spt-core presentation) is unchanged from the original conception.
338	- **Topology-independent.** Because the source is logs, the digest works for harness-hosted endpoints too — Claude Code's per-session JSONL is Path A's worked example. Freshness = file-watch + adapter hook nudges; harnesses with no usable logs push entries directly via **`api digest-entry`** (the Path-B-style door). Near-realtime tailing needs an *incremental* normalize story (design-phase concern: Path A's normalize-command was shaped for occasional pulls).
339	- **Two access modes.** **Snapshot pull** — `spt endpoint digest <[subnet:]id[@node]>` returns the current structured buffer (a Shell/CLI/GUI on-demand render; feeds the frontend's "latest session output" pane; a Gateway's agent-window). **Structured-delta stream** — a subscriber (live Shell pane, GUI, Gateway) receives only *changes* (new turn, new tool entry, collapse update), keyed to log records, for resource-efficient near-realtime reflection. **Access is address-gated** — fetch/subscribe is allowed for anyone who can address the endpoint (visible + resolvable per the resolution policy), the same gate as messaging.
340	- **"Option (C)" logging surface:** the digest may also be *persisted* (opt-in per-endpoint) as a coarse activity log appended to the native history store (Path B), on-boundary or periodic — off by default.
341	- **Implementation note:** `spt-daemon::digest` (the `DigestEngine` + manifest `PtyDigest` pattern seam) implements the superseded PTY-parse shape; reconciliation onto the log source is pending milestone work, as is the `docs/MANIFEST.md` pattern-seam schema revision.
342
343	_Avoid_: "PTY digest" (the superseded source mechanism); parsing agent content out of PTY bytes anywhere.

**live activity buffer (session digest)**:
A rolling, human-glanceable view of an endpoint's **recent session activity** — the last ~N user turns (default ~3) plus the agent output between them, with **tool-usage sprints collapsed** to a single entry (e.g. `<endpoint_id> used: **Write** `dir/file.txt` · **Bash** `first ~25 cmd chars` · […]`). Distinct from both the **raw scrollback ring** (unparsed bytes) and full **transcript history** (the conversation behind echo communes/resume): its job is *"what is this agent doing right now, at a glance,"* not conversation replay.

- **Source = normalized session logs, never PTY parsing** (revised 2026-06-12, Gateway grill — supersedes the ADR-0008 source mechanism). A TUI's PTY stream is a *rendering protocol*, not a content log: scroll/resize trigger full repaints (duplicate content), and semantic turn boundaries drown in repaint soup. The split: **render surfaces read the PTY** (the R-TERM raw byte ring, unchanged); **content surfaces read the logs**. The digest is a rolling tail-projection of the **same normalized history the echo-commune consumes** (history subsystem Path A locate+normalize / Path B native store) — one pipeline, two consumers. The presentation formula (N-turn window, collapsed sprints, ~25-char arg truncation as spt-core presentation) is unchanged from the original conception.
- **Topology-independent.** Because the source is logs, the digest works for harness-hosted endpoints too — Claude Code's per-session JSONL is Path A's worked example. Freshness = file-watch + adapter hook nudges; harnesses with no usable logs push entries directly via **`api digest-entry`** (the Path-B-style door). Near-realtime tailing needs an *incremental* normalize story (design-phase concern: Path A's normalize-command was shaped for occasional pulls).
- **Two access modes.** **Snapshot pull** — `spt endpoint digest <[subnet:]id[@node]>` returns the current structured buffer (a Shell/CLI/GUI on-demand render; feeds the frontend's "latest session output" pane; a Gateway's agent-window). **Structured-delta stream** — a subscriber (live Shell pane, GUI, Gateway) receives only *changes* (new turn, new tool entry, collapse update), keyed to log records, for resource-efficient near-realtime reflection. **Access is address-gated** — fetch/subscribe is allowed for anyone who can address the endpoint (visible + resolvable per the resolution policy), the same gate as messaging.
- **"Option (C)" logging surface:** the digest may also be *persisted* (opt-in per-endpoint) as a coarse activity log appended to the native history store (Path B), on-boundary or periodic — off by default.
- **Implementation note:** `spt-daemon::digest` (the `DigestEngine` + manifest `PtyDigest` pattern seam) implements the superseded PTY-parse shape; reconciliation onto the log source is pending milestone work, as is the `docs/MANIFEST.md` pattern-seam schema revision.

_Avoid_: "PTY digest" (the superseded source mechanism); parsing agent content out of PTY bytes anywhere.


Confirmed: CONTEXT.md ratifies the *feature* but leaves the record→Turn projection schema as explicit executor design work ("executor picks the mechanism with the history subsystem's grain"; "incremental normalize story is design-phase concern"). So it's an implementation decision for doyle to vet, not a closed fork I'd be deviating from. Writing the plan.


Write

# M9 Wave 3 (G3) — JIT execution plan: session-digest log projection

> **STATUS: DRAFT for doyle vetting (2026-06-13, todlando).** Wave 3 of M9 per
> `M9-PLAN.md §Wave 3` + ADR-0008 amendment (2026-06-12) + CONTEXT.md `session
> digest` entry (lines 334–343). Branch: `m9-digest-logs` (off `main`, lands as
> one PR). Executes on doyle greenlight; one open design decision (§2) needs a
> ruling before T7a code starts. Source-grounded against the live tree at
> `58f034f`.

---

## 1. What this wave does (recap)

Re-found the live activity buffer on **normalized session logs**, deleting the
PTY-parse engine. Product surface (CONTEXT.md 334–343) is unchanged: N-turn
window (~3), collapsed tool sprints, ~25-char arg truncation, snapshot pull +
structured-delta stream, address-gated, opt-in Option-C persistence. Only the
**source** changes: PTY bytes → the same normalized history records the
echo-commune consumes. New outcome: topology-independent (Claude-Code-hosted
endpoints get digests via Path A JSONL).

**End state at G3:** `spt endpoint digest <id>` renders for a CC-hosted
endpoint; the PTY-parse path is gone; REQ-TERM-4's title + evidence match the
log-source reality.

---

## 2. The one open decision — record→Turn source-tagging (needs a doyle ruling)

The digest is a **structured, source-tagged** projection (input / agent /
collapsed-tool turns) that spt-core computes and refreshes incrementally. But
the history subsystem treats each `HistoryRecord` as an **opaque line**
(`crates/spt-live/src/history.rs:30-35`, "spt-core only delimits, never
parses"), and the echo-commune — the sibling consumer the ADR points at —
**never parses** records: it concatenates `r.raw` and pipes them to an
adapter-owned summarizer command (`crates/spt-live/src/echo.rs:100-104`).

So the projection cannot inherit echo's opaque-pipe pattern: a structured,
incrementally-tailed digest must **source-tag** each record. The question is
*how* — and CONTEXT.md/M9-PLAN explicitly leave the mechanism to the executor
("executor picks the mechanism with the history subsystem's grain"; "incremental
normalize story is design-phase concern"). Three candidates:

| # | Approach | Rides `[history]` (no manifest seam)? | Topology-independent (CC JSONL)? | Incremental-tail friendly? | Verdict |
|---|----------|---------------------------------------|----------------------------------|----------------------------|---------|
| **A — minimal digest-record schema** | Projection parses a small, fixed, spt-core-owned field set (`role` ∈ input/agent/tool, `text`, optional `tool{name,arg}`) from each normalized JSON line; `api digest-entry` pushes the same shape for log-less adapters. | ✅ yes | ✅ yes — the mock adapter's normalize emits this shape | ✅ pure in-proc parse per record | **RECOMMENDED** |
| B — push-only (`api digest-entry` primary) | Adapters push pre-formed entries; log-projection only works when normalized lines already match the schema | ✅ | ⚠ collapses to A for the CC E2E (the JSONL must be projectable) | ✅ | partial — folds into A |
| C — adapter digest-normalize command | Pipe records through an adapter command emitting structured digest JSON (echo-style) | ❌ **adds a manifest seam** — contradicts M9-PLAN:162 "digest needs no manifest seam — it rides `[history]`" | ✅ | ❌ subprocess per refresh kills incremental tail | **REJECTED** |

**Recommended: A.** Rationale: it rides `[history]` (no new manifest seam, per
M9-PLAN:162), is the only one that cleanly satisfies the CC-fixture E2E (the
mock adapter's Path-A normalize emits the schema → projectable directly), and
keeps the echo-commune path untouched (echo still pipes raw to its summarizer;
the digest parses a *documented minimal field set* that is the **digest's own
contract**, not the history subsystem's — history still delimits-only). The
existing native fixtures already carry a role discriminator
(`{"r":"u"}`/`{"r":"a"}`, history.rs:249), so this aligns with the shape
adapters already emit.

**Doyle ruling requested:** confirm A (and the exact minimal field names — I
propose `role`, `text`, optional `tool:{name,arg}`; tool sprints = consecutive
`role:"tool"` records collapsed), or redirect. This is the only blocker before
T7a code.

---

## 3. Retire inventory — surgical, NOT wholesale file-deletion

The Map's "delete entire `digest.rs`" overstates it. The **output + delta
types** must survive (the hub and CLI depend on them); only the **parser** dies.

### KEEP (survives the retire)
- `spt-term`: `Digest`, `Turn`, `DigestEntry`, `ToolUse`, `DigestConfig` — the
  projected-digest **output shape** + presentation knobs (window depth, arg
  truncation). Imported by `digesthub.rs:29`.
- `spt-daemon::digest`: `DigestUpdate` (delta payload), `common_prefix_len`
  (delta diff), the version/`from`/`newly_completed` delta machinery, the
  `ingest_*`→`hub.publish` bridge, `persist_turn` (Option-C). These are
  **source-independent** — they operate on `Digest`/`Turn`, not on bytes.
- `spt-daemon::digesthub` — **entire file unchanged** (hub, control channel,
  `pull_snapshot`/`follow`, render/JSON helpers). It already consumes
  `Digest`+`DigestUpdate`; it never sees the source.

### DELETE (the PTY-parse machinery)
- `spt-term/src/digest.rs`: `DigestParser`, `DigestPatterns`, `DigestError`, the
  regex-over-bytes `feed()`/`digest()` + their unit tests. **Split the file**:
  relocate the surviving output types (`Digest`/`Turn`/`DigestEntry`/`ToolUse`/
  `DigestConfig`) into a `digest_types`-style module (or keep file, gut parser).
- `spt-daemon/src/digest.rs`: `DigestEngine::from_manifest` (PtyDigest seam),
  `with_parts(patterns,…)`, `feed(bytes:&[u8])`, the `parser` field, the
  byte-feed unit tests. The struct **becomes** the projection engine (§4).
- `spt-runtime`: `PtyDigest` struct + `[pty_digest]` serde/validate
  (`manifest.rs:699+`, tests `902/909/924`).
- `spt-daemon/src/brain.rs:760` `run_digest_feed` (the PTY byte-loop) → replaced
  by a record-projection driver (§4).
- `docs/MANIFEST.md:113-145` `[pty_digest]` schema section.
- `crates/spt-daemon/tests/digest.rs` B8 E2E — re-wired (§5), not just deleted.

---

## 4. T7a — projection engine (new code)

1. **Digest-record schema** (pending §2 ruling): a small parse fn
   `record_to_tagged(raw: &str) -> Option<TaggedRecord>` reading the minimal
   field set. Malformed/unknown-role line → skipped (defensive, never panics).
2. **Projection**: `project(records: &[…], config: &DigestConfig) -> Digest` —
   fold tagged records into `Turn`s, applying the unchanged formula (N-turn
   window slide, consecutive-tool collapse into one `ToolSprint`, ~25-char arg
   truncation). Reuses the surviving `Digest`/`Turn`/`DigestEntry` types.
3. **`DigestEngine` re-core**: replace `parser`+`feed(bytes)` with
   `feed_records(&[…]) -> Option<DigestUpdate>` (or `project_into` + the
   existing `common_prefix_len` delta diff — **delta machinery unchanged**, only
   its input changes from a parsed-byte digest to a projected-record digest).
   `snapshot()`, `version()`, `full_update()`, Option-C `newly_completed` all
   stay.
4. **Incremental tail** (design note — executor picks mechanism per the history
   grain): Path B native `history.log` tails cleanly (append-only file →
   file-watch + byte/record cursor). Path A (fetcher / locate+normalize) is a
   bounded full re-pull. **Fallback if Path A grain too coarse** (M9-PLAN:206):
   Path-B-first incremental + Path-A snapshot-only, an accepted degradation
   **flagged at G3, not silently shipped** (surface = a `tracing` warn + a G3
   note). Recommend: cursor-based tail over the normalized record stream,
   re-using `fetch_history`; mechanism final pick is an in-T7a design note.
5. **`api digest-entry`** — the push door for log-less adapters (entries arrive
   pre-formed in the §2 schema). Mirror the `api history-log` →
   `spt::api::reporting::append_history` precedent (a new verb + IPC sink that
   feeds the projection/hub directly).
6. **Units**: projection table from fixture records (input/agent/tool-sprint
   collapse, window slide, arg truncation); `digest-entry` merge; malformed-line
   skip. Tag `[unit->REQ-TERM-4]` **on the new tests** (moved from the deleted
   parser units in the same commit — §6).

---

## 5. T7b — re-wire + retire + CC E2E

1. **Re-wire the feed source**: `brain.rs` driver now reads the endpoint's
   normalized records (via the history subsystem) and calls `feed_records` →
   `hub.publish` (the hub + control channel + CLI are **untouched** — they
   already speak `Digest`/`DigestUpdate`).
2. **Retire** everything in §3 DELETE, with units removed alongside their code.
3. **MANIFEST.md**: drop `[pty_digest]` (113–145); the digest rides `[history]`,
   add a one-line cross-ref noting no digest-specific manifest seam.
4. **CC-fixture E2E** (`int` — the topology-independence proof): a mock adapter
   with `[history]` Path-A `locate_normalize` over a fixture JSONL (CC-style
   per-session log); assert `spt endpoint digest <id>` renders the expected
   N-turn window + collapsed sprint. New test file (proposed
   `crates/spt-daemon/tests/digest.rs` rewritten, or
   `crates/spt/tests/cc_digest_e2e.rs` — executor picks; **flag the filename
   choice at G3**). Tag `[int->REQ-TERM-4]`.

---

## 6. REQ-TERM-4 re-point (restoration discipline — one commit, never un-evidenced)

In the **same commit** the old evidence is deleted:
1. **Retitle** in `traceable-reqs.toml:415`:
   `"Live activity buffer (session digest): projection of normalized session
   logs, spt endpoint digest pull + structured-delta stream, api digest-entry
   push, opt-in Path-B log"` (drops "adapter-supplied patterns over broker
   PTY"). `required_stages = ["impl","unit","int"]` unchanged.
2. **Move tags**: `[impl->REQ-TERM-4]` onto the projection engine + `digest-entry`
   sink; `[unit->REQ-TERM-4]` onto the new projection/merge units;
   `[int->REQ-TERM-4]` onto the CC-fixture E2E — as the old parser/byte-feed/B8
   tags are deleted. **Never a commit where REQ-TERM-4 is un-evidenced**
   (M9-PLAN:204 — the `int` replacement lands with the retire).
3. `traceable-reqs check` EXIT=0 at the commit.

---

## 7. G3 gate checklist (doyle, pre-merge + milestone close)

- [ ] REQ-TERM-4 re-point audit — no orphaned tags, title honest to log-source.
- [ ] Retire completeness — **zero PTY-parse remnants** (`PtyDigest`,
      `DigestParser`, `[pty_digest]`, byte-`feed` all gone; grep clean).
- [ ] CC-fixture E2E green (topology-independence proven).
- [ ] Full suite: workspace tests 0-fail, `clippy -D`, `--no-default-features`,
      xtask check, `traceable-reqs check` EXIT=0.
- [ ] Docs sweep: mdBook digest/manifest pages, CHANGELOG `[Unreleased]`,
      ADR-0008 status note (implementation debt discharged), CONTEXT.md 341
      "implementation note" updated (debt cleared).
- [ ] History-grain fallback decision flagged (Path-A coarse? B-first taken?).
- [ ] E2E filename choice flagged.

---

## 8. Commit choreography (atomic, evidence-in-commit)

1. `feat(digest): record→Turn projection engine + schema (T7a)` — new projection,
   schema parse, units; `DigestEngine` re-core keeping delta machinery.
2. `feat(api): digest-entry push door for log-less adapters (T7a)`.
3. `feat(digest): re-wire brain feed to normalized records; retire PTY-parse +
   re-point REQ-TERM-4 (T7b)` — the retire + retitle + tag-move + CC E2E in one
   commit (restoration discipline; tree never un-evidenced).
4. `docs(m9): MANIFEST drop [pty_digest], ADR-0008/CONTEXT debt cleared,
   CHANGELOG (G3)`.

Each commit: `traceable-reqs check` EXIT=0, CI-targeted local sweep green.

---

## 9. Risks / watch-items

- **Opaque-record vs structured-projection tension** (§2) — the one real
  decision; resolved by scoping the parse to the *digest's* contract, leaving
  history opaque. STOP-and-ping if A is rejected.
- **History-grain coarseness** (M9-PLAN:206) — Path-A re-pull may be too coarse
  for near-realtime tail; fallback is B-first + A-snapshot-only, **flagged not
  silent**.
- **Don't expand scope** — no shell (M10), no rc (M11), no frontend pane. Digest
  presentation stays spt-core-owned; extraction stays adapter-owned.
- **Quietest-tree discipline** — Wave 3 deletes code + re-points active evidence;
  lands last, after G2 + gateway-wan gates close (per M9-PLAN sequencing).


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

# M9 Wave 3 (G3) — JIT execution plan: session-digest log projection

> **STATUS: DRAFT for doyle vetting (2026-06-13, todlando).** Wave 3 of M9 per
> `M9-PLAN.md §Wave 3` + ADR-0008 amendment (2026-06-12) + CONTEXT.md `session
> digest` entry (lines 334–343). Branch: `m9-digest-logs` (off `main`, lands as
> one PR). Executes on doyle greenlight; one open design decision (§2) needs a
> ruling before T7a code starts. Source-grounded against the live tree at
> `58f034f`.

---

## 1. What this wave does (recap)

Re-found the live activity buffer on **normalized session logs**, deleting the
PTY-parse engine. Product surface (CONTEXT.md 334–343) is unchanged: N-turn
window (~3), collapsed tool sprints, ~25-char arg truncation, snapshot pull +
structured-delta stream, address-gated, opt-in Option-C persistence. Only the
**source** changes: PTY bytes → the same normalized history records the
echo-commune consumes. New outcome: topology-independent (Claude-Code-hosted
endpoints get digests via Path A JSONL).

**End state at G3:** `spt endpoint digest <id>` renders for a CC-hosted
endpoint; the PTY-parse path is gone; REQ-TERM-4's title + evidence match the
log-source reality.

---

## 2. The one open decision — record→Turn source-tagging (needs a doyle ruling)

The digest is a **structured, source-tagged** projection (input / agent /
collapsed-tool turns) that spt-core computes and refreshes incrementally. But
the history subsystem treats each `HistoryRecord` as an **opaque line**
(`crates/spt-live/src/history.rs:30-35`, "spt-core only delimits, never
parses"), and the echo-commune — the sibling consumer the ADR points at —
**never parses** records: it concatenates `r.raw` and pipes them to an
adapter-owned summarizer command (`crates/spt-live/src/echo.rs:100-104`).

So the projection cannot inherit echo's opaque-pipe pattern: a structured,
incrementally-tailed digest must **source-tag** each record. The question is
*how* — and CONTEXT.md/M9-PLAN explicitly leave the mechanism to the executor
("executor picks the mechanism with the history subsystem's grain"; "incremental
normalize story is design-phase concern"). Three candidates:

| # | Approach | Rides `[history]` (no manifest seam)? | Topology-independent (CC JSONL)? | Incremental-tail friendly? | Verdict |
|---|----------|---------------------------------------|----------------------------------|----------------------------|---------|
| **A — minimal digest-record schema** | Projection parses a small, fixed, spt-core-owned field set (`role` ∈ input/agent/tool, `text`, optional `tool{name,arg}`) from each normalized JSON line; `api digest-entry` pushes the same shape for log-less adapters. | ✅ yes | ✅ yes — the mock adapter's normalize emits this shape | ✅ pure in-proc parse per record | **RECOMMENDED** |
| B — push-only (`api digest-entry` primary) | Adapters push pre-formed entries; log-projection only works when normalized lines already match the schema | ✅ | ⚠ collapses to A for the CC E2E (the JSONL must be projectable) | ✅ | partial — folds into A |
| C — adapter digest-normalize command | Pipe records through an adapter command emitting structured digest JSON (echo-style) | ❌ **adds a manifest seam** — contradicts M9-PLAN:162 "digest needs no manifest seam — it rides `[history]`" | ✅ | ❌ subprocess per refresh kills incremental tail | **REJECTED** |

**Recommended: A.** Rationale: it rides `[history]` (no new manifest seam, per
M9-PLAN:162), is the only one that cleanly satisfies the CC-fixture E2E (the
mock adapter's Path-A normalize emits the schema → projectable directly), and
keeps the echo-commune path untouched (echo still pipes raw to its summarizer;
the digest parses a *documented minimal field set* that is the **digest's own
contract**, not the history subsystem's — history still delimits-only). The
existing native fixtures already carry a role discriminator
(`{"r":"u"}`/`{"r":"a"}`, history.rs:249), so this aligns with the shape
adapters already emit.

**Doyle ruling requested:** confirm A (and the exact minimal field names — I
propose `role`, `text`, optional `tool:{name,arg}`; tool sprints = consecutive
`role:"tool"` records collapsed), or redirect. This is the only blocker before
T7a code.

---

## 3. Retire inventory — surgical, NOT wholesale file-deletion

The Map's "delete entire `digest.rs`" overstates it. The **output + delta
types** must survive (the hub and CLI depend on them); only the **parser** dies.

### KEEP (survives the retire)
- `spt-term`: `Digest`, `Turn`, `DigestEntry`, `ToolUse`, `DigestConfig` — the
  projected-digest **output shape** + presentation knobs (window depth, arg
  truncation). Imported by `digesthub.rs:29`.
- `spt-daemon::digest`: `DigestUpdate` (delta payload), `common_prefix_len`
  (delta diff), the version/`from`/`newly_completed` delta machinery, the
  `ingest_*`→`hub.publish` bridge, `persist_turn` (Option-C). These are
  **source-independent** — they operate on `Digest`/`Turn`, not on bytes.
- `spt-daemon::digesthub` — **entire file unchanged** (hub, control channel,
  `pull_snapshot`/`follow`, render/JSON helpers). It already consumes
  `Digest`+`DigestUpdate`; it never sees the source.

### DELETE (the PTY-parse machinery)
- `spt-term/src/digest.rs`: `DigestParser`, `DigestPatterns`, `DigestError`, the
  regex-over-bytes `feed()`/`digest()` + their unit tests. **Split the file**:
  relocate the surviving output types (`Digest`/`Turn`/`DigestEntry`/`ToolUse`/
  `DigestConfig`) into a `digest_types`-style module (or keep file, gut parser).
- `spt-daemon/src/digest.rs`: `DigestEngine::from_manifest` (PtyDigest seam),
  `with_parts(patterns,…)`, `feed(bytes:&[u8])`, the `parser` field, the
  byte-feed unit tests. The struct **becomes** the projection engine (§4).
- `spt-runtime`: `PtyDigest` struct + `[pty_digest]` serde/validate
  (`manifest.rs:699+`, tests `902/909/924`).
- `spt-daemon/src/brain.rs:760` `run_digest_feed` (the PTY byte-loop) → replaced
  by a record-projection driver (§4).
- `docs/MANIFEST.md:113-145` `[pty_digest]` schema section.
- `crates/spt-daemon/tests/digest.rs` B8 E2E — re-wired (§5), not just deleted.

---

## 4. T7a — projection engine (new code)

1. **Digest-record schema** (pending §2 ruling): a small parse fn
   `record_to_tagged(raw: &str) -> Option<TaggedRecord>` reading the minimal
   field set. Malformed/unknown-role line → skipped (defensive, never panics).
2. **Projection**: `project(records: &[…], config: &DigestConfig) -> Digest` —
   fold tagged records into `Turn`s, applying the unchanged formula (N-turn
   window slide, consecutive-tool collapse into one `ToolSprint`, ~25-char arg
   truncation). Reuses the surviving `Digest`/`Turn`/`DigestEntry` types.
3. **`DigestEngine` re-core**: replace `parser`+`feed(bytes)` with
   `feed_records(&[…]) -> Option<DigestUpdate>` (or `project_into` + the
   existing `common_prefix_len` delta diff — **delta machinery unchanged**, only
   its input changes from a parsed-byte digest to a projected-record digest).
   `snapshot()`, `version()`, `full_update()`, Option-C `newly_completed` all
   stay.
4. **Incremental tail** (design note — executor picks mechanism per the history
   grain): Path B native `history.log` tails cleanly (append-only file →
   file-watch + byte/record cursor). Path A (fetcher / locate+normalize) is a
   bounded full re-pull. **Fallback if Path A grain too coarse** (M9-PLAN:206):
   Path-B-first incremental + Path-A snapshot-only, an accepted degradation
   **flagged at G3, not silently shipped** (surface = a `tracing` warn + a G3
   note). Recommend: cursor-based tail over the normalized record stream,
   re-using `fetch_history`; mechanism final pick is an in-T7a design note.
5. **`api digest-entry`** — the push door for log-less adapters (entries arrive
   pre-formed in the §2 schema). Mirror the `api history-log` →
   `spt::api::reporting::append_history` precedent (a new verb + IPC sink that
   feeds the projection/hub directly).
6. **Units**: projection table from fixture records (input/agent/tool-sprint
   collapse, window slide, arg truncation); `digest-entry` merge; malformed-line
   skip. Tag `[unit->REQ-TERM-4]` **on the new tests** (moved from the deleted
   parser units in the same commit — §6).

---

## 5. T7b — re-wire + retire + CC E2E

1. **Re-wire the feed source**: `brain.rs` driver now reads the endpoint's
   normalized records (via the history subsystem) and calls `feed_records` →
   `hub.publish` (the hub + control channel + CLI are **untouched** — they
   already speak `Digest`/`DigestUpdate`).
2. **Retire** everything in §3 DELETE, with units removed alongside their code.
3. **MANIFEST.md**: drop `[pty_digest]` (113–145); the digest rides `[history]`,
   add a one-line cross-ref noting no digest-specific manifest seam.
4. **CC-fixture E2E** (`int` — the topology-independence proof): a mock adapter
   with `[history]` Path-A `locate_normalize` over a fixture JSONL (CC-style
   per-session log); assert `spt endpoint digest <id>` renders the expected
   N-turn window + collapsed sprint. New test file (proposed
   `crates/spt-daemon/tests/digest.rs` rewritten, or
   `crates/spt/tests/cc_digest_e2e.rs` — executor picks; **flag the filename
   choice at G3**). Tag `[int->REQ-TERM-4]`.

---

## 6. REQ-TERM-4 re-point (restoration discipline — one commit, never un-evidenced)

In the **same commit** the old evidence is deleted:
1. **Retitle** in `traceable-reqs.toml:415`:
   `"Live activity buffer (session digest): projection of normalized session
   logs, spt endpoint digest pull + structured-delta stream, api digest-entry
   push, opt-in Path-B log"` (drops "adapter-supplied patterns over broker
   PTY"). `required_stages = ["impl","unit","int"]` unchanged.
2. **Move tags**: `[impl->REQ-TERM-4]` onto the projection engine + `digest-entry`
   sink; `[unit->REQ-TERM-4]` onto the new projection/merge units;
   `[int->REQ-TERM-4]` onto the CC-fixture E2E — as the old parser/byte-feed/B8
   tags are deleted. **Never a commit where REQ-TERM-4 is un-evidenced**
   (M9-PLAN:204 — the `int` replacement lands with the retire).
3. `traceable-reqs check` EXIT=0 at the commit.

---

## 7. G3 gate checklist (doyle, pre-merge + milestone close)

- [ ] REQ-TERM-4 re-point audit — no orphaned tags, title honest to log-source.
- [ ] Retire completeness — **zero PTY-parse remnants** (`PtyDigest`,
      `DigestParser`, `[pty_digest]`, byte-`feed` all gone; grep clean).
- [ ] CC-fixture E2E green (topology-independence proven).
- [ ] Full suite: workspace tests 0-fail, `clippy -D`, `--no-default-features`,
      xtask check, `traceable-reqs check` EXIT=0.
- [ ] Docs sweep: mdBook digest/manifest pages, CHANGELOG `[Unreleased]`,
      ADR-0008 status note (implementation debt discharged), CONTEXT.md 341
      "implementation note" updated (debt cleared).
- [ ] History-grain fallback decision flagged (Path-A coarse? B-first taken?).
- [ ] E2E filename choice flagged.

---

## 8. Commit choreography (atomic, evidence-in-commit)

1. `feat(digest): record→Turn projection engine + schema (T7a)` — new projection,
   schema parse, units; `DigestEngine` re-core keeping delta machinery.
2. `feat(api): digest-entry push door for log-less adapters (T7a)`.
3. `feat(digest): re-wire brain feed to normalized records; retire PTY-parse +
   re-point REQ-TERM-4 (T7b)` — the retire + retitle + tag-move + CC E2E in one
   commit (restoration discipline; tree never un-evidenced).
4. `docs(m9): MANIFEST drop [pty_digest], ADR-0008/CONTEXT debt cleared,
   CHANGELOG (G3)`.

Each commit: `traceable-reqs check` EXIT=0, CI-targeted local sweep green.

---

## 9. Risks / watch-items

- **Opaque-record vs structured-projection tension** (§2) — the one real
  decision; resolved by scoping the parse to the *digest's* contract, leaving
  history opaque. STOP-and-ping if A is rejected.
- **History-grain coarseness** (M9-PLAN:206) — Path-A re-pull may be too coarse
  for near-realtime tail; fallback is B-first + A-snapshot-only, **flagged not
  silent**.
- **Don't expand scope** — no shell (M10), no rc (M11), no frontend pane. Digest
  presentation stays spt-core-owned; extraction stays adapter-owned.
- **Quietest-tree discipline** — Wave 3 deletes code + re-points active evidence;
  lands last, after G2 + gateway-wan gates close (per M9-PLAN sequencing).


Plan written to `M9-WAVE3-JIT.md`. Now send it to doyle for vetting.


Bash

$OWL send doyle todlando <<'EOF'
[M9 Wave 3 / G3 — JIT plan up for your vet] todlando here. Wave 3 (digest log-projection) JIT plan written to M9-WAVE3-JIT.md (repo root, branch m9-gateway-wan tree). Grounded against live tree @58f034f + ADR-0008 amendment + CONTEXT.md 334-343.

ONE design decision needs your ruling before T7a code (§2): the digest is a STRUCTURED source-tagged projection, but history records are OPAQUE (history.rs:30) and echo-commune never parses them — it pipes raw to an adapter summarizer (echo.rs:100-104). So the projection must source-tag records itself. My recommended approach A: a minimal spt-core-owned digest-record schema (role ∈ input/agent/tool, text, optional tool{name,arg}) parsed by the projection; api digest-entry pushes the same shape. Rides [history] (no manifest seam, per M9-PLAN:162), satisfies the CC-fixture E2E, leaves echo + history opaque-only untouched. Native fixtures already carry a role discriminator ({"r":"u"}). Rejected approach C (adapter digest-normalize cmd) = adds a manifest seam, contradicts M9-PLAN:162.

Also corrected the Map's "delete entire digest.rs" — it's surgical: output types (Digest/Turn/DigestEntry/ToolUse) + delta machinery (DigestUpdate/common_prefix_len) + digesthub.rs all SURVIVE; only the parser (DigestParser/PtyDigest/byte-feed) dies. §3 has the keep/delete inventory.

REQ-TERM-4 re-point is restoration-style: retitle + move impl/unit/int tags onto new engine+E2E in the SAME commit the old evidence deletes (never un-evidenced). Commit choreography in §8, G3 gate checklist §7.

Asks: (1) confirm approach A + the minimal field names, or redirect; (2) greenlight to execute, or flag concerns. I'll execute on your greenlight and use your judgment to clear hurrdles as they surface. Wave 3 lands last (quietest tree) after G2 + gateway-wan gates close per sequencing.
EOF

Send Wave3 JIT plan to doyle for vetting


➜ SENT:doyle


Sent to doyle. Plan summary:

**`M9-WAVE3-JIT.md`** — Wave 3 / G3 digest log-projection. Grounded against live tree, ADR-0008 amendment, CONTEXT.md.

Key contents:
- **§2 — one open decision for doyle:** records are opaque + echo never parses them, but digest must source-tag → recommended **approach A** (minimal spt-core digest-record schema: role/text/tool, rides `[history]`, no manifest seam). Rejected the adapter-normalize-command variant (adds a seam, breaks M9-PLAN:162).
- **§3 — corrected the Map's overstatement:** retire is surgical. Output types + delta machinery + `digesthub.rs` **survive**; only the parser (`DigestParser`/`PtyDigest`/byte-feed) dies.
- **§4/§5 — T7a** (projection engine, `api digest-entry`, incremental tail) + **T7b** (re-wire, retire, CC-fixture E2E).
- **§6 — REQ-TERM-4 re-point:** retitle + move impl/unit/int tags in the same commit the old evidence deletes.
- **§7/§8 — G3 gate checklist + atomic commit choreography.**

Held the doc uncommitted (draft pending doyle vet). Now standing by for doyle's ruling on §2 + greenlight. Execute on his word.


<task-notification>
<task-id>bmby1p3hm</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/9" id="34e66f38" type="msg" from="doyle">[Wave 3 / G3 JIT — doyle ruling] GREENLIT to execute, with one §2 sharpening that must land before T7a code.

VERDICT: Approach A confirmed. It's the only candidate without a manifest seam (M9-PLAN:162 verified), the only clean CC-fixture E2E path, and it keeps the history subsystem opaque (history.rs:30-31 &qu</EVENT-PART>
<EVENT-PART seq="2/9" id="34e66f38">ot;carries verbatim&quot; + echo.rs:99-104 raw-pipe both stay untouched). No REQ-HAZARD on history opacity, so A trips nothing tracked. §3 surgical retire (keep Digest/Turn/DigestEntry/ToolUse + delta machinery + digesthub; kill only parser/PtyDigest/byte-feed) is correct. §6 restoration discipline (retitle + tag-move + int-replacement SAME </EVENT-PART>
<EVENT-PART seq="3/9" id="34e66f38">commit, never un-evidenced) and §8 choreography: approved as written.

SHARPENING (blocking T7a — resolve, don't hand-wave):
Your §2 cites the native `{&quot;r&quot;:&quot;u&quot;}` fixture (history.rs:249) as evidence A is &quot;free.&quot; It is NOT. That fixture's discriminator is `r` in {u,a} — a DIFFERENT schema than your p</EVENT-PART>
<EVENT-PART seq="4/9" id="34e66f38">roposed digest contract (`role` in {input,agent,tool}). So A is not &quot;adapters already emit this&quot;; A is &quot;spt-core PUBLISHES a digest-record contract that projectable adapters must target.&quot; Name it that way. The resolution must pick ONE explicitly:

  - history-the-subsystem stays opaque (carries `raw` verbatim, adapter</EVENT-PART>
<EVENT-PART seq="5/9" id="34e66f38">-defined — UNCHANGED), AND
  - the DIGEST defines its own spt-core-owned record contract (an overlay on the content of `raw`), AND
  - there is exactly ONE digest record schema. No dual schema, no `r`/`u` to `role`/`input` mapping layer. Migrate the native `{&quot;r&quot;:&quot;u&quot;}` fixture to the digest contract (it's a fixture </EVENT-PART>
<EVENT-PART seq="6/9" id="34e66f38">— cheap), or settle the digest contract AS the native record shape. Your call which, but state it in the plan and in the T7a design note.

Because adapters encode against this contract, treat it as a published surface: enumerate `role` values exactly, document the schema in CONTEXT.md + MANIFEST (replacing the dropped [pty_digest] sect</EVENT-PART>
<EVENT-PART seq="7/9" id="34e66f38">ion, not just deleting it), and keep the unknown-field/malformed→skip forward-compat you already specced. Field names `role` / `text` / optional `tool:{name,arg}` APPROVED. Tool sprints = consecutive `role:&quot;tool&quot;` collapsed: APPROVED.

CARRY-FORWARD (already in your plan, holding you to them at G3):
- Path-A coarse to B-fi</EVENT-PART>
<EVENT-PART seq="8/9" id="34e66f38">rst+A-snapshot fallback: flagged not silent (tracing warn + G3 note). Confirmed acceptable IF flagged.
- E2E filename choice: flag at G3.
- T5 twohost honored-path int (gateway-wan tracked condition) is a SEPARA
...(truncated)</event>
</task-notification>


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


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


doyle **GREENLIT** + a blocking §2 sharpening. Captured parts 1-8 (part 9 truncated — T5 twohost note, gateway-wan scope, separate from G3).

**Ruling:** Approach A confirmed. §3 surgical retire, §6 restoration, §8 choreography — approved. Field names `role`/`text`/`tool{name,arg}` + consecutive-`tool` collapse — approved.

**Sharpening (blocking T7a):**
- A is NOT "adapters already emit this" — the `{"r":"u"}` fixture is `r`∈{u,a}, a *different* schema. A is "spt-core **PUBLISHES** a digest-record contract projectable adapters must target." Name it that way.
- Pick ONE explicitly, no dual schema, no `r`→`role` mapping layer: history stays opaque (`raw` verbatim, UNCHANGED) + digest owns its single contract. Either migrate the native fixture to the digest contract, or settle the contract AS the native shape. My call, must state it.
- Published surface: enumerate `role` exactly, document in CONTEXT.md + MANIFEST (**replace** the dropped `[pty_digest]`, not just delete), keep malformed→skip.

My pick: **migrate the stray fixture** (history must stay opaque for echo/resume — can't make the digest schema the universal native shape). Updating §2 now, then ACK + start.


Edit

## 2. The one open decision — record→Turn source-tagging (needs a doyle ruling)

The digest is a **structured, source-tagged** projection (input / agent /
collapsed-tool turns) that spt-core computes and refreshes incrementally. But
the history subsystem treats each `HistoryRecord` as an **opaque line**
(`crates/spt-live/src/history.rs:30-35`, "spt-core only delimits, never
parses"), and the echo-commune — the sibling consumer the ADR points at —
**never parses** records: it concatenates `r.raw` and pipes them to an
adapter-owned summarizer command (`crates/spt-live/src/echo.rs:100-104`).

So the projection cannot inherit echo's opaque-pipe pattern: a structured,
incrementally-tailed digest must **source-tag** each record. The question is
*how* — and CONTEXT.md/M9-PLAN explicitly leave the mechanism to the executor
("executor picks the mechanism with the history subsystem's grain"; "incremental
normalize story is design-phase concern"). Three candidates:

| # | Approach | Rides `[history]` (no manifest seam)? | Topology-independent (CC JSONL)? | Incremental-tail friendly? | Verdict |
|---|----------|---------------------------------------|----------------------------------|----------------------------|---------|
| **A — minimal digest-record schema** | Projection parses a small, fixed, spt-core-owned field set (`role` ∈ input/agent/tool, `text`, optional `tool{name,arg}`) from each normalized JSON line; `api digest-entry` pushes the same shape for log-less adapters. | ✅ yes | ✅ yes — the mock adapter's normalize emits this shape | ✅ pure in-proc parse per record | **RECOMMENDED** |
| B — push-only (`api digest-entry` primary) | Adapters push pre-formed entries; log-projection only works when normalized lines already match the schema | ✅ | ⚠ collapses to A for the CC E2E (the JSONL must be projectable) | ✅ | partial — folds into A |
| C — adapter digest-normalize command | Pipe records through an adapter command emitting structured digest JSON (echo-style) | ❌ **adds a manifest seam** — contradicts M9-PLAN:162 "digest needs no manifest seam — it rides `[history]`" | ✅ | ❌ subprocess per refresh kills incremental tail | **REJECTED** |

**Recommended: A.** Rationale: it rides `[history]` (no new manifest seam, per
M9-PLAN:162), is the only one that cleanly satisfies the CC-fixture E2E (the
mock adapter's Path-A normalize emits the schema → projectable directly), and
keeps the echo-commune path untouched (echo still pipes raw to its summarizer;
the digest parses a *documented minimal field set* that is the **digest's own
contract**, not the history subsystem's — history still delimits-only). The
existing native fixtures already carry a role discriminator
(`{"r":"u"}`/`{"r":"a"}`, history.rs:249), so this aligns with the shape
adapters already emit.

**Doyle ruling requested:** confirm A (and the exact minimal field names — I
propose `role`, `text`, optional `tool:{name,arg}`; tool sprints = consecutive
`role:"tool"` records collapsed), or redirect. This is the only blocker before
T7a code.

## 2. Record→Turn source-tagging — the digest-record contract (RULED: doyle 2026-06-13)

The digest is a **structured, source-tagged** projection (input / agent /
collapsed-tool turns) that spt-core computes and refreshes incrementally. The
history subsystem treats each `HistoryRecord` as an **opaque line**
(`crates/spt-live/src/history.rs:30-35`, "spt-core only delimits, never
parses"), and the echo-commune — the sibling consumer the ADR points at —
**never parses** records: it concatenates `r.raw` and pipes them to an
adapter-owned summarizer command (`crates/spt-live/src/echo.rs:99-104`). So the
projection cannot inherit echo's opaque-pipe pattern: a structured,
incrementally-tailed digest must **source-tag** each record. CONTEXT.md/M9-PLAN
leave the mechanism to the executor ("executor picks the mechanism with the
history subsystem's grain"; "incremental normalize story is design-phase
concern").

### Approach A — spt-core PUBLISHES a digest-record contract (RULED)

**Correct framing (doyle's sharpening):** A is NOT "adapters already emit this
shape." A is **spt-core publishes a digest-record contract that projectable
adapters must target.** The `{"r":"u"}` line at `history.rs:249` is a
*history-subsystem* test fixture whose discriminator is `r ∈ {u,a}` — a
**different** schema, cited in error in the draft as pre-alignment. It is not
evidence; A is a new published surface, not a free ride.

**The single contract (no dual schema, no mapping layer):**
- **History-the-subsystem stays opaque — UNCHANGED.** It carries `raw` verbatim
  (`history.rs:30-31`), adapter-defined; echo's raw-pipe (`echo.rs:99-104`)
  untouched. No REQ-HAZARD on history opacity is tripped.
- **The digest owns ONE record contract** — an spt-core-published overlay on the
  *content* of `raw`. JSON line: `role ∈ {input, agent, tool}` (enumerated,
  exact), `text: string`, optional `tool: {name, arg}` (present iff
  `role == "tool"`). Tool sprints = **consecutive `role:"tool"` records
  collapsed** into one `ToolSprint`. **APPROVED.**
- **Exactly one schema in the tree.** No `r`/`u → role`/`input` mapping layer,
  ever. **Decision (RULED, my call per doyle): migrate the stray native
  `{"r":"u"}` history-test fixture (`history.rs:249`) to the digest contract
  shape** — cosmetic for that test (it still only exercises *verbatim carry* of
  an opaque line; REQ-SEAM-HISTORY semantics unchanged), but it removes the
  second-schema optics so the codebase shows ONE record JSON shape. Chosen over
  "settle the digest contract AS the native record shape" because native
  history must stay adapter-opaque for echo/resume — the digest schema must not
  become the universal native shape.

**Published-surface treatment (because adapters encode against it):**
- Enumerate `role` values exactly (`input`/`agent`/`tool`) — closed set.
- **Document the contract in CONTEXT.md + `docs/MANIFEST.md`** — the MANIFEST
  `[pty_digest]` section is **replaced** by the digest-record contract doc, not
  merely deleted (it remains the digest's schema home, now log-source-shaped).
- **Forward-compat:** unknown field / malformed line / unknown `role` → **skip**
  the record (never panic, never abort the projection).

**Why A (verified by doyle):** the only candidate with no manifest seam
(M9-PLAN:162 verified), the only clean CC-fixture E2E path (the mock adapter's
Path-A normalize emits the contract → projectable directly), and it keeps the
history subsystem opaque. Rejected alternative: an adapter digest-normalize
command (echo-style) — **adds a manifest seam, contradicts M9-PLAN:162**, and a
subprocess per refresh kills incremental tail.


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

## 2. The one open decision — record→Turn source-tagging (needs a doyle ruling)

The digest is a **structured, source-tagged** projection (input / agent /
collapsed-tool turns) that spt-core computes and refreshes incrementally. But
the history subsystem treats each `HistoryRecord` as an **opaque line**
(`crates/spt-live/src/history.rs:30-35`, "spt-core only delimits, never
parses"), and the echo-commune — the sibling consumer the ADR points at —
**never parses** records: it concatenates `r.raw` and pipes them to an
adapter-owned summarizer command (`crates/spt-live/src/echo.rs:100-104`).

So the projection cannot inherit echo's opaque-pipe pattern: a structured,
incrementally-tailed digest must **source-tag** each record. The question is
*how* — and CONTEXT.md/M9-PLAN explicitly leave the mechanism to the executor
("executor picks the mechanism with the history subsystem's grain"; "incremental
normalize story is design-phase concern"). Three candidates:

| # | Approach | Rides `[history]` (no manifest seam)? | Topology-independent (CC JSONL)? | Incremental-tail friendly? | Verdict |
|---|----------|---------------------------------------|----------------------------------|----------------------------|---------|
| **A — minimal digest-record schema** | Projection parses a small, fixed, spt-core-owned field set (`role` ∈ input/agent/tool, `text`, optional `tool{name,arg}`) from each normalized JSON line; `api digest-entry` pushes the same shape for log-less adapters. | ✅ yes | ✅ yes — the mock adapter's normalize emits this shape | ✅ pure in-proc parse per record | **RECOMMENDED** |
| B — push-only (`api digest-entry` primary) | Adapters push pre-formed entries; log-projection only works when normalized lines already match the schema | ✅ | ⚠ collapses to A for the CC E2E (the JSONL must be projectable) | ✅ | partial — folds into A |
| C — adapter digest-normalize command | Pipe records through an adapter command emitting structured digest JSON (echo-style) | ❌ **adds a manifest seam** — contradicts M9-PLAN:162 "digest needs no manifest seam — it rides `[history]`" | ✅ | ❌ subprocess per refresh kills incremental tail | **REJECTED** |

**Recommended: A.** Rationale: it rides `[history]` (no new manifest seam, per
M9-PLAN:162), is the only one that cleanly satisfies the CC-fixture E2E (the
mock adapter's Path-A normalize emits the schema → projectable directly), and
keeps the echo-commune path untouched (echo still pipes raw to its summarizer;
the digest parses a *documented minimal field set* that is the **digest's own
contract**, not the history subsystem's — history still delimits-only). The
existing native fixtures already carry a role discriminator
(`{"r":"u"}`/`{"r":"a"}`, history.rs:249), so this aligns with the shape
adapters already emit.

**Doyle ruling requested:** confirm A (and the exact minimal field names — I
propose `role`, `text`, optional `tool:{name,arg}`; tool sprints = consecutive
`role:"tool"` records collapsed), or redirect. This is the only blocker before
T7a code.

## 2. Record→Turn source-tagging — the digest-record contract (RULED: doyle 2026-06-13)

The digest is a **structured, source-tagged** projection (input / agent /
collapsed-tool turns) that spt-core computes and refreshes incrementally. The
history subsystem treats each `HistoryRecord` as an **opaque line**
(`crates/spt-live/src/history.rs:30-35`, "spt-core only delimits, never
parses"), and the echo-commune — the sibling consumer the ADR points at —
**never parses** records: it concatenates `r.raw` and pipes them to an
adapter-owned summarizer command (`crates/spt-live/src/echo.rs:99-104`). So the
projection cannot inherit echo's opaque-pipe pattern: a structured,
incrementally-tailed digest must **source-tag** each record. CONTEXT.md/M9-PLAN
leave the mechanism to the executor ("executor picks the mechanism with the
history subsystem's grain"; "incremental normalize story is design-phase
concern").

### Approach A — spt-core PUBLISHES a digest-record contract (RULED)

**Correct framing (doyle's sharpening):** A is NOT "adapters already emit this
shape." A is **spt-core publishes a digest-record contract that projectable
adapters must target.** The `{"r":"u"}` line at `history.rs:249` is a
*history-subsystem* test fixture whose discriminator is `r ∈ {u,a}` — a
**different** schema, cited in error in the draft as pre-alignment. It is not
evidence; A is a new published surface, not a free ride.

**The single contract (no dual schema, no mapping layer):**
- **History-the-subsystem stays opaque — UNCHANGED.** It carries `raw` verbatim
  (`history.rs:30-31`), adapter-defined; echo's raw-pipe (`echo.rs:99-104`)
  untouched. No REQ-HAZARD on history opacity is tripped.
- **The digest owns ONE record contract** — an spt-core-published overlay on the
  *content* of `raw`. JSON line: `role ∈ {input, agent, tool}` (enumerated,
  exact), `text: string`, optional `tool: {name, arg}` (present iff
  `role == "tool"`). Tool sprints = **consecutive `role:"tool"` records
  collapsed** into one `ToolSprint`. **APPROVED.**
- **Exactly one schema in the tree.** No `r`/`u → role`/`input` mapping layer,
  ever. **Decision (RULED, my call per doyle): migrate the stray native
  `{"r":"u"}` history-test fixture (`history.rs:249`) to the digest contract
  shape** — cosmetic for that test (it still only exercises *verbatim carry* of
  an opaque line; REQ-SEAM-HISTORY semantics unchanged), but it removes the
  second-schema optics so the codebase shows ONE record JSON shape. Chosen over
  "settle the digest contract AS the native record shape" because native
  history must stay adapter-opaque for echo/resume — the digest schema must not
  become the universal native shape.

**Published-surface treatment (because adapters encode against it):**
- Enumerate `role` values exactly (`input`/`agent`/`tool`) — closed set.
- **Document the contract in CONTEXT.md + `docs/MANIFEST.md`** — the MANIFEST
  `[pty_digest]` section is **replaced** by the digest-record contract doc, not
  merely deleted (it remains the digest's schema home, now log-source-shaped).
- **Forward-compat:** unknown field / malformed line / unknown `role` → **skip**
  the record (never panic, never abort the projection).

**Why A (verified by doyle):** the only candidate with no manifest seam
(M9-PLAN:162 verified), the only clean CC-fixture E2E path (the mock adapter's
Path-A normalize emits the contract → projectable directly), and it keeps the
history subsystem opaque. Rejected alternative: an adapter digest-normalize
command (echo-style) — **adds a manifest seam, contradicts M9-PLAN:162**, and a
subprocess per refresh kills incremental tail.

-## 2. The one open decision — record→Turn source-tagging (needs a doyle ruling)

+## 2. Record→Turn source-tagging — the digest-record contract (RULED: doyle 2026-06-13)

-collapsed-tool turns) that spt-core computes and refreshes incrementally. But

-the history subsystem treats each `HistoryRecord` as an **opaque line**

+collapsed-tool turns) that spt-core computes and refreshes incrementally. The

+history subsystem treats each `HistoryRecord` as an **opaque line**

-adapter-owned summarizer command (`crates/spt-live/src/echo.rs:100-104`).

+adapter-owned summarizer command (`crates/spt-live/src/echo.rs:99-104`). So the

+projection cannot inherit echo's opaque-pipe pattern: a structured,

+incrementally-tailed digest must **source-tag** each record. CONTEXT.md/M9-PLAN

+leave the mechanism to the executor ("executor picks the mechanism with the

+history subsystem's grain"; "incremental normalize story is design-phase

-So the projection cannot inherit echo's opaque-pipe pattern: a structured,

-incrementally-tailed digest must **source-tag** each record. The question is

-*how* — and CONTEXT.md/M9-PLAN explicitly leave the mechanism to the executor

-("executor picks the mechanism with the history subsystem's grain"; "incremental

-normalize story is design-phase concern"). Three candidates:

+### Approach A — spt-core PUBLISHES a digest-record contract (RULED)

-| # | Approach | Rides `[history]` (no manifest seam)? | Topology-independent (CC JSONL)? | Incremental-tail friendly? | Verdict |

-| **A — minimal digest-record schema** | Projection parses a small, fixed, spt-core-owned field set (`role` ∈ input/agent/tool, `text`, optional `tool{name,arg}`) from each normalized JSON line; `api digest-entry` pushes the same shape for log-less adapters. | ✅ yes | ✅ yes — the mock adapter's normalize emits this shape | ✅ pure in-proc parse per record | **RECOMMENDED** |

-| B — push-only (`api digest-entry` primary) | Adapters push pre-formed entries; log-projection only works when normalized lines already match the schema | ✅ | ⚠ collapses to A for the CC E2E (the JSONL must be projectable) | ✅ | partial — folds into A |

-| C — adapter digest-normalize command | Pipe records through an adapter command emitting structured digest JSON (echo-style) | ❌ **adds a manifest seam** — contradicts M9-PLAN:162 "digest needs no manifest seam — it rides `[history]`" | ✅ | ❌ subprocess per refresh kills incremental tail | **REJECTED** |

+**Correct framing (doyle's sharpening):** A is NOT "adapters already emit this

+shape." A is **spt-core publishes a digest-record contract that projectable

+adapters must target.** The `{"r":"u"}` line at `history.rs:249` is a

+*history-subsystem* test fixture whose discriminator is `r ∈ {u,a}` — a

+**different** schema, cited in error in the draft as pre-alignment. It is not

+evidence; A is a new published surface, not a free ride.

-**Recommended: A.** Rationale: it rides `[history]` (no new manifest seam, per

-M9-PLAN:162), is the only one that cleanly satisfies the CC-fixture E2E (the

-mock adapter's Path-A normalize emits the schema → projectable directly), and

-keeps the echo-commune path untouched (echo still pipes raw to its summarizer;

-the digest parses a *documented minimal field set* that is the **digest's own

-contract**, not the history subsystem's — history still delimits-only). The

-existing native fixtures already carry a role discriminator

-(`{"r":"u"}`/`{"r":"a"}`, history.rs:249), so this aligns with the shape

+**The single contract (no dual schema, no mapping layer):**

+- **History-the-subsystem stays opaque — UNCHANGED.** It carries `raw` verbatim

+  (`history.rs:30-31`), adapter-defined; echo's raw-pipe (`echo.rs:99-104`)

+  untouched. No REQ-HAZARD on history opacity is tripped.

+- **The digest owns ONE record contract** — an spt-core-published overlay on the

+  *content* of `raw`. JSON line: `role ∈ {input, agent, tool}` (enumerated,

+  exact), `text: string`, optional `tool: {name, arg}` (present iff

+  `role == "tool"`). Tool sprints = **consecutive `role:"tool"` records

+  collapsed** into one `ToolSprint`. **APPROVED.**

+- **Exactly one schema in the tree.** No `r`/`u → role`/`input` mapping layer,

+  ever. **Decision (RULED, my call per doyle): migrate the stray native

+  `{"r":"u"}` history-test fixture (`history.rs:249`) to the digest contract

+  shape** — cosmetic for that test (it still only exercises *verbatim carry* of

+  an opaque line; REQ-SEAM-HISTORY semantics unchanged), but it removes the

+  second-schema optics so the codebase shows ONE record JSON shape. Chosen over

+  "settle the digest contract AS the native record shape" because native

+  history must stay adapter-opaque for echo/resume — the digest schema must not

-**Doyle ruling requested:** confirm A (and the exact minimal field names — I

-propose `role`, `text`, optional `tool:{name,arg}`; tool sprints = consecutive

-`role:"tool"` records collapsed), or redirect. This is the only blocker before

+**Published-surface treatment (because adapters encode against it):**

+- Enumerate `role` values exactly (`input`/`agent`/`tool`) — closed set.

+- **Document the contract in CONTEXT.md + `docs/MANIFEST.md`** — the MANIFEST

+  `[pty_digest]` section is **replaced** by the digest-record contract doc, not

+  merely deleted (it remains the digest's schema home, now log-source-shaped).

+- **Forward-compat:** unknown field / malformed line / unknown `role` → **skip**

+  the record (never panic, never abort the projection).

+**Why A (verified by doyle):** the only candidate with no manifest seam

+(M9-PLAN:162 verified), the only clean CC-fixture E2E path (the mock adapter's

+Path-A normalize emits the contract → projectable directly), and it keeps the

+history subsystem opaque. Rejected alternative: an adapter digest-normalize

+command (echo-style) — **adds a manifest seam, contradicts M9-PLAN:162**, and a

+subprocess per refresh kills incremental tail.

## 3. Retire inventory — surgical, NOT wholesale file-deletion


Now propagate the "replace not delete" + fixture-migration into §3/§5/§7.


- `docs/MANIFEST.md:113-145` `[pty_digest]` schema section.
- `crates/spt-daemon/tests/digest.rs` B8 E2E — re-wired (§5), not just deleted.

- `crates/spt-daemon/tests/digest.rs` B8 E2E — re-wired (§5), not just deleted.

### REPLACE / MIGRATE (not a clean delete)
- `docs/MANIFEST.md:113-145` `[pty_digest]` section → **replaced** by the
  digest-record contract doc (§2), not deleted — it stays the digest's schema
  home, now log-source-shaped.
- `crates/spt-live/src/history.rs:249` native fixture `{"r":"u"}`/`{"r":"a"}` →
  **migrated** to the digest-record contract shape (cosmetic; REQ-SEAM-HISTORY
  verbatim-carry semantics unchanged — kills second-schema optics, §2).


- `docs/MANIFEST.md:113-145` `[pty_digest]` schema section.
- `crates/spt-daemon/tests/digest.rs` B8 E2E — re-wired (§5), not just deleted.

- `crates/spt-daemon/tests/digest.rs` B8 E2E — re-wired (§5), not just deleted.

### REPLACE / MIGRATE (not a clean delete)
- `docs/MANIFEST.md:113-145` `[pty_digest]` section → **replaced** by the
  digest-record contract doc (§2), not deleted — it stays the digest's schema
  home, now log-source-shaped.
- `crates/spt-live/src/history.rs:249` native fixture `{"r":"u"}`/`{"r":"a"}` →
  **migrated** to the digest-record contract shape (cosmetic; REQ-SEAM-HISTORY
  verbatim-carry semantics unchanged — kills second-schema optics, §2).

- `spt-daemon/src/brain.rs:760` `run_digest_feed` (the PTY byte-loop) → replaced

-- `docs/MANIFEST.md:113-145` `[pty_digest]` schema section.

+### REPLACE / MIGRATE (not a clean delete)

+- `docs/MANIFEST.md:113-145` `[pty_digest]` section → **replaced** by the

+  digest-record contract doc (§2), not deleted — it stays the digest's schema

+- `crates/spt-live/src/history.rs:249` native fixture `{"r":"u"}`/`{"r":"a"}` →

+  **migrated** to the digest-record contract shape (cosmetic; REQ-SEAM-HISTORY

+  verbatim-carry semantics unchanged — kills second-schema optics, §2).

## 4. T7a — projection engine (new code)


3. **MANIFEST.md**: drop `[pty_digest]` (113–145); the digest rides `[history]`,
   add a one-line cross-ref noting no digest-specific manifest seam.

3. **MANIFEST.md**: **replace** `[pty_digest]` (113–145) with the
   digest-record contract doc (§2 — `role`/`text`/`tool`, enumerated roles,
   malformed→skip); note the digest rides `[history]` (no manifest *seam*) but
   the **record contract** is published here. Mirror the contract enumeration
   into CONTEXT.md (digest entry, 334–343).
4. **Fixture migration**: `history.rs:249` `{"r":"u"}` → digest-contract shape
   (cosmetic; commit message states it's optics-only, semantics unchanged).


3. **MANIFEST.md**: drop `[pty_digest]` (113–145); the digest rides `[history]`,
   add a one-line cross-ref noting no digest-specific manifest seam.

3. **MANIFEST.md**: **replace** `[pty_digest]` (113–145) with the
   digest-record contract doc (§2 — `role`/`text`/`tool`, enumerated roles,
   malformed→skip); note the digest rides `[history]` (no manifest *seam*) but
   the **record contract** is published here. Mirror the contract enumeration
   into CONTEXT.md (digest entry, 334–343).
4. **Fixture migration**: `history.rs:249` `{"r":"u"}` → digest-contract shape
   (cosmetic; commit message states it's optics-only, semantics unchanged).

`hub.publish` (the hub + control channel + CLI are **untouched** — they

2. **Retire** everything in §3 DELETE, with units removed alongside their code.

-3. **MANIFEST.md**: drop `[pty_digest]` (113–145); the digest rides `[history]`,

-   add a one-line cross-ref noting no digest-specific manifest seam.

+3. **MANIFEST.md**: **replace** `[pty_digest]` (113–145) with the

+   digest-record contract doc (§2 — `role`/`text`/`tool`, enumerated roles,

+   malformed→skip); note the digest rides `[history]` (no manifest *seam*) but

+   the **record contract** is published here. Mirror the contract enumeration

+   into CONTEXT.md (digest entry, 334–343).

+4. **Fixture migration**: `history.rs:249` `{"r":"u"}` → digest-contract shape

+   (cosmetic; commit message states it's optics-only, semantics unchanged).

4. **CC-fixture E2E** (`int` — the topology-independence proof): a mock adapter

with `[history]` Path-A `locate_normalize` over a fixture JSONL (CC-style

per-session log); assert `spt endpoint digest <id>` renders the expected


128
129	## 4. T7a — projection engine (new code)
130
131	1. **Digest-record schema** (pending §2 ruling): a small parse fn
132	   `record_to_tagged(raw: &str) -> Option<TaggedRecord>` reading the minimal
133	   field set. Malformed/unknown-role line → skipped (defensive, never panics).
134	2. **Projection**: `project(records: &[…], config: &DigestConfig) -> Digest` —
135	   fold tagged records into `Turn`s, applying the unchanged formula (N-turn
136	   window slide, consecutive-tool collapse into one `ToolSprint`, ~25-char arg
137	   truncation). Reuses the surviving `Digest`/`Turn`/`DigestEntry` types.
138	3. **`DigestEngine` re-core**: replace `parser`+`feed(bytes)` with
139	   `feed_records(&[…]) -> Option<DigestUpdate>` (or `project_into` + the
140	   existing `common_prefix_len` delta diff — **delta machinery unchanged**, only
141	   its input changes from a parsed-byte digest to a projected-record digest).
142	   `snapshot()`, `version()`, `full_update()`, Option-C `newly_completed` all
143	   stay.
144	4. **Incremental tail** (design note — executor picks mechanism per the history
145	   grain): Path B native `history.log` tails cleanly (append-only file →
146	   file-watch + byte/record cursor). Path A (fetcher / locate+normalize) is a
147	   bounded full re-pull. **Fallback if Path A grain too coarse** (M9-PLAN:206):
148	   Path-B-first incremental + Path-A snapshot-only, an accepted degradation
149	   **flagged at G3, not silently shipped** (surface = a `tracing` warn + a G3
150	   note). Recommend: cursor-based tail over the normalized record stream,
151	   re-using `fetch_history`; mechanism final pick is an in-T7a design note.
152	5. **`api digest-entry`** — the push door for log-less adapters (entries arrive
153	   pre-formed in the §2 schema). Mirror the `api history-log` →

## 4. T7a — projection engine (new code)

1. **Digest-record schema** (pending §2 ruling): a small parse fn
   `record_to_tagged(raw: &str) -> Option<TaggedRecord>` reading the minimal
   field set. Malformed/unknown-role line → skipped (defensive, never panics).
2. **Projection**: `project(records: &[…], config: &DigestConfig) -> Digest` —
   fold tagged records into `Turn`s, applying the unchanged formula (N-turn
   window slide, consecutive-tool collapse into one `ToolSprint`, ~25-char arg
   truncation). Reuses the surviving `Digest`/`Turn`/`DigestEntry` types.
3. **`DigestEngine` re-core**: replace `parser`+`feed(bytes)` with
   `feed_records(&[…]) -> Option<DigestUpdate>` (or `project_into` + the
   existing `common_prefix_len` delta diff — **delta machinery unchanged**, only
   its input changes from a parsed-byte digest to a projected-record digest).
   `snapshot()`, `version()`, `full_update()`, Option-C `newly_completed` all
   stay.
4. **Incremental tail** (design note — executor picks mechanism per the history
   grain): Path B native `history.log` tails cleanly (append-only file →
   file-watch + byte/record cursor). Path A (fetcher / locate+normalize) is a
   bounded full re-pull. **Fallback if Path A grain too coarse** (M9-PLAN:206):
   Path-B-first incremental + Path-A snapshot-only, an accepted degradation
   **flagged at G3, not silently shipped** (surface = a `tracing` warn + a G3
   note). Recommend: cursor-based tail over the normalized record stream,
   re-using `fetch_history`; mechanism final pick is an in-T7a design note.
5. **`api digest-entry`** — the push door for log-less adapters (entries arrive
   pre-formed in the §2 schema). Mirror the `api history-log` →


159	   parser units in the same commit — §6).
160
161	---
162
163	## 5. T7b — re-wire + retire + CC E2E
164
165	1. **Re-wire the feed source**: `brain.rs` driver now reads the endpoint's
166	   normalized records (via the history subsystem) and calls `feed_records` →
167	   `hub.publish` (the hub + control channel + CLI are **untouched** — they
168	   already speak `Digest`/`DigestUpdate`).
169	2. **Retire** everything in §3 DELETE, with units removed alongside their code.
170	3. **MANIFEST.md**: **replace** `[pty_digest]` (113–145) with the
171	   digest-record contract doc (§2 — `role`/`text`/`tool`, enumerated roles,
172	   malformed→skip); note the digest rides `[history]` (no manifest *seam*) but
173	   the **record contract** is published here. Mirror the contract enumeration
174	   into CONTEXT.md (digest entry, 334–343).
175	4. **Fixture migration**: `history.rs:249` `{"r":"u"}` → digest-contract shape
176	   (cosmetic; commit message states it's optics-only, semantics unchanged).
177	4. **CC-fixture E2E** (`int` — the topology-independence proof): a mock adapter
178	   with `[history]` Path-A `locate_normalize` over a fixture JSONL (CC-style
179	   per-session log); assert `spt endpoint digest <id>` renders the expected
180	   N-turn window + collapsed sprint. New test file (proposed
181	   `crates/spt-daemon/tests/digest.rs` rewritten, or
182	   `crates/spt/tests/cc_digest_e2e.rs` — executor picks; **flag the filename
183	   choice at G3**). Tag `[int->REQ-TERM-4]`.
184
185	---
186

   parser units in the same commit — §6).

---

## 5. T7b — re-wire + retire + CC E2E

1. **Re-wire the feed source**: `brain.rs` driver now reads the endpoint's
   normalized records (via the history subsystem) and calls `feed_records` →
   `hub.publish` (the hub + control channel + CLI are **untouched** — they
   already speak `Digest`/`DigestUpdate`).
2. **Retire** everything in §3 DELETE, with units removed alongside their code.
3. **MANIFEST.md**: **replace** `[pty_digest]` (113–145) with the
   digest-record contract doc (§2 — `role`/`text`/`tool`, enumerated roles,
   malformed→skip); note the digest rides `[history]` (no manifest *seam*) but
   the **record contract** is published here. Mirror the contract enumeration
   into CONTEXT.md (digest entry, 334–343).
4. **Fixture migration**: `history.rs:249` `{"r":"u"}` → digest-contract shape
   (cosmetic; commit message states it's optics-only, semantics unchanged).
4. **CC-fixture E2E** (`int` — the topology-independence proof): a mock adapter
   with `[history]` Path-A `locate_normalize` over a fixture JSONL (CC-style
   per-session log); assert `spt endpoint digest <id>` renders the expected
   N-turn window + collapsed sprint. New test file (proposed
   `crates/spt-daemon/tests/digest.rs` rewritten, or
   `crates/spt/tests/cc_digest_e2e.rs` — executor picks; **flag the filename
   choice at G3**). Tag `[int->REQ-TERM-4]`.

---


Fix the duplicate step number (two `4.`):


4. **Fixture migration**: `history.rs:249` `{"r":"u"}` → digest-contract shape
   (cosmetic; commit message states it's optics-only, semantics unchanged).
4. **CC-fixture E2E** (`int` — the topology-independence proof):

4. **Fixture migration**: `history.rs:249` `{"r":"u"}` → digest-contract shape
   (cosmetic; commit message states it's optics-only, semantics unchanged).
5. **CC-fixture E2E** (`int` — the topology-independence proof):


4. **Fixture migration**: `history.rs:249` `{"r":"u"}` → digest-contract shape
   (cosmetic; commit message states it's optics-only, semantics unchanged).
4. **CC-fixture E2E** (`int` — the topology-independence proof):

4. **Fixture migration**: `history.rs:249` `{"r":"u"}` → digest-contract shape
   (cosmetic; commit message states it's optics-only, semantics unchanged).
5. **CC-fixture E2E** (`int` — the topology-independence proof):

into CONTEXT.md (digest entry, 334–343).

-4. **CC-fixture E2E** (`int` — the topology-independence proof): a mock adapter

+5. **CC-fixture E2E** (`int` — the topology-independence proof): a mock adapter

N-turn window + collapsed sprint. New test file (proposed


1. **Digest-record schema** (pending §2 ruling): a small parse fn
   `record_to_tagged(raw: &str) -> Option<TaggedRecord>` reading the minimal
   field set. Malformed/unknown-role line → skipped (defensive, never panics).

1. **Digest-record schema** (RULED §2): a small parse fn
   `record_to_tagged(raw: &str) -> Option<TaggedRecord>` reading the published
   contract (`role ∈ {input,agent,tool}`, `text`, optional `tool{name,arg}`).
   Malformed / unknown-field / unknown-role line → skipped (forward-compat,
   never panics).


1. **Digest-record schema** (pending §2 ruling): a small parse fn
   `record_to_tagged(raw: &str) -> Option<TaggedRecord>` reading the minimal
   field set. Malformed/unknown-role line → skipped (defensive, never panics).

1. **Digest-record schema** (RULED §2): a small parse fn
   `record_to_tagged(raw: &str) -> Option<TaggedRecord>` reading the published
   contract (`role ∈ {input,agent,tool}`, `text`, optional `tool{name,arg}`).
   Malformed / unknown-field / unknown-role line → skipped (forward-compat,
   never panics).

## 4. T7a — projection engine (new code)

-1. **Digest-record schema** (pending §2 ruling): a small parse fn

-   `record_to_tagged(raw: &str) -> Option<TaggedRecord>` reading the minimal

-   field set. Malformed/unknown-role line → skipped (defensive, never panics).

+1. **Digest-record schema** (RULED §2): a small parse fn

+   `record_to_tagged(raw: &str) -> Option<TaggedRecord>` reading the published

+   contract (`role ∈ {input,agent,tool}`, `text`, optional `tool{name,arg}`).

+   Malformed / unknown-field / unknown-role line → skipped (forward-compat,

2. **Projection**: `project(records: &[…], config: &DigestConfig) -> Digest` —

fold tagged records into `Turn`s, applying the unchanged formula (N-turn

window slide, consecutive-tool collapse into one `ToolSprint`, ~25-char arg


> **STATUS: DRAFT for doyle vetting (2026-06-13, todlando).** Wave 3 of M9 per
> `M9-PLAN.md §Wave 3` + ADR-0008 amendment (2026-06-12) + CONTEXT.md `session
> digest` entry (lines 334–343). Branch: `m9-digest-logs` (off `main`, lands as
> one PR). Executes on doyle greenlight; one open design decision (§2) needs a
> ruling before T7a code starts. Source-grounded against the live tree at
> `58f034f`.

> **STATUS: GREENLIT (doyle, 2026-06-13). §2 RULED.** Wave 3 of M9 per
> `M9-PLAN.md §Wave 3` + ADR-0008 amendment (2026-06-12) + CONTEXT.md `session
> digest` entry (lines 334–343). Branch: `m9-digest-logs` (lands as one PR).
> Approach A confirmed, field names + collapse rule approved; the §2 sharpening
> (digest-record contract is a *published* surface; one schema, no mapping
> layer; migrate the stray native fixture) is folded in. Source-grounded against
> the live tree at `58f034f`. Executor: todlando.


> **STATUS: DRAFT for doyle vetting (2026-06-13, todlando).** Wave 3 of M9 per
> `M9-PLAN.md §Wave 3` + ADR-0008 amendment (2026-06-12) + CONTEXT.md `session
> digest` entry (lines 334–343). Branch: `m9-digest-logs` (off `main`, lands as
> one PR). Executes on doyle greenlight; one open design decision (§2) needs a
> ruling before T7a code starts. Source-grounded against the live tree at
> `58f034f`.

> **STATUS: GREENLIT (doyle, 2026-06-13). §2 RULED.** Wave 3 of M9 per
> `M9-PLAN.md §Wave 3` + ADR-0008 amendment (2026-06-12) + CONTEXT.md `session
> digest` entry (lines 334–343). Branch: `m9-digest-logs` (lands as one PR).
> Approach A confirmed, field names + collapse rule approved; the §2 sharpening
> (digest-record contract is a *published* surface; one schema, no mapping
> layer; migrate the stray native fixture) is folded in. Source-grounded against
> the live tree at `58f034f`. Executor: todlando.

# M9 Wave 3 (G3) — JIT execution plan: session-digest log projection

-> **STATUS: DRAFT for doyle vetting (2026-06-13, todlando).** Wave 3 of M9 per

+> **STATUS: GREENLIT (doyle, 2026-06-13). §2 RULED.** Wave 3 of M9 per

-> digest` entry (lines 334–343). Branch: `m9-digest-logs` (off `main`, lands as

-> one PR). Executes on doyle greenlight; one open design decision (§2) needs a

-> ruling before T7a code starts. Source-grounded against the live tree at

+> digest` entry (lines 334–343). Branch: `m9-digest-logs` (lands as one PR).

+> Approach A confirmed, field names + collapse rule approved; the §2 sharpening

+> (digest-record contract is a *published* surface; one schema, no mapping

+> layer; migrate the stray native fixture) is folded in. Source-grounded against

+> the live tree at `58f034f`. Executor: todlando.


- [ ] Docs sweep: mdBook digest/manifest pages, CHANGELOG `[Unreleased]`,
      ADR-0008 status note (implementation debt discharged), CONTEXT.md 341
      "implementation note" updated (debt cleared).

- [ ] Digest-record contract **published**: enumerated in CONTEXT.md (334–343)
      + `docs/MANIFEST.md` (replacing `[pty_digest]`, not just deleted).
- [ ] Native fixture (`history.rs:249`) migrated to the contract shape (one
      schema in the tree, no mapping layer).
- [ ] Docs sweep: mdBook digest/manifest pages, CHANGELOG `[Unreleased]`,
      ADR-0008 status note (implementation debt discharged), CONTEXT.md 341
      "implementation note" updated (debt cleared).


- [ ] Digest-record contract **published**: enumerated in CONTEXT.md (334–343)
      + `docs/MANIFEST.md` (replacing `[pty_digest]`, not just deleted).
- [ ] Native fixture (`history.rs:249`) migrated to the contract shape (one
      schema in the tree, no mapping layer).
- [ ] Docs sweep: mdBook digest/manifest pages, CHANGELOG `[Unreleased]`,
      ADR-0008 status note (implementation debt discharged), CONTEXT.md 341
      "implementation note" updated (debt cleared).

- [ ] CC-fixture E2E green (topology-independence proven).

- [ ] Full suite: workspace tests 0-fail, `clippy -D`, `--no-default-features`,

xtask check, `traceable-reqs check` EXIT=0.

+- [ ] Digest-record contract **published**: enumerated in CONTEXT.md (334–343)

+      + `docs/MANIFEST.md` (replacing `[pty_digest]`, not just deleted).

+- [ ] Native fixture (`history.rs:249`) migrated to the contract shape (one

+      schema in the tree, no mapping layer).


Plan fully reconciled with doyle's ruling. ACK doyle + inspect branch topology to pick the Wave3 base.


Bash

$OWL send doyle todlando <<'EOF'
[Wave 3 / G3 — sharpening folded, executing] ACK all. §2 reframed: the digest-record contract is an spt-core-PUBLISHED surface (not "adapters already emit it"); the {"r":"u"} fixture was cited in error — removed as evidence. ONE schema, no r→role mapping layer. My pick (your "my call"): MIGRATE the stray native history-test fixture (history.rs:249) to the digest-contract shape — cosmetic, REQ-SEAM-HISTORY verbatim-carry semantics unchanged — because native history must stay adapter-opaque for echo/resume, so the digest schema can't become the universal native shape. Contract enumerated (role∈{input,agent,tool}/text/optional tool{name,arg}, malformed→skip) and PUBLISHED in CONTEXT.md 334-343 + MANIFEST (replacing [pty_digest], not just deleting). Field names + consecutive-tool collapse: locked per your approval.

Plan updated: M9-WAVE3-JIT.md STATUS now GREENLIT/§2 RULED; §3 keep/delete/replace-migrate inventory; §7 gate adds contract-published + fixture-migrated rows. Carry-forwards held: Path-A-coarse→B-first fallback flagged-not-silent; E2E filename flagged at G3.

NOTE: your msg arrived 9 parts, I got 1-8; part 9 truncated mid-"T5 twohost honored-path int (gateway-wan tracked condition) is a SEPARA[TE]..." — read as: T5 twohost is gateway-wan scope, tracked separately from G3, not a Wave3 item. If part 9 carried more, resend; else I have it.

Starting T7a now on branch m9-digest-logs. Will ping at G3 when wave lands CI-green.
EOF

ACK doyle ruling, signal execution start


➜ SENT:doyle


cd C:/Users/decid/Documents/projects/spt-core && git branch -a && echo "---MAIN-LOG---" && git log main --oneline -5 && echo "---USER-SURFACES---" && git log m9-user-surfaces --oneline -3 2>&1 | head -5 && echo "---MERGE-BASE main..current---" && git merge-base main HEAD

Inspect branch topology for Wave3 base


  dev-freeform
  m9-adapter-custom
* m9-gateway-wan
  m9-user-surfaces
  main
  simple-fixes
  remotes/origin/HEAD -> origin/main
  remotes/origin/close/restoration-milestone
  remotes/origin/dev-freeform
  remotes/origin/m9-adapter-custom
  remotes/origin/m9-gateway-wan
  remotes/origin/m9-user-surfaces
  remotes/origin/main
  remotes/origin/pump-ipc-deadline
  remotes/origin/simple-fixes
---MAIN-LOG---
d338c2d docs(roadmap): M9 adapter customization & session surfaces — milestone entry + JIT plan
bd469b9 docs(context): milestone-A mints — live role, keyword hints, spt rc; Gateway revival settled on existing machinery
8dc22ff docs(context): Gateway grill — endpoint type, Shell model extensions, adapter profiles, digest log-source revision
55c4e59 Merge pull request #6 from SaberMage/close/restoration-milestone
a7d7a08 docs(roadmap): close the broker/brain restoration milestone (v0.4.0–v0.4.2)
---USER-SURFACES---
77e0fb1 fix(daemon): reconcile structurally excludes live-role.md (G2 blocker)
1dbb39e docs(m9): live-role.md no-automated-writer survey — G2 artifact
51ddc37 feat(cli): spt endpoint role — the sole writer of live-role.md (T6)
---MERGE-BASE main..current---
d338c2d8a36018ee5d21b548a40483c1cc07b1cc


Branch topology clear. M9 work is a stack off main (none merged — release-only): `main → m9-adapter-custom (W1) → m9-user-surfaces (W2) → m9-gateway-wan (gateway-wan, current HEAD)`. Wave3 lands last = base off the stack tip (current HEAD `58f034f`), keeping the linear M9 line for the operator's eventual single merge-to-main. Create the branch, then read the spt-term digest primitive before writing the projection.


cd C:/Users/decid/Documents/projects/spt-core && git checkout -b m9-digest-logs && git status --short && echo "HEAD: $(git rev-parse --short HEAD)"

Create Wave3 branch off M9 stack tip


<owl_messages>
<owl_message from="doyle" priority="high">[ACK — clear to run] §2 resolution endorsed. Your migrate-the-fixture call is the RIGHT one and better-reasoned than my framing: native history MUST stay adapter-opaque (echo/resume carry raw verbatim), so the digest contract can't be the universal native shape — it's a published overlay, fixture migrates to it. Locked.

Part 9 read CONFIRMED COMPLETE — nothing lost. Full text was: "T5 twohost honored-path int (gateway-wan tracked condition) is a SEPARATE item — not Wave 3 scope, but verify it got logged on REQ-MSG-6/ROADMAP while you're in the tree." Your paraphrase is exact. No resend needed.

Go. Ping at G3 CI-green.</owl_message>
</owl_messages>

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


Switched to a new branch 'm9-digest-logs'
?? M9-WAVE3-JIT.md
HEAD: 58f034f