/// Pull the current structured digest for `id` over the control channel `name`.
/// `Ok(None)` ⇒ the daemon holds no live digest for that endpoint.
// [impl->REQ-TERM-4]
pub fn pull_snapshot(name: &str, id: &str) -> io::Result<Option<(Digest, u64)>> {
    let mut conn = connect(name)?;
    let req = DigestReq {
        id: id.to_string(),
        from_version: 0,
    };
    write_frame(
        &mut conn,
        &Envelope::new(KIND_DIGEST_SNAPSHOT, serde_json::to_value(req).unwrap()),
    )?;
    let env = read_frame(&mut conn)?;
    let res: DigestResult = serde_json::from_value(env.payload)
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
    Ok(res.digest.map(|d| (d, res.version)))
}

/// Pull the current structured digest for `id` over the control channel `name`:
/// the daemon projects it on demand from the endpoint's normalized records.
/// `Ok(None)` ⇒ nothing was projectable for that endpoint (no source / no records).
// [impl->REQ-TERM-4]
pub fn pull_snapshot(name: &str, id: &str) -> io::Result<Option<(Digest, u64)>> {
    let mut conn = connect(name)?;
    let req = DigestReq {
        id: id.to_string(),
        from_version: 0,
    };
    write_frame(
        &mut conn,
        &Envelope::new(KIND_DIGEST_SNAPSHOT, serde_json::to_value(req).unwrap()),
    )?;
    let env = read_frame(&mut conn)?;
    let res: DigestResult = serde_json::from_value(env.payload)
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
    Ok(res.digest.map(|d| (d, res.version)))
}

/// Trigger an on-demand re-projection of `id` on the daemon and publish any change
/// to live subscribers — the push-driven delta after an `api digest-entry` append.
/// Fire-and-forget (no reply): a daemon that isn't running just means the next
/// pull projects fresh.
// [impl->REQ-TERM-4]
pub fn reproject(name: &str, id: &str) -> io::Result<()> {
    let mut conn = connect(name)?;
    let req = DigestReq {
        id: id.to_string(),
        from_version: 0,
    };
    write_frame(
        &mut conn,
        &Envelope::new(KIND_DIGEST_REPROJECT, serde_json::to_value(req).unwrap()),
    )?;
    Ok(())
}

-/// Pull the current structured digest for `id` over the control channel `name`.

-/// `Ok(None)` ⇒ the daemon holds no live digest for that endpoint.

+/// Pull the current structured digest for `id` over the control channel `name`:

+/// the daemon projects it on demand from the endpoint's normalized records.

+/// `Ok(None)` ⇒ nothing was projectable for that endpoint (no source / no records).

+/// Trigger an on-demand re-projection of `id` on the daemon and publish any change

+/// to live subscribers — the push-driven delta after an `api digest-entry` append.

+/// Fire-and-forget (no reply): a daemon that isn't running just means the next

+pub fn reproject(name: &str, id: &str) -> io::Result<()> {

+        &Envelope::new(KIND_DIGEST_REPROJECT, serde_json::to_value(req).unwrap()),

/// Subscribe to `id`'s delta-stream over `name`, invoking `on_update` for the base

/// snapshot and each subsequent change. Stops when `on_update` returns `false` or

/// the stream ends. `from_version` skips changes the caller already has.


Now the hub tests reference the removed `publish` + `newly_completed`. Re-read the test module:


375	                    }
376	                }
377	                DigestEntry::ToolSprint(tools) => {
378	                    let joined = tools
379	                        .iter()
380	                        .map(|tu| {
381	                            if tu.arg.is_empty() {
382	                                tu.name.clone()
383	                            } else {
384	                                format!("{}({})", tu.name, tu.arg)
385	                            }
386	                        })
387	                        .collect::<Vec<_>>()
388	                        .join(", ");
389	                    out.push_str(&format!("  used: {joined}\n"));
390	                }
391	            }
392	        }
393	    }
394	    out
395	}
396
397	/// Collapse internal whitespace/newlines to a single spaced line for glanceable
398	/// rendering (the digest is a summary, not a transcript).
399	fn one_line(s: &str) -> String {
400	    s.split_whitespace().collect::<Vec<_>>().join(" ")
401	}
402
403	#[cfg(test)]
404	mod tests {
405	    use super::*;
406	    use spt_term::{DigestEntry, ToolUse, Turn};
407
408	    fn turn(input: &str) -> Turn {
409	        Turn {
410	            input: Some(input.to_string()),
411	            entries: Vec::new(),
412	        }
413	    }
414
415	    fn an_update(version: u64) -> DigestUpdate {
416	        DigestUpdate {
417	            version,
418	            from: 0,
419	            turns: vec![turn("hi")],
420	            newly_completed: Vec::new(),
421	        }
422	    }
423
424	    // [unit->REQ-TERM-4] publish stores the latest snapshot; a pull reads it back.
425	    #[test]
426	    fn publish_then_snapshot_reads_latest() {
427	        let hub = DigestHub::new();
428	        assert!(
429	            hub.snapshot("doyle").is_none(),
430	            "no digest before any publish"
431	        );
432	        let snap = Digest {
433	            turns: vec![turn("hi")],
434	        };
435	        hub.publish("doyle", snap.clone(), an_update(1));
436	        let (got, v) = hub.snapshot("doyle").expect("a live digest");
437	        assert_eq!(v, 1);
438	        assert_eq!(got, snap);
439	    }
440
441	    // [unit->REQ-TERM-4] a subscriber receives published changes; a dropped
442	    // subscriber is pruned on the next publish.
443	    #[test]
444	    fn subscriber_receives_changes_then_is_pruned() {
445	        let hub = DigestHub::new();
446	        let (_base, _v, rx) = hub.subscribe("doyle");
447	        assert_eq!(hub.subscriber_count("doyle"), 1);
448
449	        hub.publish(
450	            "doyle",
451	            Digest {
452	                turns: vec![turn("a")],
453	            },
454	            an_update(1),
455	        );
456	        assert_eq!(rx.recv().unwrap().version, 1, "the change was fanned out");
457
458	        // Drop the receiver → the next publish's send fails → the subscriber is pruned.
459	        drop(rx);
460	        hub.publish(
461	            "doyle",
462	            Digest {
463	                turns: vec![turn("b")],
464	            },
465	            an_update(2),
466	        );
467	        assert_eq!(hub.subscriber_count("doyle"), 0, "dead subscriber pruned");
468	    }
469
470	    // [unit->REQ-TERM-4] a fresh subscribe returns the current snapshot as its
471	    // base so later deltas have an apply anchor.
472	    #[test]
473	    fn subscribe_returns_current_base() {
474	        let hub = DigestHub::new();
475	        hub.publish(
476	            "doyle",
477	            Digest {
478	                turns: vec![turn("base")],
479	            },
480	            an_update(7),
481	        );
482	        let (base, version, _rx) = hub.subscribe("doyle");
483	        assert_eq!(version, 7);
484	        assert_eq!(base.turns[0].input.as_deref(), Some("base"));

                    }
                }
                DigestEntry::ToolSprint(tools) => {
                    let joined = tools
                        .iter()
                        .map(|tu| {
                            if tu.arg.is_empty() {
                                tu.name.clone()
                            } else {
                                format!("{}({})", tu.name, tu.arg)
                            }
                        })
                        .collect::<Vec<_>>()
                        .join(", ");
                    out.push_str(&format!("  used: {joined}\n"));
                }
            }
        }
    }
    out
}

/// Collapse internal whitespace/newlines to a single spaced line for glanceable
/// rendering (the digest is a summary, not a transcript).
fn one_line(s: &str) -> String {
    s.split_whitespace().collect::<Vec<_>>().join(" ")
}

#[cfg(test)]
mod tests {
    use super::*;
    use spt_term::{DigestEntry, ToolUse, Turn};

    fn turn(input: &str) -> Turn {
        Turn {
            input: Some(input.to_string()),
            entries: Vec::new(),
        }
    }

    fn an_update(version: u64) -> DigestUpdate {
        DigestUpdate {
            version,
            from: 0,
            turns: vec![turn("hi")],
            newly_completed: Vec::new(),
        }
    }

    // [unit->REQ-TERM-4] publish stores the latest snapshot; a pull reads it back.
    #[test]
    fn publish_then_snapshot_reads_latest() {
        let hub = DigestHub::new();
        assert!(
            hub.snapshot("doyle").is_none(),
            "no digest before any publish"
        );
        let snap = Digest {
            turns: vec![turn("hi")],
        };
        hub.publish("doyle", snap.clone(), an_update(1));
        let (got, v) = hub.snapshot("doyle").expect("a live digest");
        assert_eq!(v, 1);
        assert_eq!(got, snap);
    }

    // [unit->REQ-TERM-4] a subscriber receives published changes; a dropped
    // subscriber is pruned on the next publish.
    #[test]
    fn subscriber_receives_changes_then_is_pruned() {
        let hub = DigestHub::new();
        let (_base, _v, rx) = hub.subscribe("doyle");
        assert_eq!(hub.subscriber_count("doyle"), 1);

        hub.publish(
            "doyle",
            Digest {
                turns: vec![turn("a")],
            },
            an_update(1),
        );
        assert_eq!(rx.recv().unwrap().version, 1, "the change was fanned out");

        // Drop the receiver → the next publish's send fails → the subscriber is pruned.
        drop(rx);
        hub.publish(
            "doyle",
            Digest {
                turns: vec![turn("b")],
            },
            an_update(2),
        );
        assert_eq!(hub.subscriber_count("doyle"), 0, "dead subscriber pruned");
    }

    // [unit->REQ-TERM-4] a fresh subscribe returns the current snapshot as its
    // base so later deltas have an apply anchor.
    #[test]
    fn subscribe_returns_current_base() {
        let hub = DigestHub::new();
        hub.publish(
            "doyle",
            Digest {
                turns: vec![turn("base")],
            },
            an_update(7),
        );
        let (base, version, _rx) = hub.subscribe("doyle");
        assert_eq!(version, 7);
        assert_eq!(base.turns[0].input.as_deref(), Some("base"));


484	        assert_eq!(base.turns[0].input.as_deref(), Some("base"));
485	    }
486
487	    // [unit->REQ-TERM-4] the human render shows input, collapsed agent text, and a
488	    // single `used:` line per tool sprint.
489	    #[test]
490	    fn render_is_glanceable() {
491	        let digest = Digest {
492	            turns: vec![Turn {
493	                input: Some("fix the\n  bug".to_string()),
494	                entries: vec![
495	                    DigestEntry::Agent("on it\nlooking".to_string()),
496	                    DigestEntry::ToolSprint(vec![
497	                        ToolUse {
498	                            name: "Write".into(),
499	                            arg: "a.rs".into(),
500	                        },
501	                        ToolUse {
502	                            name: "Bash".into(),
503	                            arg: "cargo".into(),
504	                        },
505	                    ]),
506	                ],
507	            }],
508	        };
509	        let out = render_digest(&digest);
510	        assert!(out.contains("» fix the bug"), "input on one line: {out}");
511	        assert!(out.contains("  on it looking"), "agent collapsed: {out}");
512	        assert!(
513	            out.contains("used: Write(a.rs), Bash(cargo)"),
514	            "tool sprint: {out}"
515	        );
516	    }
517	}
518

        assert_eq!(base.turns[0].input.as_deref(), Some("base"));
    }

    // [unit->REQ-TERM-4] the human render shows input, collapsed agent text, and a
    // single `used:` line per tool sprint.
    #[test]
    fn render_is_glanceable() {
        let digest = Digest {
            turns: vec![Turn {
                input: Some("fix the\n  bug".to_string()),
                entries: vec![
                    DigestEntry::Agent("on it\nlooking".to_string()),
                    DigestEntry::ToolSprint(vec![
                        ToolUse {
                            name: "Write".into(),
                            arg: "a.rs".into(),
                        },
                        ToolUse {
                            name: "Bash".into(),
                            arg: "cargo".into(),
                        },
                    ]),
                ],
            }],
        };
        let out = render_digest(&digest);
        assert!(out.contains("» fix the bug"), "input on one line: {out}");
        assert!(out.contains("  on it looking"), "agent collapsed: {out}");
        assert!(
            out.contains("used: Write(a.rs), Bash(cargo)"),
            "tool sprint: {out}"
        );
    }
}


Rewrite the three publish-based tests to drive `project_and_publish`, and add delta-diff coverage:


Edit

    fn an_update(version: u64) -> DigestUpdate {
        DigestUpdate {
            version,
            from: 0,
            turns: vec![turn("hi")],
            newly_completed: Vec::new(),
        }
    }

    // [unit->REQ-TERM-4] publish stores the latest snapshot; a pull reads it back.
    #[test]
    fn publish_then_snapshot_reads_latest() {
        let hub = DigestHub::new();
        assert!(
            hub.snapshot("doyle").is_none(),
            "no digest before any publish"
        );
        let snap = Digest {
            turns: vec![turn("hi")],
        };
        hub.publish("doyle", snap.clone(), an_update(1));
        let (got, v) = hub.snapshot("doyle").expect("a live digest");
        assert_eq!(v, 1);
        assert_eq!(got, snap);
    }

    // [unit->REQ-TERM-4] a subscriber receives published changes; a dropped
    // subscriber is pruned on the next publish.
    #[test]
    fn subscriber_receives_changes_then_is_pruned() {
        let hub = DigestHub::new();
        let (_base, _v, rx) = hub.subscribe("doyle");
        assert_eq!(hub.subscriber_count("doyle"), 1);

        hub.publish(
            "doyle",
            Digest {
                turns: vec![turn("a")],
            },
            an_update(1),
        );
        assert_eq!(rx.recv().unwrap().version, 1, "the change was fanned out");

        // Drop the receiver → the next publish's send fails → the subscriber is pruned.
        drop(rx);
        hub.publish(
            "doyle",
            Digest {
                turns: vec![turn("b")],
            },
            an_update(2),
        );
        assert_eq!(hub.subscriber_count("doyle"), 0, "dead subscriber pruned");
    }

    // [unit->REQ-TERM-4] a fresh subscribe returns the current snapshot as its
    // base so later deltas have an apply anchor.
    #[test]
    fn subscribe_returns_current_base() {
        let hub = DigestHub::new();
        hub.publish(
            "doyle",
            Digest {
                turns: vec![turn("base")],
            },
            an_update(7),
        );
        let (base, version, _rx) = hub.subscribe("doyle");
        assert_eq!(version, 7);
        assert_eq!(base.turns[0].input.as_deref(), Some("base"));
    }

    fn digest(turns: Vec<Turn>) -> Digest {
        Digest { turns }
    }

    // [unit->REQ-TERM-4] project_and_publish stores the latest snapshot + bumps the
    // version; a pull reads it back. An unchanged re-projection is a no-op.
    #[test]
    fn project_and_publish_stores_latest() {
        let hub = DigestHub::new();
        assert!(
            hub.snapshot("doyle").is_none(),
            "no digest before any projection"
        );
        let snap = digest(vec![turn("hi")]);
        let (got, v) = hub.project_and_publish("doyle", snap.clone());
        assert_eq!(v, 1);
        assert_eq!(got, snap);
        // An identical re-projection neither bumps the version nor changes state.
        let (_again, v2) = hub.project_and_publish("doyle", snap.clone());
        assert_eq!(v2, 1, "unchanged projection is a no-op");
        let (read, rv) = hub.snapshot("doyle").expect("a stored digest");
        assert_eq!((read, rv), (snap, 1));
    }

    // [unit->REQ-TERM-4] the delta a publish fans out: an active-turn growth is a
    // tail delta (from past the unchanged head); a window slide refreshes from 0.
    #[test]
    fn publish_delta_is_tail_then_full_refresh() {
        let hub = DigestHub::new();
        let (_b, _v, rx) = hub.subscribe("doyle");
        hub.project_and_publish("doyle", digest(vec![turn("a"), turn("b")]));
        let up = rx.recv().unwrap();
        assert_eq!((up.version, up.from, up.turns.len()), (1, 0, 2), "base");
        // Append a third turn: 'a','b' are the unchanged head → tail delta.
        hub.project_and_publish("doyle", digest(vec![turn("a"), turn("b"), turn("c")]));
        let up = rx.recv().unwrap();
        assert_eq!(up.from, 2, "unchanged head of two");
        assert_eq!(up.turns.len(), 1);
        assert_eq!(up.turns[0].input.as_deref(), Some("c"));
        // A front eviction (window slide) diverges at 0 → full refresh.
        hub.project_and_publish("doyle", digest(vec![turn("b"), turn("c"), turn("d")]));
        let up = rx.recv().unwrap();
        assert_eq!(up.from, 0, "front eviction resends from 0");
        assert_eq!(up.turns.len(), 3);
    }

    // [unit->REQ-TERM-4] a dropped subscriber is pruned on the next publish.
    #[test]
    fn dropped_subscriber_is_pruned() {
        let hub = DigestHub::new();
        let (_base, _v, rx) = hub.subscribe("doyle");
        assert_eq!(hub.subscriber_count("doyle"), 1);
        hub.project_and_publish("doyle", digest(vec![turn("a")]));
        assert_eq!(rx.recv().unwrap().version, 1, "the change was fanned out");
        drop(rx);
        hub.project_and_publish("doyle", digest(vec![turn("b")]));
        assert_eq!(hub.subscriber_count("doyle"), 0, "dead subscriber pruned");
    }

    // [unit->REQ-TERM-4] a fresh subscribe returns the current snapshot as its
    // base so later deltas have an apply anchor.
    #[test]
    fn subscribe_returns_current_base() {
        let hub = DigestHub::new();
        hub.project_and_publish("doyle", digest(vec![turn("base")]));
        let (base, version, _rx) = hub.subscribe("doyle");
        assert_eq!(version, 1);
        assert_eq!(base.turns[0].input.as_deref(), Some("base"));
    }


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

    fn an_update(version: u64) -> DigestUpdate {
        DigestUpdate {
            version,
            from: 0,
            turns: vec![turn("hi")],
            newly_completed: Vec::new(),
        }
    }

    // [unit->REQ-TERM-4] publish stores the latest snapshot; a pull reads it back.
    #[test]
    fn publish_then_snapshot_reads_latest() {
        let hub = DigestHub::new();
        assert!(
            hub.snapshot("doyle").is_none(),
            "no digest before any publish"
        );
        let snap = Digest {
            turns: vec![turn("hi")],
        };
        hub.publish("doyle", snap.clone(), an_update(1));
        let (got, v) = hub.snapshot("doyle").expect("a live digest");
        assert_eq!(v, 1);
        assert_eq!(got, snap);
    }

    // [unit->REQ-TERM-4] a subscriber receives published changes; a dropped
    // subscriber is pruned on the next publish.
    #[test]
    fn subscriber_receives_changes_then_is_pruned() {
        let hub = DigestHub::new();
        let (_base, _v, rx) = hub.subscribe("doyle");
        assert_eq!(hub.subscriber_count("doyle"), 1);

        hub.publish(
            "doyle",
            Digest {
                turns: vec![turn("a")],
            },
            an_update(1),
        );
        assert_eq!(rx.recv().unwrap().version, 1, "the change was fanned out");

        // Drop the receiver → the next publish's send fails → the subscriber is pruned.
        drop(rx);
        hub.publish(
            "doyle",
            Digest {
                turns: vec![turn("b")],
            },
            an_update(2),
        );
        assert_eq!(hub.subscriber_count("doyle"), 0, "dead subscriber pruned");
    }

    // [unit->REQ-TERM-4] a fresh subscribe returns the current snapshot as its
    // base so later deltas have an apply anchor.
    #[test]
    fn subscribe_returns_current_base() {
        let hub = DigestHub::new();
        hub.publish(
            "doyle",
            Digest {
                turns: vec![turn("base")],
            },
            an_update(7),
        );
        let (base, version, _rx) = hub.subscribe("doyle");
        assert_eq!(version, 7);
        assert_eq!(base.turns[0].input.as_deref(), Some("base"));
    }

    fn digest(turns: Vec<Turn>) -> Digest {
        Digest { turns }
    }

    // [unit->REQ-TERM-4] project_and_publish stores the latest snapshot + bumps the
    // version; a pull reads it back. An unchanged re-projection is a no-op.
    #[test]
    fn project_and_publish_stores_latest() {
        let hub = DigestHub::new();
        assert!(
            hub.snapshot("doyle").is_none(),
            "no digest before any projection"
        );
        let snap = digest(vec![turn("hi")]);
        let (got, v) = hub.project_and_publish("doyle", snap.clone());
        assert_eq!(v, 1);
        assert_eq!(got, snap);
        // An identical re-projection neither bumps the version nor changes state.
        let (_again, v2) = hub.project_and_publish("doyle", snap.clone());
        assert_eq!(v2, 1, "unchanged projection is a no-op");
        let (read, rv) = hub.snapshot("doyle").expect("a stored digest");
        assert_eq!((read, rv), (snap, 1));
    }

    // [unit->REQ-TERM-4] the delta a publish fans out: an active-turn growth is a
    // tail delta (from past the unchanged head); a window slide refreshes from 0.
    #[test]
    fn publish_delta_is_tail_then_full_refresh() {
        let hub = DigestHub::new();
        let (_b, _v, rx) = hub.subscribe("doyle");
        hub.project_and_publish("doyle", digest(vec![turn("a"), turn("b")]));
        let up = rx.recv().unwrap();
        assert_eq!((up.version, up.from, up.turns.len()), (1, 0, 2), "base");
        // Append a third turn: 'a','b' are the unchanged head → tail delta.
        hub.project_and_publish("doyle", digest(vec![turn("a"), turn("b"), turn("c")]));
        let up = rx.recv().unwrap();
        assert_eq!(up.from, 2, "unchanged head of two");
        assert_eq!(up.turns.len(), 1);
        assert_eq!(up.turns[0].input.as_deref(), Some("c"));
        // A front eviction (window slide) diverges at 0 → full refresh.
        hub.project_and_publish("doyle", digest(vec![turn("b"), turn("c"), turn("d")]));
        let up = rx.recv().unwrap();
        assert_eq!(up.from, 0, "front eviction resends from 0");
        assert_eq!(up.turns.len(), 3);
    }

    // [unit->REQ-TERM-4] a dropped subscriber is pruned on the next publish.
    #[test]
    fn dropped_subscriber_is_pruned() {
        let hub = DigestHub::new();
        let (_base, _v, rx) = hub.subscribe("doyle");
        assert_eq!(hub.subscriber_count("doyle"), 1);
        hub.project_and_publish("doyle", digest(vec![turn("a")]));
        assert_eq!(rx.recv().unwrap().version, 1, "the change was fanned out");
        drop(rx);
        hub.project_and_publish("doyle", digest(vec![turn("b")]));
        assert_eq!(hub.subscriber_count("doyle"), 0, "dead subscriber pruned");
    }

    // [unit->REQ-TERM-4] a fresh subscribe returns the current snapshot as its
    // base so later deltas have an apply anchor.
    #[test]
    fn subscribe_returns_current_base() {
        let hub = DigestHub::new();
        hub.project_and_publish("doyle", digest(vec![turn("base")]));
        let (base, version, _rx) = hub.subscribe("doyle");
        assert_eq!(version, 1);
        assert_eq!(base.turns[0].input.as_deref(), Some("base"));
    }

-    fn an_update(version: u64) -> DigestUpdate {

-            newly_completed: Vec::new(),

+    fn digest(turns: Vec<Turn>) -> Digest {

-    // [unit->REQ-TERM-4] publish stores the latest snapshot; a pull reads it back.

+    // [unit->REQ-TERM-4] project_and_publish stores the latest snapshot + bumps the

+    // version; a pull reads it back. An unchanged re-projection is a no-op.

-    fn publish_then_snapshot_reads_latest() {

+    fn project_and_publish_stores_latest() {

-            "no digest before any publish"

+            "no digest before any projection"

-        hub.publish("doyle", snap.clone(), an_update(1));

-        let (got, v) = hub.snapshot("doyle").expect("a live digest");

+        let snap = digest(vec![turn("hi")]);

+        let (got, v) = hub.project_and_publish("doyle", snap.clone());

+        // An identical re-projection neither bumps the version nor changes state.

+        let (_again, v2) = hub.project_and_publish("doyle", snap.clone());

+        assert_eq!(v2, 1, "unchanged projection is a no-op");

+        let (read, rv) = hub.snapshot("doyle").expect("a stored digest");

+        assert_eq!((read, rv), (snap, 1));

-    // [unit->REQ-TERM-4] a subscriber receives published changes; a dropped

-    // subscriber is pruned on the next publish.

+    // [unit->REQ-TERM-4] the delta a publish fans out: an active-turn growth is a

+    // tail delta (from past the unchanged head); a window slide refreshes from 0.

-    fn subscriber_receives_changes_then_is_pruned() {

+    fn publish_delta_is_tail_then_full_refresh() {

+        let (_b, _v, rx) = hub.subscribe("doyle");

+        hub.project_and_publish("doyle", digest(vec![turn("a"), turn("b")]));

+        assert_eq!((up.version, up.from, up.turns.len()), (1, 0, 2), "base");

+        // Append a third turn: 'a','b' are the unchanged head → tail delta.

+        hub.project_and_publish("doyle", digest(vec![turn("a"), turn("b"), turn("c")]));

+        assert_eq!(up.from, 2, "unchanged head of two");

+        assert_eq!(up.turns[0].input.as_deref(), Some("c"));

+        // A front eviction (window slide) diverges at 0 → full refresh.

+        hub.project_and_publish("doyle", digest(vec![turn("b"), turn("c"), turn("d")]));

+        assert_eq!(up.from, 0, "front eviction resends from 0");

+    // [unit->REQ-TERM-4] a dropped subscriber is pruned on the next publish.

+    fn dropped_subscriber_is_pruned() {

-                turns: vec![turn("a")],

+        hub.project_and_publish("doyle", digest(vec![turn("a")]));

-        // Drop the receiver → the next publish's send fails → the subscriber is pruned.

-                turns: vec![turn("b")],

+        hub.project_and_publish("doyle", digest(vec![turn("b")]));

-                turns: vec![turn("base")],

+        hub.project_and_publish("doyle", digest(vec![turn("base")]));


Update `spt-daemon/src/lib.rs` exports (add `reproject`, drop any retired digest symbols):


170	pub use brain::{Brain, BrainState, BrokerEvent};
171	pub use broker::Broker;
172	pub use config::{DaemonConfig, RelayChoice};
173	pub use consent::{decide as decide_consent, most_recently_active, ConsentDecision, ConsentTarget};
174	pub use daemon::{
175	    active_service_conflict, ensure_running, is_running, request_stop, start_daemon, stop_daemon,
176	    Daemon, StartOutcome, StopOutcome,
177	};
178	pub use digest::{ingest_chunk, DigestEngine, DigestUpdate};
179	pub use digesthub::{
180	    digest_to_json, follow, pull_snapshot, render_digest, render_update, serve_digest_control,
181	    update_to_json, DigestHub,
182	};
183	pub use effect::{EffectJournal, EffectKey, EffectKind, Outcome};
184	pub use endpoint::{
185	    broker_socket_name, daemon_pid_path, digest_socket_name, pump_heartbeat_path,
186	    read_pump_heartbeat, seed_socket_name,
187	};
188	pub use frame::{
189	    accept_hello, Envelope, HandshakeError, Hello, Role, IPC_PROTOCOL_VERSION,
190	    MIN_COMPATIBLE_VERSION,
191	};

pub use brain::{Brain, BrainState, BrokerEvent};
pub use broker::Broker;
pub use config::{DaemonConfig, RelayChoice};
pub use consent::{decide as decide_consent, most_recently_active, ConsentDecision, ConsentTarget};
pub use daemon::{
    active_service_conflict, ensure_running, is_running, request_stop, start_daemon, stop_daemon,
    Daemon, StartOutcome, StopOutcome,
};
pub use digest::{ingest_chunk, DigestEngine, DigestUpdate};
pub use digesthub::{
    digest_to_json, follow, pull_snapshot, render_digest, render_update, serve_digest_control,
    update_to_json, DigestHub,
};
pub use effect::{EffectJournal, EffectKey, EffectKind, Outcome};
pub use endpoint::{
    broker_socket_name, daemon_pid_path, digest_socket_name, pump_heartbeat_path,
    read_pump_heartbeat, seed_socket_name,
};
pub use frame::{
    accept_hello, Envelope, HandshakeError, Hello, Role, IPC_PROTOCOL_VERSION,
    MIN_COMPATIBLE_VERSION,
};


pub use digest::{ingest_chunk, DigestEngine, DigestUpdate};
pub use digesthub::{
    digest_to_json, follow, pull_snapshot, render_digest, render_update, serve_digest_control,
    update_to_json, DigestHub,
};

pub use digest::{common_prefix_len, project_endpoint_digest, DigestUpdate};
pub use digesthub::{
    digest_to_json, follow, pull_snapshot, render_digest, render_update, reproject,
    serve_digest_control, update_to_json, DigestHub,
};


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

pub use digest::{ingest_chunk, DigestEngine, DigestUpdate};
pub use digesthub::{
    digest_to_json, follow, pull_snapshot, render_digest, render_update, serve_digest_control,
    update_to_json, DigestHub,
};

pub use digest::{common_prefix_len, project_endpoint_digest, DigestUpdate};
pub use digesthub::{
    digest_to_json, follow, pull_snapshot, render_digest, render_update, reproject,
    serve_digest_control, update_to_json, DigestHub,
};

active_service_conflict, ensure_running, is_running, request_stop, start_daemon, stop_daemon,

-pub use digest::{ingest_chunk, DigestEngine, DigestUpdate};

+pub use digest::{common_prefix_len, project_endpoint_digest, DigestUpdate};

-    digest_to_json, follow, pull_snapshot, render_digest, render_update, serve_digest_control,

+    digest_to_json, follow, pull_snapshot, render_digest, render_update, reproject,

+    serve_digest_control, update_to_json, DigestHub,

pub use effect::{EffectJournal, EffectKey, EffectKind, Outcome};


Now wire `cmd_digest_entry` to reproject after append (push-driven delta), and rewrite the daemon E2E. First the reproject call:


    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    match spt_store::history::append_digest_entry(&perch_path, record) {
        Ok(n) => {
            eprintln!("DIGEST_ENTRY_LOGGED:{id}:{n}");
            0
        }
        Err(e) => {
            eprintln!("DIGEST_ENTRY_FAIL:{id}: {e}");
            EXIT_REFUSED
        }
    }
}

    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    match spt_store::history::append_digest_entry(&perch_path, record) {
        Ok(n) => {
            eprintln!("DIGEST_ENTRY_LOGGED:{id}:{n}");
            // Push-driven delta: nudge the daemon to re-project now so any live
            // `--follow` subscriber sees the change (best-effort — a daemonless
            // run just means the next pull projects fresh).
            let _ = spt_daemon::reproject(&spt_daemon::digest_socket_name(), id);
            0
        }
        Err(e) => {
            eprintln!("DIGEST_ENTRY_FAIL:{id}: {e}");
            EXIT_REFUSED
        }
    }
}


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

    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    match spt_store::history::append_digest_entry(&perch_path, record) {
        Ok(n) => {
            eprintln!("DIGEST_ENTRY_LOGGED:{id}:{n}");
            0
        }
        Err(e) => {
            eprintln!("DIGEST_ENTRY_FAIL:{id}: {e}");
            EXIT_REFUSED
        }
    }
}

    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    match spt_store::history::append_digest_entry(&perch_path, record) {
        Ok(n) => {
            eprintln!("DIGEST_ENTRY_LOGGED:{id}:{n}");
            // Push-driven delta: nudge the daemon to re-project now so any live
            // `--follow` subscriber sees the change (best-effort — a daemonless
            // run just means the next pull projects fresh).
            let _ = spt_daemon::reproject(&spt_daemon::digest_socket_name(), id);
            0
        }
        Err(e) => {
            eprintln!("DIGEST_ENTRY_FAIL:{id}: {e}");
            EXIT_REFUSED
        }
    }
}

+            // Push-driven delta: nudge the daemon to re-project now so any live

+            // `--follow` subscriber sees the change (best-effort — a daemonless

+            // run just means the next pull projects fresh).

+            let _ = spt_daemon::reproject(&spt_daemon::digest_socket_name(), id);


Build the daemon lib to catch src errors before rewriting the E2E:


Bash

cd C:/Users/decid/Documents/projects/spt-core && cargo build -p spt-daemon 2>&1 | tail -30

Build spt-daemon lib to catch errors


   Compiling spt-store v0.4.2 (C:\Users\decid\Documents\projects\spt-core\crates\spt-store)
   Compiling spt-msg v0.4.2 (C:\Users\decid\Documents\projects\spt-core\crates\spt-msg)
   Compiling spt-runtime v0.4.2 (C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime)
   Compiling spt-term v0.4.2 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
   Compiling spt-net v0.4.2 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
   Compiling spt-live v0.4.2 (C:\Users\decid\Documents\projects\spt-core\crates\spt-live)
   Compiling spt-daemon v0.4.2 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
warning: unused imports: `AtomicBool` and `Ordering`
  --> crates\spt-daemon\src\brain.rs:26:25
   |
26 | use std::sync::atomic::{AtomicBool, Ordering};
   |                         ^^^^^^^^^^  ^^^^^^^^
   |
   = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default

warning: `spt-daemon` (lib) generated 1 warning (run `cargo fix --lib -p spt-daemon` to apply 1 suggestion)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 12.04s


Daemon lib compiles. Remove the now-unused import in `brain.rs`:


26:use std::sync::atomic::{AtomicBool, Ordering};


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

use std::sync::mpsc::{self, Receiver, RecvTimeoutError};

use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};


Now the daemon E2E. Read the current `tests/digest.rs` to rewrite it as the CC-fixture (Path A locate+normalize) topology-independence proof:


1	//! B8 integration: the **PTY-digest daemon-half**, end to end through the real
2	//! IPC (ADR-0008 / REQ-TERM-4).
3	//!
4	//! A [`Broker`] hosts a real PTY child; a [`Brain`] folds that child's live output
5	//! through a [`DigestEngine`] (the brain-side parse — never the broker, ADR-0004
6	//! §B) and publishes each change into the [`DigestHub`]; a separate client reads
7	//! it back over the real digest-control channel both ways ADR-0008 specifies — a
8	//! `spt digest` **snapshot pull** ([`pull_snapshot`]) and a **delta-stream**
9	//! ([`follow`]) — and the opt-in Path-B persistence appends settled turns to the
10	//! perch history store. This is the whole B8 contract over the actual named-pipe
11	//! / UDS transport, on both OSes.
12	//!
13	//! The child is the same cross-OS echo program the B1 broker test uses (`cat` /
14	//! `findstr "."`): bytes injected into its PTY round-trip back as output carrying
15	//! the digest boundary markers, so the engine has a real stream to parse. PTY
16	//! echo may duplicate a line; the assertions are therefore content-tolerant
17	//! (`any`), exactly as the digest parser's own repaint-tolerance contract expects.
18	// [int->REQ-TERM-4]
19
20	use std::sync::atomic::{AtomicU32, Ordering};
21	use std::sync::Arc;
22	use std::thread;
23	use std::time::{Duration, Instant};
24
25	use spt_daemon::msg::SpawnReq;
26	use spt_daemon::{
27	    follow, pull_snapshot, serve_digest_control, Brain, Broker, DigestEngine, DigestHub,
28	};
29	use spt_runtime::PtyDigest;
30	use spt_store::perch::{resolve_perch_path, ParentHint};
31
32	static SEQ: AtomicU32 = AtomicU32::new(0);
33	fn unique(prefix: &str) -> String {
34	    let n = SEQ.fetch_add(1, Ordering::Relaxed);
35	    format!("spt-daemon-b8-{prefix}-{}-{}.sock", std::process::id(), n)
36	}
37
38	/// The cross-OS echo child: reads stdin, writes each line to stdout.
39	fn echo_req() -> SpawnReq {
40	    #[cfg(unix)]
41	    let (program, args) = ("cat".to_string(), Vec::<String>::new());
42	    #[cfg(windows)]
43	    let (program, args) = ("findstr".to_string(), vec![".".to_string()]);
44	    SpawnReq {
45	        program,
46	        args,
47	        rows: 24,
48	        cols: 80,
49	        endpoint: String::new(),
50	    }
51	}
52
53	/// The adapter-supplied digest seam, with persistence opted in.
54	fn seam() -> PtyDigest {
55	    PtyDigest {
56	        input_pattern: r"\nUSER> ".to_string(),
57	        agent_pattern: r"\nAGENT> ".to_string(),
58	        tool_patterns: vec![r"(?P<name>Write)\((?P<arg>[^)]*)\)".to_string()],
59	        catchall_pattern: None,
60	        window_turns: Some(5),
61	        persist: true,
62	    }
63	}
64
65	fn contains(hay: &[u8], needle: &[u8]) -> bool {
66	    hay.windows(needle.len()).any(|w| w == needle)
67	}
68
69	/// Block up to `dur` for `cond` to hold, polling gently.
70	fn wait_until(dur: Duration, mut cond: impl FnMut() -> bool) -> bool {
71	    let start = Instant::now();
72	    while start.elapsed() < dur {
73	        if cond() {
74	            return true;
75	        }
76	        thread::sleep(Duration::from_millis(15));
77	    }
78	    cond()
79	}
80
81	#[test]
82	fn digest_daemon_half_end_to_end() {
83	    // Isolate on-disk state (the perch + Path-B history.log) to a temp home.
84	    let home = tempfile::tempdir().expect("tempdir");
85	    std::env::set_var("SPT_HOME", home.path());
86
87	    let broker_name = unique("broker");
88	    let digest_name = unique("digest");
89
90	    // The digest hub + its control channel (the `spt digest` access surface).
91	    let hub = Arc::new(DigestHub::new());
92	    {
93	        let hub = Arc::clone(&hub);
94	        let name = digest_name.clone();
95	        thread::spawn(move || {
96	            let _ = serve_digest_control(&name, hub);
97	        });
98	    }
99
100	    // The broker hosts PTYs; serve it on a background thread.
101	    let broker = Broker::bind(&broker_name).expect("bind broker");
102	    {
103	        let broker = Arc::clone(&broker);
104	        thread::spawn(move || {
105	            let _ = broker.serve();
106	        });
107	    }
108
109	    // One brain spawns the echo child, injects the marker stream, and folds the
110	    // echoed output through the engine (publishing each change to the hub).
111	    let mut brain = connect_brain(&broker_name);
112	    let _sid = brain.spawn_session(echo_req()).expect("spawn echo child");
113	    let mut engine = DigestEngine::from_manifest(&seam()).expect("compile patterns");
114
115	    // Inject TWO complete turns + a tool sprint, then a sentinel: the first turn
116	    // settles (→ persistence), the second is the active turn. Lines END IN `\r`
117	    // (the Enter key): a real PTY submits + echoes a line on `\r`, and the engine
118	    // normalizes the echoed `\r\n` back to `\n` so the `\nUSER> ` boundary matches.
119	    // Injecting bare `\n` would echo as a lone `\r` the parser strips — collapsing
120	    // the boundaries (the cross-OS PTY stream-contract gotcha, Spike #4/#5).
121	    let blob = b"\rUSER> first task\rAGENT> working on it\rWrite(src/a.rs) done\rUSER> second task\rAGENT> all set\r";
122	    brain.send_input(blob).expect("inject markers");
123
124	    // Read the echoed output, feeding the engine + publishing, until both turns
125	    // have round-tripped through the real PTY.
126	    let mut acc = Vec::<u8>::new();
127	    let mut guard = 0;
128	    loop {
129	        guard += 1;
130	        assert!(
131	            guard < 100_000,
132	            "echoed markers never arrived: {:?}",
133	            String::from_utf8_lossy(&acc)
134	        );
135	        if contains(&acc, b"second task") && contains(&acc, b"first task") {
136	            break;
137	        }
138	        match brain.read_event().expect("read event") {
139	            spt_daemon::BrokerEvent::Output { bytes, .. } => {
140	                acc.extend_from_slice(&bytes);
141	                spt_daemon::ingest_chunk(&mut engine, &hub, "doyle", &bytes);
142	            }
143	            spt_daemon::BrokerEvent::Exit { .. } => panic!("child exited before echo"),
144	            _ => {}
145	        }
146	    }
147
148	    // ── Snapshot pull over the control channel ──────────────────────────────
149	    let (digest, version) = pull_snapshot(&digest_name, "doyle")
150	        .expect("pull ok")
151	        .expect("a live digest exists for the daemon-hosted session");
152	    assert!(version >= 1, "the digest advanced at least one version");
153	    assert!(
154	        digest
155	            .turns
156	            .iter()
157	            .any(|t| t.input.as_deref() == Some("first task")),
158	        "the snapshot carries the parsed user turn: {digest:?}"
159	    );
160	    // The render is the glanceable view; a collapsed tool sprint shows as a
161	    // `used: …` line (avoids depending on the internal entry enum here).
162	    let rendered = spt_daemon::render_digest(&digest);
163	    assert!(
164	        rendered.contains("used: Write(src/a.rs)"),
165	        "the snapshot carries a collapsed tool sprint: {rendered}"
166	    );
167
168	    // ── Delta-stream: a subscriber receives only the new change ─────────────
169	    let base = version;
170	    let (tx, rx) = std::sync::mpsc::channel::<u64>();
171	    {
172	        let dname = digest_name.clone();
173	        thread::spawn(move || {
174	            // Skip the base (== base) and stop on the first true delta (> base).
175	            let _ = follow(&dname, "doyle", base, |up| {
176	                if up.version > base {
177	                    let _ = tx.send(up.version);
178	                    false
179	                } else {
180	                    true
181	                }
182	            });
183	        });
184	    }
185	    // Give the subscriber a moment to register, then drive a third turn.
186	    thread::sleep(Duration::from_millis(80));
187	    brain
188	        .send_input(b"\rUSER> third task\rAGENT> reflecting\r")
189	        .expect("inject third");
190	    // Keep feeding until the third turn round-trips (publishing new versions).
191	    guard = 0;
192	    while !contains(&acc, b"third task") {
193	        guard += 1;
194	        assert!(guard < 100_000, "third turn never echoed");
195	        if let spt_daemon::BrokerEvent::Output { bytes, .. } =
196	            brain.read_event().expect("read event")
197	        {
198	            acc.extend_from_slice(&bytes);
199	            spt_daemon::ingest_chunk(&mut engine, &hub, "doyle", &bytes);
200	        }
201	    }
202	    let delta_version = rx
203	        .recv_timeout(Duration::from_secs(5))
204	        .expect("the subscriber received a delta beyond the base");
205	    assert!(
206	        delta_version > base,
207	        "the delta is a newer version than the base"
208	    );
209
210	    // ── Opt-in Path-B persistence: the settled turn was logged ──────────────
211	    let history = resolve_perch_path("doyle", ParentHint::Infer).join("history.log");
212	    assert!(
213	        wait_until(Duration::from_secs(2), || history.exists()),
214	        "persistence created the Path-B history log"
215	    );
216	    let log = std::fs::read_to_string(&history).expect("read history.log");
217	    assert!(
218	        log.contains("digest_turn"),
219	        "a digest activity record was appended: {log}"
220	    );
221	    assert!(
222	        log.contains("first task"),
223	        "the settled turn was persisted: {log}"
224	    );
225
226	    brain.kill_session().expect("kill child");
227	}
228
229	/// Connect a brain to the broker, retrying briefly while `accept` lags the bind.
230	fn connect_brain(name: &str) -> Brain {
231	    for _ in 0..200 {
232	        if let Ok(b) = Brain::cold_start(name, 1) {
233	            return b;
234	        }
235	        thread::sleep(Duration::from_millis(10));
236	    }
237	    panic!("could not connect a brain to the broker");
238	}
239

//! B8 integration: the **PTY-digest daemon-half**, end to end through the real
//! IPC (ADR-0008 / REQ-TERM-4).
//!
//! A [`Broker`] hosts a real PTY child; a [`Brain`] folds that child's live output
//! through a [`DigestEngine`] (the brain-side parse — never the broker, ADR-0004
//! §B) and publishes each change into the [`DigestHub`]; a separate client reads
//! it back over the real digest-control channel both ways ADR-0008 specifies — a
//! `spt digest` **snapshot pull** ([`pull_snapshot`]) and a **delta-stream**
//! ([`follow`]) — and the opt-in Path-B persistence appends settled turns to the
//! perch history store. This is the whole B8 contract over the actual named-pipe
//! / UDS transport, on both OSes.
//!
//! The child is the same cross-OS echo program the B1 broker test uses (`cat` /
//! `findstr "."`): bytes injected into its PTY round-trip back as output carrying
//! the digest boundary markers, so the engine has a real stream to parse. PTY
//! echo may duplicate a line; the assertions are therefore content-tolerant
//! (`any`), exactly as the digest parser's own repaint-tolerance contract expects.
// [int->REQ-TERM-4]

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

use spt_daemon::msg::SpawnReq;
use spt_daemon::{
    follow, pull_snapshot, serve_digest_control, Brain, Broker, DigestEngine, DigestHub,
};
use spt_runtime::PtyDigest;
use spt_store::perch::{resolve_perch_path, ParentHint};

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

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

/// The adapter-supplied digest seam, with persistence opted in.
fn seam() -> PtyDigest {
    PtyDigest {
        input_pattern: r"\nUSER> ".to_string(),
        agent_pattern: r"\nAGENT> ".to_string(),
        tool_patterns: vec![r"(?P<name>Write)\((?P<arg>[^)]*)\)".to_string()],
        catchall_pattern: None,
        window_turns: Some(5),
        persist: true,
    }
}

fn contains(hay: &[u8], needle: &[u8]) -> bool {
    hay.windows(needle.len()).any(|w| w == needle)
}

/// Block up to `dur` for `cond` to hold, polling gently.
fn wait_until(dur: Duration, mut cond: impl FnMut() -> bool) -> bool {
    let start = Instant::now();
    while start.elapsed() < dur {
        if cond() {
            return true;
        }
        thread::sleep(Duration::from_millis(15));
    }
    cond()
}

#[test]
fn digest_daemon_half_end_to_end() {
    // Isolate on-disk state (the perch + Path-B history.log) to a temp home.
    let home = tempfile::tempdir().expect("tempdir");
    std::env::set_var("SPT_HOME", home.path());

    let broker_name = unique("broker");
    let digest_name = unique("digest");

    // The digest hub + its control channel (the `spt digest` access surface).
    let hub = Arc::new(DigestHub::new());
    {
        let hub = Arc::clone(&hub);
        let name = digest_name.clone();
        thread::spawn(move || {
            let _ = serve_digest_control(&name, hub);
        });
    }

    // The broker hosts PTYs; serve it on a background thread.
    let broker = Broker::bind(&broker_name).expect("bind broker");
    {
        let broker = Arc::clone(&broker);
        thread::spawn(move || {
            let _ = broker.serve();
        });
    }

    // One brain spawns the echo child, injects the marker stream, and folds the
    // echoed output through the engine (publishing each change to the hub).
    let mut brain = connect_brain(&broker_name);
    let _sid = brain.spawn_session(echo_req()).expect("spawn echo child");
    let mut engine = DigestEngine::from_manifest(&seam()).expect("compile patterns");

    // Inject TWO complete turns + a tool sprint, then a sentinel: the first turn
    // settles (→ persistence), the second is the active turn. Lines END IN `\r`
    // (the Enter key): a real PTY submits + echoes a line on `\r`, and the engine
    // normalizes the echoed `\r\n` back to `\n` so the `\nUSER> ` boundary matches.
    // Injecting bare `\n` would echo as a lone `\r` the parser strips — collapsing
    // the boundaries (the cross-OS PTY stream-contract gotcha, Spike #4/#5).
    let blob = b"\rUSER> first task\rAGENT> working on it\rWrite(src/a.rs) done\rUSER> second task\rAGENT> all set\r";
    brain.send_input(blob).expect("inject markers");

    // Read the echoed output, feeding the engine + publishing, until both turns
    // have round-tripped through the real PTY.
    let mut acc = Vec::<u8>::new();
    let mut guard = 0;
    loop {
        guard += 1;
        assert!(
            guard < 100_000,
            "echoed markers never arrived: {:?}",
            String::from_utf8_lossy(&acc)
        );
        if contains(&acc, b"second task") && contains(&acc, b"first task") {
            break;
        }
        match brain.read_event().expect("read event") {
            spt_daemon::BrokerEvent::Output { bytes, .. } => {
                acc.extend_from_slice(&bytes);
                spt_daemon::ingest_chunk(&mut engine, &hub, "doyle", &bytes);
            }
            spt_daemon::BrokerEvent::Exit { .. } => panic!("child exited before echo"),
            _ => {}
        }
    }

    // ── Snapshot pull over the control channel ──────────────────────────────
    let (digest, version) = pull_snapshot(&digest_name, "doyle")
        .expect("pull ok")
        .expect("a live digest exists for the daemon-hosted session");
    assert!(version >= 1, "the digest advanced at least one version");
    assert!(
        digest
            .turns
            .iter()
            .any(|t| t.input.as_deref() == Some("first task")),
        "the snapshot carries the parsed user turn: {digest:?}"
    );
    // The render is the glanceable view; a collapsed tool sprint shows as a
    // `used: …` line (avoids depending on the internal entry enum here).
    let rendered = spt_daemon::render_digest(&digest);
    assert!(
        rendered.contains("used: Write(src/a.rs)"),
        "the snapshot carries a collapsed tool sprint: {rendered}"
    );

    // ── Delta-stream: a subscriber receives only the new change ─────────────
    let base = version;
    let (tx, rx) = std::sync::mpsc::channel::<u64>();
    {
        let dname = digest_name.clone();
        thread::spawn(move || {
            // Skip the base (== base) and stop on the first true delta (> base).
            let _ = follow(&dname, "doyle", base, |up| {
                if up.version > base {
                    let _ = tx.send(up.version);
                    false
                } else {
                    true
                }
            });
        });
    }
    // Give the subscriber a moment to register, then drive a third turn.
    thread::sleep(Duration::from_millis(80));
    brain
        .send_input(b"\rUSER> third task\rAGENT> reflecting\r")
        .expect("inject third");
    // Keep feeding until the third turn round-trips (publishing new versions).
    guard = 0;
    while !contains(&acc, b"third task") {
        guard += 1;
        assert!(guard < 100_000, "third turn never echoed");
        if let spt_daemon::BrokerEvent::Output { bytes, .. } =
            brain.read_event().expect("read event")
        {
            acc.extend_from_slice(&bytes);
            spt_daemon::ingest_chunk(&mut engine, &hub, "doyle", &bytes);
        }
    }
    let delta_version = rx
        .recv_timeout(Duration::from_secs(5))
        .expect("the subscriber received a delta beyond the base");
    assert!(
        delta_version > base,
        "the delta is a newer version than the base"
    );

    // ── Opt-in Path-B persistence: the settled turn was logged ──────────────
    let history = resolve_perch_path("doyle", ParentHint::Infer).join("history.log");
    assert!(
        wait_until(Duration::from_secs(2), || history.exists()),
        "persistence created the Path-B history log"
    );
    let log = std::fs::read_to_string(&history).expect("read history.log");
    assert!(
        log.contains("digest_turn"),
        "a digest activity record was appended: {log}"
    );
    assert!(
        log.contains("first task"),
        "the settled turn was persisted: {log}"
    );

    brain.kill_session().expect("kill child");
}

/// Connect a brain to the broker, retrying briefly while `accept` lags the bind.
fn connect_brain(name: &str) -> Brain {
    for _ in 0..200 {
        if let Ok(b) = Brain::cold_start(name, 1) {
            return b;
        }
        thread::sleep(Duration::from_millis(10));
    }
    panic!("could not connect a brain to the broker");
}


This entire E2E is byte-feed/PTY/parser-based — replace it wholesale with the CC-fixture (Path A locate+normalize) topology-independence proof. First confirm the `[history]` TOML schema (field names, strategy tokens):


9://! Closed vocabularies (adapter kind, history strategy, env direction, inject
106:#[serde(rename_all = "snake_case")]
179:    #[serde(rename = "self", default, skip_serializing_if = "Option::is_none")]
270:#[serde(rename_all = "snake_case")]
276:/// `[history]` — transcript access strategy.
278:pub struct History {
279:    pub strategy: HistoryStrategy,
280:    /// `fetcher` strategy: adapter binary emitting normalized history.
283:    /// `locate_normalize` strategy: where the raw transcript lives.
285:    pub locate_template: Option<String>,
286:    /// `locate_normalize` strategy: command normalizing the raw transcript.
288:    pub normalize_command: Option<String>,
292:#[serde(rename_all = "snake_case")]
293:pub enum HistoryStrategy {
295:    Fetcher,
297:    LocateNormalize,
299:    Native,
312:#[serde(rename_all = "snake_case")]
330:#[serde(rename_all = "snake_case")]
378:#[serde(rename_all = "snake_case")]
456:    #[serde(rename = "subnet")]
458:    #[serde(rename = "same-node")]
460:    #[serde(rename = "none")]
465:#[serde(rename_all = "snake_case")]
476:#[serde(rename_all = "snake_case")]
496:#[serde(rename_all = "snake_case")]
613:         invariants (kind<->[shell] agreement, strategy/avenue field \
640:    /// agreement, strategy↔field presence, inject completeness.
671:        // history strategy ↔ required fields.
673:            match h.strategy {
674:                HistoryStrategy::Fetcher if h.fetcher.is_none() => {
675:                    return err("[history] strategy = \"fetcher\" requires `fetcher`".into());
677:                HistoryStrategy::LocateNormalize
678:                    if h.locate_template.is_none() || h.normalize_command.is_none() =>
680:                    return err("[history] strategy = \"locate_normalize\" requires both `locate_template` and `normalize_command`".into());
772:strategy = "fetcher"
846:        assert_eq!(m.history.unwrap().strategy, HistoryStrategy::Fetcher);
966:strategy = "telepathy"
971:        assert!(e.to_string().contains("telepathy") || e.to_string().contains("strategy"));
1069:    fn fetcher_strategy_requires_fetcher_field() {

[Showing results with pagination = limit: 40]


`[history]` schema confirmed: `strategy = "locate_normalize"`, `locate_template`, `normalize_command` (snake_case). Now write the new CC-fixture E2E (topology-independent — no broker, no PTY):


Write

//! Session-digest **log-projection** integration — the topology-independence
//! proof (ADR-0008 amendment / REQ-TERM-4).
//!
//! There is no broker and no PTY here: the endpoint is **harness-hosted** (a
//! Claude-Code-style per-session log is Path A's worked example). The daemon
//! projects its digest **on demand** from the adapter's `[history]` strategy
//! (`locate_normalize` over a fixture JSONL of published digest-records) and
//! serves it over the real digest-control channel:
//!
//! - **snapshot pull** ([`pull_snapshot`]) projects the current buffer and
//!   returns it — proving a log-sourced digest works with no broker-owned PTY
//!   (impossible under the superseded byte-parse shape);
//! - **delta-stream** ([`follow`]) + a [`reproject`] nudge after the log grows
//!   delivers the structured delta to a subscriber (the push-driven freshness
//!   the `api digest-entry` path also drives — autonomous file-watch is deferred
//!   to the consuming frontend milestone).
// [int->REQ-TERM-4]

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

use spt_daemon::{follow, pull_snapshot, reproject, serve_digest_control, DigestHub};
use spt_store::info::{write_info, InfoJson};
use spt_store::perch::{self, resolve_perch_path, ParentHint};

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

/// A stdin pass-through "normalizer": the located transcript bytes round-trip to
/// stdout unchanged, so the fixture JSONL *is* the normalized record stream.
#[cfg(unix)]
const PASSTHROUGH: &str = "cat";
#[cfg(windows)]
const PASSTHROUGH: &str = r#"cmd /C findstr /r "."#;

/// Register a harness adapter whose `[history]` is `locate_normalize` over
/// `fixture` (a digest-record JSONL), and establish a harness-hosted perch for
/// `id` that records the adapter — the daemon resolves the projection source from
/// these two facts alone (no broker, no live feed).
fn establish_cc_endpoint(id: &str, fixture: &std::path::Path) {
    // TOML literal (single-quoted) strings carry the path + command verbatim —
    // no escaping of Windows backslashes or the findstr quotes.
    let manifest = format!(
        "[adapter]\nname = \"mock-cc\"\nkind = \"harness\"\nversion = \"1\"\n\
         min_spt_core_version = \"0\"\n\n\
         [history]\nstrategy = \"locate_normalize\"\n\
         locate_template = '{}'\nnormalize_command = '{}'\n",
        fixture.display(),
        PASSTHROUGH,
    );
    let src = perch::spt_home().join("srcs").join("mock-cc-src");
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("manifest.toml"), manifest).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    let perch_path = resolve_perch_path(id, ParentHint::Infer);
    std::fs::create_dir_all(&perch_path).unwrap();
    let mut rec = InfoJson::new(id, "0", std::process::id(), "cc-session", "live_agent");
    rec.adapter = Some("mock-cc".to_string());
    write_info(&perch_path, &rec).unwrap();
}

/// Serve a fresh digest-control channel on a unique socket; returns its name.
fn serve_hub() -> String {
    let name = unique("digest");
    let hub = Arc::new(DigestHub::new());
    let n = name.clone();
    thread::spawn(move || {
        let _ = serve_digest_control(&n, hub);
    });
    name
}

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

    // A fixture JSONL of published digest-records — the harness's "session log".
    let fixture = home.path().join("cc-session.jsonl");
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"add a file\"}\n\
         {\"role\":\"agent\",\"text\":\"on it\"}\n\
         {\"role\":\"tool\",\"tool\":{\"name\":\"Write\",\"arg\":\"src/a.rs\"}}\n",
    )
    .unwrap();
    establish_cc_endpoint("cc", &fixture);

    let digest_name = serve_hub();

    // ── Snapshot pull: projected on demand from the log, no broker/PTY ───────
    let (digest, version) = pull_snapshot(&digest_name, "cc")
        .expect("pull ok")
        .expect("a digest projects for the harness-hosted endpoint");
    assert!(version >= 1, "the projection has a version");
    assert_eq!(digest.turns.len(), 1, "one user turn: {digest:?}");
    assert_eq!(digest.turns[0].input.as_deref(), Some("add a file"));
    let rendered = spt_daemon::render_digest(&digest);
    assert!(
        rendered.contains("» add a file") && rendered.contains("used: Write(src/a.rs)"),
        "glanceable render carries the turn + collapsed sprint: {rendered}"
    );
}

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

    let fixture = home.path().join("cc-session.jsonl");
    std::fs::write(&fixture, "{\"role\":\"input\",\"text\":\"first\"}\n").unwrap();
    establish_cc_endpoint("cc2", &fixture);
    let digest_name = serve_hub();

    // Seed the hub via a pull, then subscribe from that version.
    let (_d, base) = pull_snapshot(&digest_name, "cc2").unwrap().unwrap();

    let (tx, rx) = std::sync::mpsc::channel::<usize>();
    {
        let dname = digest_name.clone();
        thread::spawn(move || {
            let _ = follow(&dname, "cc2", base, |up| {
                if up.version > base {
                    // The grown log adds a second turn → its delta.
                    let last = up.turns.last().and_then(|t| t.input.clone());
                    if last.as_deref() == Some("second") {
                        let _ = tx.send(up.turns.len());
                        return false;
                    }
                }
                true
            });
        });
    }
    // Let the subscriber register, then grow the log and nudge a re-projection.
    thread::sleep(Duration::from_millis(80));
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"first\"}\n{\"role\":\"input\",\"text\":\"second\"}\n",
    )
    .unwrap();
    reproject(&digest_name, "cc2").expect("nudge a re-projection");

    let got = wait_recv(&rx, Duration::from_secs(5));
    assert!(got.is_some(), "the subscriber received the push-driven delta");
}

/// Block up to `dur` for one value on `rx`.
fn wait_recv(rx: &std::sync::mpsc::Receiver<usize>, dur: Duration) -> Option<usize> {
    let start = Instant::now();
    loop {
        if let Ok(v) = rx.try_recv() {
            return Some(v);
        }
        if start.elapsed() >= dur {
            return None;
        }
        thread::sleep(Duration::from_millis(15));
    }
}


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

//! Session-digest **log-projection** integration — the topology-independence
//! proof (ADR-0008 amendment / REQ-TERM-4).
//!
//! There is no broker and no PTY here: the endpoint is **harness-hosted** (a
//! Claude-Code-style per-session log is Path A's worked example). The daemon
//! projects its digest **on demand** from the adapter's `[history]` strategy
//! (`locate_normalize` over a fixture JSONL of published digest-records) and
//! serves it over the real digest-control channel:
//!
//! - **snapshot pull** ([`pull_snapshot`]) projects the current buffer and
//!   returns it — proving a log-sourced digest works with no broker-owned PTY
//!   (impossible under the superseded byte-parse shape);
//! - **delta-stream** ([`follow`]) + a [`reproject`] nudge after the log grows
//!   delivers the structured delta to a subscriber (the push-driven freshness
//!   the `api digest-entry` path also drives — autonomous file-watch is deferred
//!   to the consuming frontend milestone).
// [int->REQ-TERM-4]

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

use spt_daemon::{follow, pull_snapshot, reproject, serve_digest_control, DigestHub};
use spt_store::info::{write_info, InfoJson};
use spt_store::perch::{self, resolve_perch_path, ParentHint};

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

/// A stdin pass-through "normalizer": the located transcript bytes round-trip to
/// stdout unchanged, so the fixture JSONL *is* the normalized record stream.
#[cfg(unix)]
const PASSTHROUGH: &str = "cat";
#[cfg(windows)]
const PASSTHROUGH: &str = r#"cmd /C findstr /r "."#;

/// Register a harness adapter whose `[history]` is `locate_normalize` over
/// `fixture` (a digest-record JSONL), and establish a harness-hosted perch for
/// `id` that records the adapter — the daemon resolves the projection source from
/// these two facts alone (no broker, no live feed).
fn establish_cc_endpoint(id: &str, fixture: &std::path::Path) {
    // TOML literal (single-quoted) strings carry the path + command verbatim —
    // no escaping of Windows backslashes or the findstr quotes.
    let manifest = format!(
        "[adapter]\nname = \"mock-cc\"\nkind = \"harness\"\nversion = \"1\"\n\
         min_spt_core_version = \"0\"\n\n\
         [history]\nstrategy = \"locate_normalize\"\n\
         locate_template = '{}'\nnormalize_command = '{}'\n",
        fixture.display(),
        PASSTHROUGH,
    );
    let src = perch::spt_home().join("srcs").join("mock-cc-src");
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("manifest.toml"), manifest).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    let perch_path = resolve_perch_path(id, ParentHint::Infer);
    std::fs::create_dir_all(&perch_path).unwrap();
    let mut rec = InfoJson::new(id, "0", std::process::id(), "cc-session", "live_agent");
    rec.adapter = Some("mock-cc".to_string());
    write_info(&perch_path, &rec).unwrap();
}

/// Serve a fresh digest-control channel on a unique socket; returns its name.
fn serve_hub() -> String {
    let name = unique("digest");
    let hub = Arc::new(DigestHub::new());
    let n = name.clone();
    thread::spawn(move || {
        let _ = serve_digest_control(&n, hub);
    });
    name
}

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

    // A fixture JSONL of published digest-records — the harness's "session log".
    let fixture = home.path().join("cc-session.jsonl");
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"add a file\"}\n\
         {\"role\":\"agent\",\"text\":\"on it\"}\n\
         {\"role\":\"tool\",\"tool\":{\"name\":\"Write\",\"arg\":\"src/a.rs\"}}\n",
    )
    .unwrap();
    establish_cc_endpoint("cc", &fixture);

    let digest_name = serve_hub();

    // ── Snapshot pull: projected on demand from the log, no broker/PTY ───────
    let (digest, version) = pull_snapshot(&digest_name, "cc")
        .expect("pull ok")
        .expect("a digest projects for the harness-hosted endpoint");
    assert!(version >= 1, "the projection has a version");
    assert_eq!(digest.turns.len(), 1, "one user turn: {digest:?}");
    assert_eq!(digest.turns[0].input.as_deref(), Some("add a file"));
    let rendered = spt_daemon::render_digest(&digest);
    assert!(
        rendered.contains("» add a file") && rendered.contains("used: Write(src/a.rs)"),
        "glanceable render carries the turn + collapsed sprint: {rendered}"
    );
}

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

    let fixture = home.path().join("cc-session.jsonl");
    std::fs::write(&fixture, "{\"role\":\"input\",\"text\":\"first\"}\n").unwrap();
    establish_cc_endpoint("cc2", &fixture);
    let digest_name = serve_hub();

    // Seed the hub via a pull, then subscribe from that version.
    let (_d, base) = pull_snapshot(&digest_name, "cc2").unwrap().unwrap();

    let (tx, rx) = std::sync::mpsc::channel::<usize>();
    {
        let dname = digest_name.clone();
        thread::spawn(move || {
            let _ = follow(&dname, "cc2", base, |up| {
                if up.version > base {
                    // The grown log adds a second turn → its delta.
                    let last = up.turns.last().and_then(|t| t.input.clone());
                    if last.as_deref() == Some("second") {
                        let _ = tx.send(up.turns.len());
                        return false;
                    }
                }
                true
            });
        });
    }
    // Let the subscriber register, then grow the log and nudge a re-projection.
    thread::sleep(Duration::from_millis(80));
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"first\"}\n{\"role\":\"input\",\"text\":\"second\"}\n",
    )
    .unwrap();
    reproject(&digest_name, "cc2").expect("nudge a re-projection");

    let got = wait_recv(&rx, Duration::from_secs(5));
    assert!(got.is_some(), "the subscriber received the push-driven delta");
}

/// Block up to `dur` for one value on `rx`.
fn wait_recv(rx: &std::sync::mpsc::Receiver<usize>, dur: Duration) -> Option<usize> {
    let start = Instant::now();
    loop {
        if let Ok(v) = rx.try_recv() {
            return Some(v);
        }
        if start.elapsed() >= dur {
            return None;
        }
        thread::sleep(Duration::from_millis(15));
    }
}

-//! B8 integration: the **PTY-digest daemon-half**, end to end through the real

+//! Session-digest **log-projection** integration — the topology-independence

+//! proof (ADR-0008 amendment / REQ-TERM-4).

-//! A [`Broker`] hosts a real PTY child; a [`Brain`] folds that child's live output

-//! through a [`DigestEngine`] (the brain-side parse — never the broker, ADR-0004

-//! §B) and publishes each change into the [`DigestHub`]; a separate client reads

-//! it back over the real digest-control channel both ways ADR-0008 specifies — a

-//! `spt digest` **snapshot pull** ([`pull_snapshot`]) and a **delta-stream**

-//! ([`follow`]) — and the opt-in Path-B persistence appends settled turns to the

-//! perch history store. This is the whole B8 contract over the actual named-pipe

+//! There is no broker and no PTY here: the endpoint is **harness-hosted** (a

+//! Claude-Code-style per-session log is Path A's worked example). The daemon

+//! projects its digest **on demand** from the adapter's `[history]` strategy

+//! (`locate_normalize` over a fixture JSONL of published digest-records) and

+//! serves it over the real digest-control channel:

-//! The child is the same cross-OS echo program the B1 broker test uses (`cat` /

-//! `findstr "."`): bytes injected into its PTY round-trip back as output carrying

-//! the digest boundary markers, so the engine has a real stream to parse. PTY

-//! echo may duplicate a line; the assertions are therefore content-tolerant

-//! (`any`), exactly as the digest parser's own repaint-tolerance contract expects.

+//! - **snapshot pull** ([`pull_snapshot`]) projects the current buffer and

+//!   returns it — proving a log-sourced digest works with no broker-owned PTY

+//!   (impossible under the superseded byte-parse shape);

+//! - **delta-stream** ([`follow`]) + a [`reproject`] nudge after the log grows

+//!   delivers the structured delta to a subscriber (the push-driven freshness

+//!   the `api digest-entry` path also drives — autonomous file-watch is deferred

+//!   to the consuming frontend milestone).

-    follow, pull_snapshot, serve_digest_control, Brain, Broker, DigestEngine, DigestHub,

+use spt_daemon::{follow, pull_snapshot, reproject, serve_digest_control, DigestHub};

+use spt_store::perch::{self, resolve_perch_path, ParentHint};

static SEQ: AtomicU32 = AtomicU32::new(0);

let n = SEQ.fetch_add(1, Ordering::Relaxed);

-    format!("spt-daemon-b8-{prefix}-{}-{}.sock", std::process::id(), n)

+    format!("spt-daemon-digest-{prefix}-{}-{}.sock", std::process::id(), n)

-/// The cross-OS echo child: reads stdin, writes each line to stdout.

-    let (program, args) = ("cat".to_string(), Vec::<String>::new());

-    let (program, args) = ("findstr".to_string(), vec![".".to_string()]);

+/// A stdin pass-through "normalizer": the located transcript bytes round-trip to

+/// stdout unchanged, so the fixture JSONL *is* the normalized record stream.

+const PASSTHROUGH: &str = r#"cmd /C findstr /r "."#;

-/// The adapter-supplied digest seam, with persistence opted in.

-        input_pattern: r"\nUSER> ".to_string(),

-        agent_pattern: r"\nAGENT> ".to_string(),

-        tool_patterns: vec![r"(?P<name>Write)\((?P<arg>[^)]*)\)".to_string()],

+/// Register a harness adapter whose `[history]` is `locate_normalize` over

+/// `fixture` (a digest-record JSONL), and establish a harness-hosted perch for

+/// `id` that records the adapter — the daemon resolves the projection source from

+/// these two facts alone (no broker, no live feed).

+fn establish_cc_endpoint(id: &str, fixture: &std::path::Path) {

+    // TOML literal (single-quoted) strings carry the path + command verbatim —

+    // no escaping of Windows backslashes or the findstr quotes.

+        "[adapter]\nname = \"mock-cc\"\nkind = \"harness\"\nversion = \"1\"\n\

+         min_spt_core_version = \"0\"\n\n\

+         [history]\nstrategy = \"locate_normalize\"\n\

+         locate_template = '{}'\nnormalize_command = '{}'\n",

+    let src = perch::spt_home().join("srcs").join("mock-cc-src");

+    std::fs::create_dir_all(&src).unwrap();

+    std::fs::write(src.join("manifest.toml"), manifest).unwrap();

+    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

-fn contains(hay: &[u8], needle: &[u8]) -> bool {

-    hay.windows(needle.len()).any(|w| w == needle)

+    let perch_path = resolve_perch_path(id, ParentHint::Infer);

+    std::fs::create_dir_all(&perch_path).unwrap();

+    let mut rec = InfoJson::new(id, "0", std::process::id(), "cc-session", "live_agent");

+    rec.adapter = Some("mock-cc".to_string());

+    write_info(&perch_path, &rec).unwrap();

-/// Block up to `dur` for `cond` to hold, polling gently.

-fn wait_until(dur: Duration, mut cond: impl FnMut() -> bool) -> bool {

-        thread::sleep(Duration::from_millis(15));

+/// Serve a fresh digest-control channel on a unique socket; returns its name.

+    let hub = Arc::new(DigestHub::new());

+        let _ = serve_digest_control(&n, hub);

-    // Isolate on-disk state (the perch + Path-B history.log) to a temp home.

let home = tempfile::tempdir().expect("tempdir");

-    let broker_name = unique("broker");

-    let digest_name = unique("digest");

+    // A fixture JSONL of published digest-records — the harness's "session log".

+    let fixture = home.path().join("cc-session.jsonl");

+    establish_cc_endpoint("cc", &fixture);

-    // The digest hub + its control channel (the `spt digest` access surface).

-    let hub = Arc::new(DigestHub::new());

-        let name = digest_name.clone();

-            let _ = serve_digest_control(&name, hub);

-    // The broker hosts PTYs; serve it on a background thread.

-    let broker = Broker::bind(&broker_name).expect("bind broker");

-        let broker = Arc::clone(&broker);

-    // One brain spawns the echo child, injects the marker stream, and folds the

-    // echoed output through the engine (publishing each change to the hub).

-    let mut brain = connect_brain(&broker_name);

-    let _sid = brain.spawn_session(echo_req()).expect("spawn echo child");

-    let mut engine = DigestEngine::from_manifest(&seam()).expect("compile patterns");

-    // Inject TWO complete turns + a tool sprint, then a sentinel: the first turn

-    // settles (→ persistence), the second is the active turn. Lines END IN `\r`

-    // (the Enter key): a real PTY submits + echoes a line on `\r`, and the engine

-    // normalizes the echoed `\r\n` back to `\n` so the `\nUSER> ` boundary matches.

-    // Injecting bare `\n` would echo as a lone `\r` the parser strips — collapsing

-    // the boundaries (the cross-OS PTY stream-contract gotcha, Spike #4/#5).

-    let blob = b"\rUSER> first task\rAGENT> working on it\rWrite(src/a.rs) done\rUSER> second task\rAGENT> all set\r";

-    brain.send_input(blob).expect("inject markers");

-    // Read the echoed output, feeding the engine + publishing, until both turns

-    // have round-tripped through the real PTY.

-            "echoed markers never arrived: {:?}",

-            String::from_utf8_lossy(&acc)

-        if contains(&acc, b"second task") && contains(&acc, b"first task") {

-        match brain.read_event().expect("read event") {

-            spt_daemon::BrokerEvent::Output { bytes, .. } => {

-                acc.extend_from_slice(&bytes);

-                spt_daemon::ingest_chunk(&mut engine, &hub, "doyle", &bytes);

-            spt_daemon::BrokerEvent::Exit { .. } => panic!("child exited before echo"),

-    // ── Snapshot pull over the control channel ──────────────────────────────

-    let (digest, version) = pull_snapshot(&digest_name, "doyle")

+    // ── Snapshot pull: projected on demand from the log, no broker/PTY ───────

+    let (digest, version) = pull_snapshot(&digest_name, "cc")

-        .expect("a live digest exists for the daemon-hosted session");

-    assert!(version >= 1, "the digest advanced at least one version");

-            .any(|t| t.input.as_deref() == Some("first task")),

-        "the snapshot carries the parsed user turn: {digest:?}"

-    // The render is the glanceable view; a collapsed tool sprint shows as a

-    // `used: …` line (avoids depending on the internal entry enum here).

+        .expect("a digest projects for the harness-hosted endpoint");

+    assert!(version >= 1, "the projection has a version");

+    assert_eq!(digest.turns.len(), 1, "one user turn: {digest:?}");

+    assert_eq!(digest.turns[0].input.as_deref(), Some("add a file"));

let rendered = spt_daemon::render_digest(&digest);

-        rendered.contains("used: Write(src/a.rs)"),

-        "the snapshot carries a collapsed tool sprint: {rendered}"

+        rendered.contains("» add a file") && rendered.contains("used: Write(src/a.rs)"),

+        "glanceable render carries the turn + collapsed sprint: {rendered}"

-    // ── Delta-stream: a subscriber receives only the new change ─────────────

-    let (tx, rx) = std::sync::mpsc::channel::<u64>();

+    let home = tempfile::tempdir().expect("tempdir");

+    std::env::set_var("SPT_HOME", home.path());

+    let fixture = home.path().join("cc-session.jsonl");

+    std::fs::write(&fixture, "{\"role\":\"input\",\"text\":\"first\"}\n").unwrap();

+    establish_cc_endpoint("cc2", &fixture);

+    // Seed the hub via a pull, then subscribe from that version.

+    let (_d, base) = pull_snapshot(&digest_name, "cc2").unwrap().unwrap();

+    let (tx, rx) = std::sync::mpsc::channel::<usize>();

-            // Skip the base (== base) and stop on the first true delta (> base).

-            let _ = follow(&dname, "doyle", base, |up| {

+            let _ = follow(&dname, "cc2", base, |up| {

-                    let _ = tx.send(up.version);

+                    // The grown log adds a second turn → its delta.

+                    let last = up.turns.last().and_then(|t| t.input.clone());

+                    if last.as_deref() == Some("second") {

+                        let _ = tx.send(up.turns.len());

-    // Give the subscriber a moment to register, then drive a third turn.

+    // Let the subscriber register, then grow the log and nudge a re-projection.

-        .send_input(b"\rUSER> third task\rAGENT> reflecting\r")

-    // Keep feeding until the third turn round-trips (publishing new versions).

-    while !contains(&acc, b"third task") {

-        assert!(guard < 100_000, "third turn never echoed");

-        if let spt_daemon::BrokerEvent::Output { bytes, .. } =

-            brain.read_event().expect("read event")

-            acc.extend_from_slice(&bytes);

-            spt_daemon::ingest_chunk(&mut engine, &hub, "doyle", &bytes);

-        .recv_timeout(Duration::from_secs(5))

-        .expect("the subscriber received a delta beyond the base");

-        "the delta is a newer version than the base"

+    reproject(&digest_name, "cc2").expect("nudge a re-projection");

-    // ── Opt-in Path-B persistence: the settled turn was logged ──────────────

-    let history = resolve_perch_path("doyle", ParentHint::Infer).join("history.log");

-        wait_until(Duration::from_secs(2), || history.exists()),

-        "persistence created the Path-B history log"

-    let log = std::fs::read_to_string(&history).expect("read history.log");

-        "a digest activity record was appended: {log}"

-        "the settled turn was persisted: {log}"

-    brain.kill_session().expect("kill child");

+    let got = wait_recv(&rx, Duration::from_secs(5));

+    assert!(got.is_some(), "the subscriber received the push-driven delta");

-/// Connect a brain to the broker, retrying briefly while `accept` lags the bind.

-fn connect_brain(name: &str) -> Brain {

-        if let Ok(b) = Brain::cold_start(name, 1) {

+/// Block up to `dur` for one value on `rx`.

+fn wait_recv(rx: &std::sync::mpsc::Receiver<usize>, dur: Duration) -> Option<usize> {

-        thread::sleep(Duration::from_millis(10));

+        thread::sleep(Duration::from_millis(15));

-    panic!("could not connect a brain to the broker");

//! B8 integration: the **PTY-digest daemon-half**, end to end through the real
//! IPC (ADR-0008 / REQ-TERM-4).
//!
//! A [`Broker`] hosts a real PTY child; a [`Brain`] folds that child's live output
//! through a [`DigestEngine`] (the brain-side parse — never the broker, ADR-0004
//! §B) and publishes each change into the [`DigestHub`]; a separate client reads
//! it back over the real digest-control channel both ways ADR-0008 specifies — a
//! `spt digest` **snapshot pull** ([`pull_snapshot`]) and a **delta-stream**
//! ([`follow`]) — and the opt-in Path-B persistence appends settled turns to the
//! perch history store. This is the whole B8 contract over the actual named-pipe
//! / UDS transport, on both OSes.
//!
//! The child is the same cross-OS echo program the B1 broker test uses (`cat` /
//! `findstr "."`): bytes injected into its PTY round-trip back as output carrying
//! the digest boundary markers, so the engine has a real stream to parse. PTY
//! echo may duplicate a line; the assertions are therefore content-tolerant
//! (`any`), exactly as the digest parser's own repaint-tolerance contract expects.
// [int->REQ-TERM-4]

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

use spt_daemon::msg::SpawnReq;
use spt_daemon::{
    follow, pull_snapshot, serve_digest_control, Brain, Broker, DigestEngine, DigestHub,
};
use spt_runtime::PtyDigest;
use spt_store::perch::{resolve_perch_path, ParentHint};

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

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

/// The adapter-supplied digest seam, with persistence opted in.
fn seam() -> PtyDigest {
    PtyDigest {
        input_pattern: r"\nUSER> ".to_string(),
        agent_pattern: r"\nAGENT> ".to_string(),
        tool_patterns: vec![r"(?P<name>Write)\((?P<arg>[^)]*)\)".to_string()],
        catchall_pattern: None,
        window_turns: Some(5),
        persist: true,
    }
}

fn contains(hay: &[u8], needle: &[u8]) -> bool {
    hay.windows(needle.len()).any(|w| w == needle)
}

/// Block up to `dur` for `cond` to hold, polling gently.
fn wait_until(dur: Duration, mut cond: impl FnMut() -> bool) -> bool {
    let start = Instant::now();
    while start.elapsed() < dur {
        if cond() {
            return true;
        }
        thread::sleep(Duration::from_millis(15));
    }
    cond()
}

#[test]
fn digest_daemon_half_end_to_end() {
    // Isolate on-disk state (the perch + Path-B history.log) to a temp home.
    let home = tempfile::tempdir().expect("tempdir");
    std::env::set_var("SPT_HOME", home.path());

    let broker_name = unique("broker");
    let digest_name = unique("digest");

    // The digest hub + its control channel (the `spt digest` access surface).
    let hub = Arc::new(DigestHub::new());
    {
        let hub = Arc::clone(&hub);
        let name = digest_name.clone();
        thread::spawn(move || {
            let _ = serve_digest_control(&name, hub);
        });
    }

    // The broker hosts PTYs; serve it on a background thread.
    let broker = Broker::bind(&broker_name).expect("bind broker");
    {
        let broker = Arc::clone(&broker);
        thread::spawn(move || {
            let _ = broker.serve();
        });
    }

    // One brain spawns the echo child, injects the marker stream, and folds the
    // echoed output through the engine (publishing each change to the hub).
    let mut brain = connect_brain(&broker_name);
    let _sid = brain.spawn_session(echo_req()).expect("spawn echo child");
    let mut engine = DigestEngine::from_manifest(&seam()).expect("compile patterns");

    // Inject TWO complete turns + a tool sprint, then a sentinel: the first turn
    // settles (→ persistence), the second is the active turn. Lines END IN `\r`
    // (the Enter key): a real PTY submits + echoes a line on `\r`, and the engine
    // normalizes the echoed `\r\n` back to `\n` so the `\nUSER> ` boundary matches.
    // Injecting bare `\n` would echo as a lone `\r` the parser strips — collapsing
    // the boundaries (the cross-OS PTY stream-contract gotcha, Spike #4/#5).
    let blob = b"\rUSER> first task\rAGENT> working on it\rWrite(src/a.rs) done\rUSER> second task\rAGENT> all set\r";
    brain.send_input(blob).expect("inject markers");

    // Read the echoed output, feeding the engine + publishing, until both turns
    // have round-tripped through the real PTY.
    let mut acc = Vec::<u8>::new();
    let mut guard = 0;
    loop {
        guard += 1;
        assert!(
            guard < 100_000,
            "echoed markers never arrived: {:?}",
            String::from_utf8_lossy(&acc)
        );
        if contains(&acc, b"second task") && contains(&acc, b"first task") {
            break;
        }
        match brain.read_event().expect("read event") {
            spt_daemon::BrokerEvent::Output { bytes, .. } => {
                acc.extend_from_slice(&bytes);
                spt_daemon::ingest_chunk(&mut engine, &hub, "doyle", &bytes);
            }
            spt_daemon::BrokerEvent::Exit { .. } => panic!("child exited before echo"),
            _ => {}
        }
    }

    // ── Snapshot pull over the control channel ──────────────────────────────
    let (digest, version) = pull_snapshot(&digest_name, "doyle")
        .expect("pull ok")
        .expect("a live digest exists for the daemon-hosted session");
    assert!(version >= 1, "the digest advanced at least one version");
    assert!(
        digest
            .turns
            .iter()
            .any(|t| t.input.as_deref() == Some("first task")),
        "the snapshot carries the parsed user turn: {digest:?}"
    );
    // The render is the glanceable view; a collapsed tool sprint shows as a
    // `used: …` line (avoids depending on the internal entry enum here).
    let rendered = spt_daemon::render_digest(&digest);
    assert!(
        rendered.contains("used: Write(src/a.rs)"),
        "the snapshot carries a collapsed tool sprint: {rendered}"
    );

    // ── Delta-stream: a subscriber receives only the new change ─────────────
    let base = version;
    let (tx, rx) = std::sync::mpsc::channel::<u64>();
    {
        let dname = digest_name.clone();
        thread::spawn(move || {
            // Skip the base (== base) and stop on the first true delta (> base).
            let _ = follow(&dname, "doyle", base, |up| {
                if up.version > base {
                    let _ = tx.send(up.version);
                    false
                } else {
                    true
                }
            });
        });
    }
    // Give the subscriber a moment to register, then drive a third turn.
    thread::sleep(Duration::from_millis(80));
    brain
        .send_input(b"\rUSER> third task\rAGENT> reflecting\r")
        .expect("inject third");
    // Keep feeding until the third turn round-trips (publishing new versions).
    guard = 0;
    while !contains(&acc, b"third task") {
        guard += 1;
        assert!(guard < 100_000, "third turn never echoed");
        if let spt_daemon::BrokerEvent::Output { bytes, .. } =
            brain.read_event().expect("read event")
        {
            acc.extend_from_slice(&bytes);
            spt_daemon::ingest_chunk(&mut engine, &hub, "doyle", &bytes);
        }
    }
    let delta_version = rx
        .recv_timeout(Duration::from_secs(5))
        .expect("the subscriber received a delta beyond the base");
    assert!(
        delta_version > base,
        "the delta is a newer version than the base"
    );

    // ── Opt-in Path-B persistence: the settled turn was logged ──────────────
    let history = resolve_perch_path("doyle", ParentHint::Infer).join("history.log");
    assert!(
        wait_until(Duration::from_secs(2), || history.exists()),
        "persistence created the Path-B history log"
    );
    let log = std::fs::read_to_string(&history).expect("read history.log");
    assert!(
        log.contains("digest_turn"),
        "a digest activity record was appended: {log}"
    );
    assert!(
        log.contains("first task"),
        "the settled turn was persisted: {log}"
    );

    brain.kill_session().expect("kill child");
}

/// Connect a brain to the broker, retrying briefly while `accept` lags the bind.
fn connect_brain(name: &str) -> Brain {
    for _ in 0..200 {
        if let Ok(b) = Brain::cold_start(name, 1) {
            return b;
        }
        thread::sleep(Duration::from_millis(10));
    }
    panic!("could not connect a brain to the broker");
}


Fix the Windows `PASSTHROUGH` raw-string (malformed delimiter):


Edit

#[cfg(unix)]
const PASSTHROUGH: &str = "cat";
#[cfg(windows)]
const PASSTHROUGH: &str = r#"cmd /C findstr /r "."#;

#[cfg(unix)]
const PASSTHROUGH: &str = "cat";
#[cfg(windows)]
const PASSTHROUGH: &str = "cmd /C findstr /r \".\"";


#[cfg(unix)]
const PASSTHROUGH: &str = "cat";
#[cfg(windows)]
const PASSTHROUGH: &str = "cmd /C findstr /r \".\"";

//! Session-digest **log-projection** integration — the topology-independence
//! proof (ADR-0008 amendment / REQ-TERM-4).
//!
//! There is no broker and no PTY here: the endpoint is **harness-hosted** (a
//! Claude-Code-style per-session log is Path A's worked example). The daemon
//! projects its digest **on demand** from the adapter's `[history]` strategy
//! (`locate_normalize` over a fixture JSONL of published digest-records) and
//! serves it over the real digest-control channel:
//!
//! - **snapshot pull** ([`pull_snapshot`]) projects the current buffer and
//!   returns it — proving a log-sourced digest works with no broker-owned PTY
//!   (impossible under the superseded byte-parse shape);
//! - **delta-stream** ([`follow`]) + a [`reproject`] nudge after the log grows
//!   delivers the structured delta to a subscriber (the push-driven freshness
//!   the `api digest-entry` path also drives — autonomous file-watch is deferred
//!   to the consuming frontend milestone).
// [int->REQ-TERM-4]

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

use spt_daemon::{follow, pull_snapshot, reproject, serve_digest_control, DigestHub};
use spt_store::info::{write_info, InfoJson};
use spt_store::perch::{self, resolve_perch_path, ParentHint};

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

/// A stdin pass-through "normalizer": the located transcript bytes round-trip to
/// stdout unchanged, so the fixture JSONL *is* the normalized record stream.
#[cfg(unix)]
const PASSTHROUGH: &str = "cat";
#[cfg(windows)]
const PASSTHROUGH: &str = r#"cmd /C findstr /r "."#;

/// Register a harness adapter whose `[history]` is `locate_normalize` over
/// `fixture` (a digest-record JSONL), and establish a harness-hosted perch for
/// `id` that records the adapter — the daemon resolves the projection source from
/// these two facts alone (no broker, no live feed).
fn establish_cc_endpoint(id: &str, fixture: &std::path::Path) {
    // TOML literal (single-quoted) strings carry the path + command verbatim —
    // no escaping of Windows backslashes or the findstr quotes.
    let manifest = format!(
        "[adapter]\nname = \"mock-cc\"\nkind = \"harness\"\nversion = \"1\"\n\
         min_spt_core_version = \"0\"\n\n\
         [history]\nstrategy = \"locate_normalize\"\n\
         locate_template = '{}'\nnormalize_command = '{}'\n",
        fixture.display(),
        PASSTHROUGH,
    );
    let src = perch::spt_home().join("srcs").join("mock-cc-src");
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("manifest.toml"), manifest).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    let perch_path = resolve_perch_path(id, ParentHint::Infer);
    std::fs::create_dir_all(&perch_path).unwrap();
    let mut rec = InfoJson::new(id, "0", std::process::id(), "cc-session", "live_agent");
    rec.adapter = Some("mock-cc".to_string());
    write_info(&perch_path, &rec).unwrap();
}

/// Serve a fresh digest-control channel on a unique socket; returns its name.
fn serve_hub() -> String {
    let name = unique("digest");
    let hub = Arc::new(DigestHub::new());
    let n = name.clone();
    thread::spawn(move || {
        let _ = serve_digest_control(&n, hub);
    });
    name
}

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

    // A fixture JSONL of published digest-records — the harness's "session log".
    let fixture = home.path().join("cc-session.jsonl");
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"add a file\"}\n\
         {\"role\":\"agent\",\"text\":\"on it\"}\n\
         {\"role\":\"tool\",\"tool\":{\"name\":\"Write\",\"arg\":\"src/a.rs\"}}\n",
    )
    .unwrap();
    establish_cc_endpoint("cc", &fixture);

    let digest_name = serve_hub();

    // ── Snapshot pull: projected on demand from the log, no broker/PTY ───────
    let (digest, version) = pull_snapshot(&digest_name, "cc")
        .expect("pull ok")
        .expect("a digest projects for the harness-hosted endpoint");
    assert!(version >= 1, "the projection has a version");
    assert_eq!(digest.turns.len(), 1, "one user turn: {digest:?}");
    assert_eq!(digest.turns[0].input.as_deref(), Some("add a file"));
    let rendered = spt_daemon::render_digest(&digest);
    assert!(
        rendered.contains("» add a file") && rendered.contains("used: Write(src/a.rs)"),
        "glanceable render carries the turn + collapsed sprint: {rendered}"
    );
}

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

    let fixture = home.path().join("cc-session.jsonl");
    std::fs::write(&fixture, "{\"role\":\"input\",\"text\":\"first\"}\n").unwrap();
    establish_cc_endpoint("cc2", &fixture);
    let digest_name = serve_hub();

    // Seed the hub via a pull, then subscribe from that version.
    let (_d, base) = pull_snapshot(&digest_name, "cc2").unwrap().unwrap();

    let (tx, rx) = std::sync::mpsc::channel::<usize>();
    {
        let dname = digest_name.clone();
        thread::spawn(move || {
            let _ = follow(&dname, "cc2", base, |up| {
                if up.version > base {
                    // The grown log adds a second turn → its delta.
                    let last = up.turns.last().and_then(|t| t.input.clone());
                    if last.as_deref() == Some("second") {
                        let _ = tx.send(up.turns.len());
                        return false;
                    }
                }
                true
            });
        });
    }
    // Let the subscriber register, then grow the log and nudge a re-projection.
    thread::sleep(Duration::from_millis(80));
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"first\"}\n{\"role\":\"input\",\"text\":\"second\"}\n",
    )
    .unwrap();
    reproject(&digest_name, "cc2").expect("nudge a re-projection");

    let got = wait_recv(&rx, Duration::from_secs(5));
    assert!(got.is_some(), "the subscriber received the push-driven delta");
}

/// Block up to `dur` for one value on `rx`.
fn wait_recv(rx: &std::sync::mpsc::Receiver<usize>, dur: Duration) -> Option<usize> {
    let start = Instant::now();
    loop {
        if let Ok(v) = rx.try_recv() {
            return Some(v);
        }
        if start.elapsed() >= dur {
            return None;
        }
        thread::sleep(Duration::from_millis(15));
    }
}

-const PASSTHROUGH: &str = r#"cmd /C findstr /r "."#;

+const PASSTHROUGH: &str = "cmd /C findstr /r \".\"";


Now build + test the daemon (lib + the new E2E):


Bash

cd C:/Users/decid/Documents/projects/spt-core && cargo test -p spt-daemon --lib digest 2>&1 | tail -20

Run daemon lib digest tests