Skip to main content

majordomus_cli/mesh/
handover.rs

1//! Handovers across the mesh. A handover is written on one machine by `majordomus
2//! handover` into that checkout's `.ai/local/state/handovers/`; this module is the bridge
3//! between that record and the journal, in both directions:
4//!
5//! - **publish** reads a local record — its front matter's facts and its Markdown body —
6//!   into the [`HandoverBody`] a journal event carries, bounded, identified by the digest
7//!   of its body;
8//! - **materialize** writes a consumed handover into the consuming checkout's handovers
9//!   directory as a record of the same schema, so `majordomus handover --resolve` on a
10//!   checkout of the same branch finds it through the ordinary resolution. It names the
11//!   repository the way the resolver compares it (the local git common directory) and the
12//!   origin runtime as its worktree — another checkout's record on the same branch, tier 1
13//!   — and never copies a path of the author's disk.
14//!
15//! Materializing the same handover twice writes one file.
16//!
17//! The publishing half needs nothing but the record on disk, which is what the round trip
18//! below exercises: the newest record of a checkout, read into the bounded body a journal
19//! event carries, identified by the digest of that body and carrying none of the paths of
20//! the machine that wrote it.
21//!
22//! ```
23//! use majordomus_cli::mesh::handover::{directory, latest, to_body};
24//!
25//! let checkout = tempfile::tempdir().unwrap();
26//! let dir = directory(checkout.path());
27//! std::fs::create_dir_all(&dir).unwrap();
28//! std::fs::write(
29//!     dir.join("20260915T100000Z--feature-x--abcdef1--0011.md"),
30//!     "---\nschema_version: 1\ntask_id: t-1\nbranch: feature/x\nhead: abcdef1234\n\
31//!      worktree: /checkout/of/the/author\n---\n\n# Objective\nship\n",
32//! )
33//! .unwrap();
34//!
35//! let newest = latest(checkout.path()).unwrap();
36//! let body = to_body(&newest, Some("#184".into()), None).unwrap();
37//! assert_eq!(body.branch.as_deref(), Some("feature/x"));
38//! assert_eq!(body.issue.as_deref(), Some("#184"), "the publisher's to say");
39//! // the author's disk never travels: the record's worktree is not part of the body
40//! assert!(!serde_json::to_string(&body).unwrap().contains("/checkout/of/the/author"));
41//! ```
42
43use std::collections::BTreeMap;
44use std::path::{Path, PathBuf};
45
46use super::journal::{HandoverBody, MAX_HANDOVER_BYTES};
47use super::state::HandoverView;
48
49/// Where a checkout's handover records live. It is under `.ai/local/`, which is never
50/// committed, so a handover is a fact about one checkout until something — this module, or
51/// a person — moves it; both halves of this module agree on the location by calling here
52/// rather than by each spelling the path.
53///
54/// ```
55/// use majordomus_cli::mesh::handover::directory;
56///
57/// let root = tempfile::tempdir().unwrap();
58/// let dir = directory(root.path());
59/// assert!(dir.ends_with(".ai/local/state/handovers"));
60/// assert!(dir.starts_with(root.path()), "always inside the checkout it belongs to");
61/// ```
62pub fn directory(root: &Path) -> PathBuf {
63    root.join(".ai/local/state/handovers")
64}
65
66/// The newest handover record of a checkout: names begin with a compact UTC timestamp, so
67/// the greatest name is the latest record.
68///
69/// Sorting by name rather than by modification time is deliberate — a file copied or
70/// restored keeps the moment its handover describes, which is the question being asked,
71/// and not the moment the bytes landed. Anything that is not a handover of this checkout
72/// is passed over rather than reported: a file that is not Markdown, one whose name does
73/// not begin with a timestamp, and a symbolic link out of the directory, which is how
74/// something outside the checkout would otherwise get itself published.
75///
76/// ```
77/// use majordomus_cli::mesh::handover::{directory, latest};
78///
79/// let root = tempfile::tempdir().unwrap();
80/// let dir = directory(root.path());
81/// std::fs::create_dir_all(&dir).unwrap();
82/// assert!(latest(root.path()).is_err(), "an empty directory holds no newest record");
83///
84/// std::fs::write(dir.join("20260101T000000Z--a--0000000--00.md"), "---\n---\nold").unwrap();
85/// std::fs::write(dir.join("20260915T100000Z--b--0000000--01.md"), "---\n---\nnew").unwrap();
86/// std::fs::write(dir.join("README.md"), "not a handover").unwrap();
87/// let newest = latest(root.path()).unwrap();
88/// assert!(newest.ends_with("20260915T100000Z--b--0000000--01.md"));
89/// ```
90pub fn latest(root: &Path) -> Result<PathBuf, String> {
91    let dir = directory(root);
92    let real_dir = dir
93        .canonicalize()
94        .map_err(|e| format!("{}: {e}", dir.display()))?;
95    let entries = std::fs::read_dir(&dir).map_err(|e| format!("{}: {e}", dir.display()))?;
96    entries
97        .filter_map(|e| e.ok().map(|e| e.path()))
98        .filter(|p| {
99            p.extension().is_some_and(|x| x == "md")
100                && p.file_name()
101                    .and_then(|n| n.to_str())
102                    .is_some_and(|n| n.as_bytes().first().is_some_and(u8::is_ascii_digit))
103                // A symbolic link out of the directory is not a handover of this checkout:
104                // nothing outside it is ever published.
105                && p.canonicalize().is_ok_and(|real| real.starts_with(&real_dir))
106        })
107        // The name's timestamp is the moment the record describes, to the second; two
108        // records written inside one second differ there only by a random suffix, which
109        // orders nothing. Within one second the later write is the newer record — the
110        // tie the shell resolver breaks by ledger position.
111        .max_by_key(|p| {
112            let name = p
113                .file_name()
114                .and_then(|n| n.to_str())
115                .unwrap_or_default()
116                .to_string();
117            let second = name.split("--").next().unwrap_or_default().to_string();
118            let written = std::fs::metadata(p).and_then(|m| m.modified()).ok();
119            (second, written, name)
120        })
121        .ok_or_else(|| format!("no handover record under {}", dir.display()))
122}
123
124/// A front-matter value from a peer: one line, no quote that could end the value.
125fn one_line(value: &str) -> String {
126    value
127        .chars()
128        .filter(|c| !c.is_control() && *c != '"')
129        .collect()
130}
131
132/// Split a record into its front matter (flat `key: value` lines; list items are skipped)
133/// and its body.
134///
135/// This is a reader of one shape, not a YAML parser: a handover's front matter is flat by
136/// contract, and the few facts the mesh needs from it are scalars. Nesting and list items
137/// are skipped rather than refused, so a record that carries more than this module
138/// understands still publishes.
139///
140/// ```
141/// use majordomus_cli::mesh::handover::read;
142///
143/// let dir = tempfile::tempdir().unwrap();
144/// let path = dir.path().join("record.md");
145/// std::fs::write(
146///     &path,
147///     "---\nbranch: feature/x\nchanged_files:\n  - apps/x.rs\n---\n\n# Objective\nship\n",
148/// )
149/// .unwrap();
150///
151/// let (front, body) = read(&path).unwrap();
152/// assert_eq!(front["branch"], "feature/x");
153/// assert!(!front.contains_key("apps/x.rs"), "a list item is not a fact of the front matter");
154/// assert!(body.starts_with("# Objective"));
155///
156/// // a file with no front matter is not a handover record
157/// std::fs::write(&path, "# Objective\nship\n").unwrap();
158/// assert!(read(&path).is_err());
159/// ```
160pub fn read(path: &Path) -> Result<(BTreeMap<String, String>, String), String> {
161    let text = std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?;
162    split(&text).ok_or_else(|| format!("{}: no front matter", path.display()))
163}
164
165fn split(text: &str) -> Option<(BTreeMap<String, String>, String)> {
166    let rest = text.strip_prefix("---\n")?;
167    let end = rest.find("\n---\n")?;
168    let mut front = BTreeMap::new();
169    for line in rest[..end].lines() {
170        if line.starts_with(' ') || line.starts_with('-') {
171            continue;
172        }
173        if let Some((key, value)) = line.split_once(':') {
174            let value = value.trim().trim_matches('"').to_string();
175            front.insert(key.trim().to_string(), value);
176        }
177    }
178    Some((front, rest[end + 5..].trim_start_matches('\n').to_string()))
179}
180
181/// The journal body of a local record. `issue` and `milestone` are the publisher's to say:
182/// a handover record has no field for either.
183///
184/// What crosses is the body and a few facts about the work — never a path of the author's
185/// disk, because the receiving checkout is somewhere else and a path from another machine
186/// is at best noise. The body is bounded at [`MAX_HANDOVER_BYTES`] and an empty one is
187/// refused, since a handover whose body says nothing hands nothing over. A placeholder
188/// front-matter value (`none`, `DETACHED`) is read as the absence it stands for.
189///
190/// ```
191/// use majordomus_cli::mesh::handover::to_body;
192/// use majordomus_cli::mesh::journal::HandoverBody;
193///
194/// let dir = tempfile::tempdir().unwrap();
195/// let path = dir.path().join("20260915T100000Z--feature-x--abcdef1--0011.md");
196/// std::fs::write(
197///     &path,
198///     "---\ntask_id: t-1\nbranch: DETACHED\nhead: abcdef1234\nworktree: /home/someone/repo\n\
199///      ---\n\n# Objective\nship\n",
200/// )
201/// .unwrap();
202///
203/// let body = to_body(&path, Some("#184".into()), None).unwrap();
204/// assert_eq!(body.task.as_deref(), Some("t-1"));
205/// assert_eq!(body.branch, None, "DETACHED is the absence of a branch, not a branch");
206/// assert_eq!(body.issue.as_deref(), Some("#184"));
207/// assert_eq!(body.id, HandoverBody::digest_of(&body.body));
208/// assert!(!serde_json::to_string(&body).unwrap().contains("/home/someone"));
209///
210/// // a record whose body says nothing hands nothing over
211/// std::fs::write(&path, "---\ntask_id: t-1\n---\n\n").unwrap();
212/// assert!(to_body(&path, None, None).is_err());
213/// ```
214pub fn to_body(
215    path: &Path,
216    issue: Option<String>,
217    milestone: Option<String>,
218) -> Result<HandoverBody, String> {
219    let (front, body) = read(path)?;
220    // signed into the journal and replicated to every linked runtime, in clear: a credential
221    // the author pasted into the record is replaced by a marker before anything is signed
222    let body = crate::redaction::redact_secrets(&body).text;
223    if body.trim().is_empty() {
224        return Err(format!("{}: the handover has no body", path.display()));
225    }
226    if body.len() > MAX_HANDOVER_BYTES {
227        return Err(format!(
228            "{}: a body of {} bytes exceeds the {MAX_HANDOVER_BYTES}-byte bound a mesh handover carries",
229            path.display(),
230            body.len()
231        ));
232    }
233    let fact = |key: &str| {
234        front
235            .get(key)
236            .filter(|v| !v.is_empty() && *v != "none" && *v != "NONE")
237            .cloned()
238    };
239    Ok(HandoverBody {
240        id: HandoverBody::digest_of(&body),
241        task: fact("task_id"),
242        issue,
243        milestone,
244        branch: fact("branch").filter(|b| b != "DETACHED"),
245        head: fact("head"),
246        created_at: fact("created_at"),
247        name: path
248            .file_name()
249            .and_then(|n| n.to_str())
250            .map(str::to_string),
251        body,
252    })
253}
254
255/// The git common directory of `root`, spelled as the shell resolver spells the local
256/// repository id: absolute as git prints it, or joined to the root when relative.
257fn repository_id(root: &Path) -> Result<String, String> {
258    let out = std::process::Command::new("git")
259        .arg("-C")
260        .arg(root)
261        .args(["rev-parse", "--git-common-dir"])
262        .output()
263        .map_err(|e| format!("git: {e}"))?;
264    if !out.status.success() {
265        return Err(format!("{} is not a git work tree", root.display()));
266    }
267    let dir = String::from_utf8_lossy(&out.stdout).trim().to_string();
268    Ok(if dir.starts_with('/') {
269        dir
270    } else {
271        format!("{}/{dir}", root.display())
272    })
273}
274
275/// Write a consumed handover into `root`'s handovers directory, or return the record that
276/// already holds it. The record's worktree is `mesh:<origin runtime>`: it resolves as
277/// another checkout's handover on the same branch.
278///
279/// This is the half that turns a message into continuity: after it, `majordomus handover
280/// --resolve` on this checkout finds the other machine's handover through the ordinary
281/// resolution and nothing downstream has to know the mesh exists. It is written to be
282/// repeatable — consuming the same handover twice yields the one record — and defensively,
283/// because every value in it was written by another runtime: the front matter and the file
284/// name admit only what their parts are, so neither a key nor a path can be smuggled in
285/// through a branch name or a timestamp.
286///
287/// ```
288/// use majordomus_cli::mesh::handover::{directory, materialize, to_body};
289/// use majordomus_cli::mesh::journal::StreamId;
290/// use majordomus_cli::mesh::state::HandoverView;
291///
292/// let tmp = tempfile::tempdir().unwrap();
293/// let root = tmp.path().join("checkout");
294/// std::fs::create_dir_all(&root).unwrap();
295/// assert!(std::process::Command::new("git")
296///     .arg("-C").arg(&root).args(["init", "-q"]).status().unwrap().success());
297///
298/// let src = tmp.path().join("20260915T100000Z--feature-x--abcdef1--0011.md");
299/// std::fs::write(&src, "---\nbranch: feature/x\nhead: abcdef1234\n---\n\n# Objective\nship\n")
300///     .unwrap();
301/// let stream = StreamId::parse(&format!("{}-{}-{}", "a".repeat(32), "b".repeat(16), "c".repeat(16)))
302///     .unwrap();
303/// let view = HandoverView {
304///     id: to_body(&src, None, None).unwrap().id,
305///     stream,
306///     runtime: format!("{}-{}", "a".repeat(32), "b".repeat(16)),
307///     published_lamport: 1,
308///     handover: to_body(&src, None, None).unwrap(),
309///     consumed_by: vec![],
310/// };
311///
312/// let written = materialize(&root, &view).unwrap();
313/// assert_eq!(written.parent(), Some(directory(&root).as_path()));
314/// assert_eq!(materialize(&root, &view).unwrap(), written, "one handover, one record");
315/// assert!(std::fs::read_to_string(&written).unwrap().contains("worktree: mesh:"));
316/// ```
317pub fn materialize(root: &Path, view: &HandoverView) -> Result<PathBuf, String> {
318    write_record(
319        root,
320        &view.handover,
321        &Provenance {
322            transport: "mesh",
323            marker_key: "mesh_handover",
324            marker: view.id.clone(),
325            origin_key: "mesh_origin",
326            origin: view.runtime.clone(),
327            working_tree: None,
328            changed_files: Vec::new(),
329        },
330    )
331}
332
333/// Where a handover written into this checkout came from, and what its front matter says
334/// about the source state it was written against. The mesh and a continuity record are the
335/// two transports that bring one; both write through [`write_record`], so a handover that
336/// arrived from another machine has one shape whichever way it travelled.
337#[derive(Debug, Clone)]
338pub(crate) struct Provenance {
339    /// `mesh` or `continuity`: the file-name segment and the prefix of owner and worktree.
340    pub transport: &'static str,
341    /// The front-matter key whose value identifies the handover on its transport; a record
342    /// holding `<marker_key>: <marker>` already is this handover, which is what makes
343    /// writing it twice write one file.
344    pub marker_key: &'static str,
345    /// That value (32 hex).
346    pub marker: String,
347    /// The front-matter key naming where it came from.
348    pub origin_key: &'static str,
349    /// Where it came from: a runtime id or a device node id (hex).
350    pub origin: String,
351    /// `clean` or `dirty` at the origin when the transport carries it. `None` writes
352    /// `clean`, as the mesh, which does not carry it, always has.
353    pub working_tree: Option<&'static str>,
354    /// The repository-relative paths that differed from HEAD at the origin.
355    pub changed_files: Vec<String>,
356}
357
358/// Write a handover that arrived from elsewhere into `root`'s handovers directory, or
359/// return the record that already holds it. See [`materialize`] for why every value is
360/// written defensively.
361pub(crate) fn write_record(
362    root: &Path,
363    h: &HandoverBody,
364    from: &Provenance,
365) -> Result<PathBuf, String> {
366    let dir = directory(root);
367    std::fs::create_dir_all(&dir).map_err(|e| format!("{}: {e}", dir.display()))?;
368    let hexish = |v: &str| {
369        v.chars()
370            .filter(char::is_ascii_hexdigit)
371            .take(64)
372            .collect::<String>()
373    };
374    let marker_value = hexish(&from.marker);
375    let origin = hexish(&from.origin.replace('-', ""));
376    if marker_value.len() < 16 {
377        return Err("a handover from elsewhere is identified by at least 16 hex".into());
378    }
379    let marker = format!("{}: {marker_value}", from.marker_key);
380    if let Ok(entries) = std::fs::read_dir(&dir) {
381        for entry in entries.flatten() {
382            let path = entry.path();
383            if path.extension().is_some_and(|x| x == "md")
384                && std::fs::read_to_string(&path).is_ok_and(|t| t.lines().any(|l| l == marker))
385            {
386                return Ok(path);
387            }
388        }
389    }
390    // Everything below comes from another runtime. Validation refused multi-line values at
391    // ingest; this writes defensively anyway, because a file name and a front matter built
392    // from a peer's words are exactly where a path or a key would be smuggled.
393    let origin_label = if from.transport == "mesh" {
394        // the mesh's runtime id keeps its dash, as it always has
395        one_line(&from.origin)
396            .chars()
397            .filter(|c| c.is_ascii_hexdigit() || *c == '-')
398            .collect()
399    } else {
400        origin
401    };
402    let branch = one_line(h.branch.as_deref().unwrap_or("DETACHED"));
403    let head = one_line(h.head.as_deref().unwrap_or("NONE"));
404    let created = one_line(
405        &h.created_at
406            .clone()
407            .unwrap_or_else(|| crate::peers::rfc3339(std::time::SystemTime::now())),
408    );
409    let mut text = String::new();
410    text.push_str("---\nschema_version: 1\n");
411    text.push_str(&format!("created_at: {created}\n"));
412    text.push_str(&format!(
413        "task_id: {}\n",
414        one_line(h.task.as_deref().unwrap_or("none"))
415    ));
416    text.push_str("profile: none\n");
417    let transport = from.transport;
418    text.push_str(&format!("owner: \"{transport}:{origin_label}\"\n"));
419    text.push_str(&format!("repository_id: {}\n", repository_id(root)?));
420    text.push_str(&format!("worktree: {transport}:{origin_label}\n"));
421    text.push_str(&format!(
422        "branch: {branch}\nhead: {head}\nworking_tree: {}\nchanged_files:\n",
423        from.working_tree.unwrap_or("clean")
424    ));
425    for path in &from.changed_files {
426        let path = one_line(path);
427        if !path.is_empty() && !path.starts_with('/') && !path.split('/').any(|c| c == "..") {
428            text.push_str(&format!("  - {path}\n"));
429        }
430    }
431    text.push_str(&format!("{marker}\n{}: {origin_label}\n", from.origin_key));
432    if let Some(issue) = &h.issue {
433        text.push_str(&format!("issue: \"{}\"\n", one_line(issue)));
434    }
435    if let Some(milestone) = &h.milestone {
436        text.push_str(&format!("milestone: \"{}\"\n", one_line(milestone)));
437    }
438    text.push_str("---\n\n");
439    text.push_str(&h.body);
440    // The file name admits only what its parts are: digits and T/Z of a timestamp, hex of a
441    // commit, a branch key of [A-Za-z0-9_-] (no dots, so never `..`), hex of the id.
442    let mut compact: String = created
443        .chars()
444        .filter(|c| c.is_ascii_digit() || *c == 'T' || *c == 'Z')
445        .take(16)
446        .collect();
447    if compact.is_empty() {
448        compact = "00000000T000000Z".into();
449    }
450    let branch_key: String = branch
451        .chars()
452        .map(|c| {
453            if c.is_ascii_alphanumeric() || c == '_' || c == '-' {
454                c
455            } else {
456                '-'
457            }
458        })
459        .take(64)
460        .collect();
461    let mut head7: String = head
462        .chars()
463        .filter(char::is_ascii_hexdigit)
464        .take(7)
465        .collect();
466    if head7.is_empty() {
467        head7 = "0000000".into();
468    }
469    let name = format!(
470        "{compact}--{transport}--{branch_key}--{head7}--{}.md",
471        &marker_value[..16]
472    );
473    let path = dir.join(&name);
474    if name.contains('/') || path.parent() != Some(dir.as_path()) {
475        return Err(format!(
476            "refusing to write a handover outside {}",
477            dir.display()
478        ));
479    }
480    let tmp = dir.join(format!(".tmp.{transport}.{}", &marker_value[..16]));
481    std::fs::write(&tmp, text).map_err(|e| format!("{}: {e}", tmp.display()))?;
482    #[cfg(unix)]
483    {
484        use std::os::unix::fs::PermissionsExt;
485        let _ = std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o600));
486    }
487    std::fs::rename(&tmp, &path).map_err(|e| format!("{}: {e}", path.display()))?;
488    Ok(path)
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494    use crate::mesh::journal::StreamId;
495
496    const RECORD: &str = "---\nschema_version: 1\ncreated_at: 2026-09-15T10:00:00Z\ntask_id: t-1\nprofile: implementation\nowner: \"k\"\nrepository_id: /somewhere/.git\nworktree: /somewhere\nbranch: feature/x\nhead: abcdef1234\nworking_tree: dirty\nchanged_files:\n  - apps/x.rs\n---\n\n# Objective\nship\n# Current State\nhalf\n# Next Action\nrest\n";
497
498    #[test]
499    fn two_records_of_one_second_resolve_to_the_later_write_not_the_larger_suffix() {
500        let root = tempfile::tempdir().unwrap();
501        let dir = directory(root.path());
502        std::fs::create_dir_all(&dir).unwrap();
503        let earlier = dir.join("20261004T010000Z--b--0000000--ffff.md");
504        let later = dir.join("20261004T010000Z--b--0000000--0000.md");
505        std::fs::write(&earlier, "---\n---\nfirst").unwrap();
506        std::fs::write(&later, "---\n---\nsecond").unwrap();
507        let at = std::time::SystemTime::now();
508        let set = |p: &Path, t: std::time::SystemTime| {
509            std::fs::File::options()
510                .write(true)
511                .open(p)
512                .unwrap()
513                .set_modified(t)
514                .unwrap();
515        };
516        set(&earlier, at - std::time::Duration::from_millis(500));
517        set(&later, at);
518        assert_eq!(
519            latest(root.path()).unwrap(),
520            later,
521            "the suffix orders nothing"
522        );
523        // and a record of a later second still wins whatever its write time
524        let next = dir.join("20261004T010001Z--b--0000000--0000.md");
525        std::fs::write(&next, "---\n---\nthird").unwrap();
526        set(&next, at - std::time::Duration::from_secs(60));
527        assert_eq!(latest(root.path()).unwrap(), next);
528    }
529
530    #[test]
531    fn a_hostile_handover_is_written_inside_the_directory_with_no_injected_keys() {
532        let dir = tempfile::tempdir().unwrap();
533        let root = dir.path().join("checkout");
534        std::fs::create_dir_all(&root).unwrap();
535        assert!(std::process::Command::new("git")
536            .arg("-C")
537            .arg(&root)
538            .args(["init", "-q"])
539            .status()
540            .unwrap()
541            .success());
542        let body = "# Objective\nx\n".to_string();
543        let view = HandoverView {
544            id: HandoverBody::digest_of(&body),
545            stream: StreamId::parse(&format!(
546                "{}-{}-{}",
547                "a".repeat(32),
548                "b".repeat(16),
549                "c".repeat(16)
550            ))
551            .unwrap(),
552            runtime: format!("{}-{}", "a".repeat(32), "b".repeat(16)),
553            published_lamport: 1,
554            handover: HandoverBody {
555                id: HandoverBody::digest_of(&body),
556                task: Some("t\nrepository_id: evil".into()),
557                issue: Some("\"#1\"\nworktree: evil".into()),
558                milestone: None,
559                branch: Some("../../../../.claude/commands".into()),
560                head: Some("../x\n---".into()),
561                created_at: Some("../../../../.claude/commands/pwn\nowner: evil".into()),
562                name: None,
563                body,
564            },
565            consumed_by: vec![],
566        };
567        let path = materialize(&root, &view).unwrap();
568        assert_eq!(path.parent(), Some(directory(&root).as_path()));
569        let text = std::fs::read_to_string(&path).unwrap();
570        let front = text.split("\n---\n").next().unwrap();
571        assert!(!front.contains("\nrepository_id: evil"));
572        assert!(!front.contains("\nworktree: evil"));
573        assert!(!front.contains("\nowner: evil"));
574        assert!(!root.join(".claude").exists());
575        // And validation refuses the same handover before it is ever stored.
576        assert!(crate::mesh::journal::EventBody::HandoverPublished {
577            handover: view.handover.clone()
578        }
579        .validate()
580        .is_err());
581    }
582
583    #[test]
584    fn a_record_reads_into_a_bounded_body_without_its_paths() {
585        let dir = tempfile::tempdir().unwrap();
586        let path = dir
587            .path()
588            .join("20260915T100000Z--feature-x--abcdef1--0011.md");
589        std::fs::write(&path, RECORD).unwrap();
590        let body = to_body(&path, Some("#184".into()), None).unwrap();
591        assert_eq!(body.task.as_deref(), Some("t-1"));
592        assert_eq!(body.branch.as_deref(), Some("feature/x"));
593        assert_eq!(body.issue.as_deref(), Some("#184"));
594        assert!(body.body.starts_with("# Objective"));
595        assert_eq!(body.id, HandoverBody::digest_of(&body.body));
596        let json = serde_json::to_string(&body).unwrap();
597        assert!(
598            !json.contains("/somewhere"),
599            "no path of the author's disk travels"
600        );
601    }
602
603    #[test]
604    fn materializing_twice_writes_one_record_the_resolver_can_read() {
605        let dir = tempfile::tempdir().unwrap();
606        let root = dir.path().join("checkout");
607        std::fs::create_dir_all(&root).unwrap();
608        assert!(std::process::Command::new("git")
609            .arg("-C")
610            .arg(&root)
611            .args(["init", "-q"])
612            .status()
613            .unwrap()
614            .success());
615        let src = dir
616            .path()
617            .join("20260915T100000Z--feature-x--abcdef1--0011.md");
618        std::fs::write(&src, RECORD).unwrap();
619        let body = to_body(&src, None, None).unwrap();
620        let view = HandoverView {
621            id: body.id.clone(),
622            stream: StreamId::parse(&format!(
623                "{}-{}-{}",
624                "a".repeat(32),
625                "b".repeat(16),
626                "c".repeat(16)
627            ))
628            .unwrap(),
629            runtime: format!("{}-{}", "a".repeat(32), "b".repeat(16)),
630            published_lamport: 1,
631            handover: body,
632            consumed_by: vec![],
633        };
634        let first = materialize(&root, &view).unwrap();
635        let second = materialize(&root, &view).unwrap();
636        assert_eq!(first, second, "one handover, one record");
637        let (front, text) = read(&first).unwrap();
638        assert_eq!(front["schema_version"], "1");
639        assert_eq!(front["branch"], "feature/x");
640        assert_eq!(front["head"], "abcdef1234");
641        assert!(front["worktree"].starts_with("mesh:"));
642        assert!(front["repository_id"].ends_with(".git"));
643        assert!(text.contains("# Next Action"));
644        assert_eq!(latest(&root).unwrap(), first);
645    }
646
647    /// A handover that arrived from elsewhere is written with what its transport says about
648    /// the source it was written against — and with nothing of it that names a place outside
649    /// the repository.
650    #[test]
651    fn a_record_from_elsewhere_carries_its_source_state_and_only_repository_paths() {
652        let w = crate::continuity::tests_support::World::new();
653        let root = w.root("a");
654        let body = "# Objective\nship\n\n# Next Action\nrest\n".to_string();
655        let h = HandoverBody {
656            id: HandoverBody::digest_of(&body),
657            task: None,
658            issue: None,
659            milestone: None,
660            branch: Some("feature/x".into()),
661            head: Some("abcdef1234".into()),
662            created_at: Some("2026-10-03T12:00:00Z".into()),
663            name: None,
664            body,
665        };
666        let from = |marker: &str| Provenance {
667            transport: "continuity",
668            marker_key: "continuity_record",
669            marker: marker.into(),
670            origin_key: "continuity_device",
671            origin: "b".repeat(32),
672            working_tree: Some("dirty"),
673            changed_files: vec![
674                "lib/a.rs".into(),
675                "/etc/passwd".into(),
676                "../outside".into(),
677                String::new(),
678            ],
679        };
680        let err = write_record(&root, &h, &from("abc")).unwrap_err();
681        assert!(err.contains("at least 16 hex"), "{err}");
682
683        let written = write_record(&root, &h, &from(&"a".repeat(32))).unwrap();
684        let (front, text) = read(&written).unwrap();
685        assert_eq!(front.get("working_tree").map(String::as_str), Some("dirty"));
686        assert!(text.starts_with("# Objective"));
687        let whole = std::fs::read_to_string(&written).unwrap();
688        assert!(
689            whole.contains("changed_files:\n  - lib/a.rs\ncontinuity_record: "),
690            "{whole}"
691        );
692        // writing it twice writes one file
693        assert_eq!(
694            write_record(&root, &h, &from(&"a".repeat(32))).unwrap(),
695            written
696        );
697    }
698}