// SPDX-License-Identifier: GPL-3.0-only
//! `doctrine boot` — regenerate the cache-friendly governance snapshot.
//!
//! The snapshot is a **pure projection** of stable governance state into the
//! agent's cacheable session-start prefix (`@`-imported by `CLAUDE.md`), so an
//! agent pays for governance once per change, not once per session (SL-011 §1).
//!
//! Pure/imperative split (house rule): a pure assembler — `boot_sequence` +
//! `render_boot` — takes section bodies and yields a deterministic string with
//! no clock/rng/disk. The thin impure shell (`produce`, `write_if_changed`,
//! `run`) gathers bodies, resolves `current_exe()`, and reaches disk through the
//! shared `fsutil::write_atomic` seam, writing **only on content change** so an
//! unchanged snapshot keeps the cache warm (§1, the content-diff cache key).
//!
//! This phase (PHASE-01) stands up the frame: every section renders a benign
//! marker except `Invoking doctrine`, which carries the resolved exec path.
//! Later phases feed the ADR/memory/governance/static producers real bodies.

use std::collections::BTreeSet;
use std::ffi::OsStr;
use std::fs;
use std::io::{self, Write as _};
use std::path::{Path, PathBuf};

use anyhow::{Context, bail};
use serde_json::{Map, Value};

use crate::{adr, fsutil, governance, install, memory, policy, root, standard};

/// The snapshot lives in the runtime-state tree — derived, gitignored
/// (inherits the `.doctrine/*` ignore), `rm -rf`-able. Never authoritative.
const BOOT_REL: &str = ".doctrine/state/boot.md";

/// First line of the snapshot — warns a human off editing a regenerated file.
const BOOT_HEADER: &str =
    "<!-- Generated by `doctrine boot` — do not edit; regenerated each session -->";

/// The project's user-owned governance pointer layer — seeded by `doctrine
/// install` (Skip-if-exists), read here as the `Governance` section body, or a
/// benign marker when absent. NOT the embed: this file is the editable layer.
const GOVERNANCE_REL: &str = ".doctrine/governance.md";

// ---------------------------------------------------------------------------
// Pure: snapshot assembly (the extensible seam)
// ---------------------------------------------------------------------------

/// One rendered section: a heading and its already-produced body.
struct Section {
    heading: String,
    body: String,
}

/// Where a section's body comes from. New governance kinds append a `GovRows`
/// row to the sequence with no other change. `ExecPath` is build-volatile, not
/// governance, so it is ordered **last** (see `boot_sequence`).
enum SourceKind {
    /// The resolved `current_exe()` path — so the agent invokes the off-PATH CLI.
    ExecPath,
    /// An embedded digest asset, named by its `install/`-relative filename. The
    /// name selects the canonical embed read by `produce(Static)` via
    /// `install::asset_text` — deterministic and not user-clobberable.
    Static(&'static str),
    /// The project's `.doctrine/governance.md` body, read from disk (the
    /// user-owned layer), or a marker when absent.
    Governance,
    /// A governance-kind listing filtered to its in-force status SET — accepted
    /// ADRs, required policies, default+required standards. One arm projects all
    /// numbered governance kinds: the kind descriptor + the status set that
    /// reveals past the list hide-set (SL-033 — the per-kind-arm collapse).
    GovRows(&'static governance::GovKind, &'static [&'static str]),
    /// Memory pointers — its own variant: `memory::list_rows` takes a distinct
    /// signature (a `None` scope arg), not the `GovKind` spine.
    Memories,
}

/// The declared heading + source order. The `Invoking doctrine` (`ExecPath`)
/// section is **last**: the exec path is build-volatile (§5.3/F4), so ordering
/// it last confines a path change to the snapshot tail and leaves the
/// governance prefix cache-warm.
fn boot_sequence() -> Vec<(&'static str, SourceKind)> {
    vec![
        (
            "Routing & Process",
            SourceKind::Static("routing-process.md"),
        ),
        ("Governance (project)", SourceKind::Governance),
        (
            "Accepted ADRs",
            SourceKind::GovRows(&adr::ADR_KIND, &["accepted"]),
        ),
        (
            "Active Policies",
            SourceKind::GovRows(&policy::POLICY_KIND, &["required"]),
        ),
        (
            "Active Standards",
            SourceKind::GovRows(&standard::STANDARD_KIND, &["default", "required"]),
        ),
        ("Memory", SourceKind::Memories),
        // build-volatile — keep LAST so a path change hits only the cache tail.
        ("Invoking doctrine", SourceKind::ExecPath),
    ]
}

/// Assemble the snapshot string: header comment, title, then each section as
/// `## {heading}\n{body}`. Deterministic — same sections in, byte-identical out.
fn render_boot(sections: &[Section]) -> String {
    let mut out = String::new();
    out.push_str(BOOT_HEADER);
    out.push('\n');
    out.push_str("# Doctrine Boot Context\n");
    for section in sections {
        out.push_str("\n## ");
        out.push_str(&section.heading);
        out.push('\n');
        out.push_str(&section.body);
        out.push('\n');
    }
    out
}

/// The fixed body for a not-yet-populated section. Fixed (no clock/inputs) so
/// `render_boot` stays byte-stable and the content-diff cache key holds.
fn marker(label: &str) -> String {
    format!("<!-- {label}: not yet populated -->")
}

/// A governance-specific nudge for empty Policies/Standards sections — points
/// at the shipped signpost memory covering these governance kinds.
fn gov_nudge(heading: &str) -> String {
    let kind = heading
        .strip_prefix("Active ")
        .unwrap_or(heading)
        .to_lowercase();
    format!("<!-- No active {kind} yet. See mem.signpost.doctrine.policies-standards -->")
}

// ---------------------------------------------------------------------------
// Impure shell: produce each body, write on change, run the verb
// ---------------------------------------------------------------------------

/// Produce one section's body. `ExecPath` carries the resolved exec path; every
/// other kind renders the benign marker this phase. Never panics on a missing
/// source — a miss is a marker, not a crash (the ordering-knot break, §5.5).
fn produce(heading: &str, kind: &SourceKind, root: &Path, exec: &Path) -> Section {
    let body = match kind {
        SourceKind::ExecPath => exec.display().to_string(),
        // A numbered governance kind, filtered to its IN-FORCE status set: accepted
        // ADRs, required policies, default+required standards. The explicit
        // `status` set reveals these past the list hide-set (deprecated|retired|…) —
        // exactly the boot intent (SL-025 §5.1 — boot is a declared non-clap
        // consumer building a `listing::ListArgs` directly). One arm for every
        // kind (SL-033): a single-element set reproduces the old per-kind arms
        // byte-for-byte; a multi-element set (standards: default+required) is what
        // the `&[&str]` buys. The error≡empty marker collapse and supersession⇏status
        // gap are inherited, shared, and out of scope here (§5.5) — documented, not fixed.
        SourceKind::GovRows(kind, set) => {
            let produced = governance::list_rows(
                kind,
                root,
                crate::listing::ListArgs {
                    status: set.iter().map(|s| (*s).to_string()).collect(),
                    ..Default::default()
                },
            );
            // Empty Policies or Standards sections get a specific nudge
            // pointing at the shipped signpost memory; every other governance
            // kind gets the generic marker.
            match (&produced, heading) {
                (Ok(rows), "Active Policies" | "Active Standards") if rows.trim().is_empty() => {
                    gov_nudge(heading)
                }
                _ => section_or_marker(heading, produced),
            }
        }
        // ACTIVE memories only — an explicit boot predicate, DECOUPLED from the
        // CLI `memory list` default (which keeps `draft` visible). boot is an
        // agent-context PRODUCER and `draft` is unreviewed, so it must not leak
        // into the snapshot (SL-025 §5.1 / C-4). The explicit `status:["active"]`
        // also reveals past the list hide-set, mirroring the ADR section above.
        SourceKind::Memories => section_or_marker(
            heading,
            memory::list_rows(
                root,
                Some(memory::MemoryType::Signpost),
                crate::listing::ListArgs {
                    status: vec!["active".to_string()],
                    ..Default::default()
                },
            ),
        ),
        // canonical embedded digest — read from the embed, never disk (§5.2).
        SourceKind::Static(name) => section_or_marker(heading, install::asset_text(name)),
        // user-owned governance pointer layer — read from disk (§5.3).
        SourceKind::Governance => section_or_marker(
            heading,
            fs::read_to_string(root.join(GOVERNANCE_REL)).map_err(anyhow::Error::from),
        ),
    };
    Section {
        heading: heading.to_string(),
        body,
    }
}

/// Map a producer's result to a section body: the real rows when present, else
/// the benign marker. An error OR an empty listing both fall back to the marker —
/// a miss is never a crash (§5.5). The trailing newline is trimmed so a section
/// never grows a blank tail line under `render_boot`.
fn section_or_marker(heading: &str, produced: anyhow::Result<String>) -> String {
    match produced {
        Ok(rows) if !rows.trim().is_empty() => rows.trim_end().to_string(),
        _ => marker(heading),
    }
}

/// Write `content` to `path` only when it differs from what is already there,
/// returning whether a write happened. The content-diff is the cache key (§1):
/// a no-op write would needlessly bust the cached prefix. Ensures the parent
/// dir, then swaps atomically through `fsutil::write_atomic`.
fn write_if_changed(path: &Path, content: &str) -> anyhow::Result<bool> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .with_context(|| format!("Failed to create {}", parent.display()))?;
    }
    // any read failure (absent / unreadable) → treat as "differs" and rewrite.
    if fs::read_to_string(path).ok().as_deref() == Some(content) {
        return Ok(false);
    }
    fsutil::write_atomic(path, content.as_bytes())?;
    Ok(true)
}

/// Produce every section's body in the declared order — the single render path
/// shared by `regenerate` (which writes it) and `boot_check` (which diffs it).
/// One source of the snapshot, never a second fork (Charge V).
fn build_sections(root: &Path, exec: &Path) -> Vec<Section> {
    boot_sequence()
        .iter()
        .map(|(heading, kind)| produce(heading, kind, root, exec))
        .collect()
}

/// Regenerate `.doctrine/state/boot.md` under `root`, resolving section bodies
/// against `exec`. Returns whether the file changed. Pure assembly, single write.
fn regenerate(root: &Path, exec: &Path) -> anyhow::Result<bool> {
    let content = render_boot(&build_sections(root, exec));
    write_if_changed(&root.join(BOOT_REL), &content)
}

/// Whether a section still carries its not-yet-populated marker body — i.e. its
/// source was missing or empty at render time. Exact, not a substring grep: a
/// section is unpopulated iff its body is byte-equal to `marker(heading)` or
/// `gov_nudge(heading)` (SL-069 — governance empty-section nudge).
fn is_marker(section: &Section) -> bool {
    section.body == marker(&section.heading) || section.body == gov_nudge(&section.heading)
}

/// The disk freshness/health report (Charge II/III). DETERMINISTIC — built from
/// a recompute and a single on-disk read, no clock, no embedded timestamp (which
/// would bust the cache every session, §5.5). `stale` ⇒ on-disk `boot.md` differs
/// from the recompute; `marker_sections` ⇒ sections whose source is unpopulated.
#[derive(Debug, PartialEq, Eq)]
struct CheckReport {
    stale: bool,
    marker_sections: Vec<String>,
}

impl CheckReport {
    /// A current, fully-populated file: in sync on disk, every source projected.
    fn is_clean(&self) -> bool {
        !self.stale && self.marker_sections.is_empty()
    }
}

/// Recompute the snapshot and compare it to what is on disk (Charge II). A DISK
/// sentry, not a session sentry (§5.4, codex F2): it sees the *file* fresh while
/// the *current inlined prefix* stays stale until `/clear`/restart — so callers
/// must NOT read a clean report as proof the live context is fresh. Absent /
/// unreadable on-disk file ⇒ stale (the recompute differs from nothing).
fn boot_check(root: &Path, exec: &Path) -> CheckReport {
    let sections = build_sections(root, exec);
    let recomputed = render_boot(&sections);
    let on_disk = fs::read_to_string(root.join(BOOT_REL)).ok();
    CheckReport {
        stale: on_disk.as_deref() != Some(recomputed.as_str()),
        marker_sections: sections
            .iter()
            .filter(|s| is_marker(s))
            .map(|s| s.heading.clone())
            .collect(),
    }
}

/// `doctrine boot [-p ROOT]` — resolve the root, resolve `current_exe()` in the
/// shell (never in the pure layer), regenerate, and report wrote vs unchanged.
pub(crate) fn run(path: Option<PathBuf>) -> anyhow::Result<()> {
    let root = root::find(path, &root::default_markers())?;
    let exec = std::env::current_exe().context("Failed to resolve the doctrine executable path")?;
    let dest = root.join(BOOT_REL);
    let verb = if regenerate(&root, &exec)? {
        "Wrote"
    } else {
        "Unchanged"
    };
    writeln!(io::stdout(), "{verb} {}", dest.display())?;
    Ok(())
}

/// `doctrine boot --check` — resolve the root + `current_exe()` in the shell,
/// run the disk sentry, and report. DISK-scoped wording only: never claim the
/// current session's inlined prefix is fresh (§5.4 — that is `/route`'s lag
/// warning + the freshen-now ritual, not this verb).
pub(crate) fn run_check(path: Option<PathBuf>) -> anyhow::Result<()> {
    let root = root::find(path, &root::default_markers())?;
    let exec = std::env::current_exe().context("Failed to resolve the doctrine executable path")?;
    let report = boot_check(&root, &exec);
    let dest = root.join(BOOT_REL);
    let mut out = io::stdout();
    if report.is_clean() {
        writeln!(out, "clean {} — on-disk snapshot in sync", dest.display())?;
        return Ok(());
    }
    if report.stale {
        writeln!(
            out,
            "stale {} — differs from current governance; run `doctrine boot`, then `/clear` or restart",
            dest.display()
        )?;
    }
    if !report.marker_sections.is_empty() {
        writeln!(
            out,
            "unpopulated sections: {}",
            report.marker_sections.join(", ")
        )?;
    }
    Ok(())
}

// ===========================================================================
// PHASE-04 — harness seam + `doctrine boot install` wiring
// ===========================================================================
//
// Per-harness wiring behind an `enum Harness` + `match` seam (NOT trait/Box<dyn>
// — design D8/Charge IV; mirrors `skills.rs`'s Claude-vs-Other shape). Decision
// logic is pure (`plan_boot_import`, `plan_session_hook`); the disk read/write is
// a thin imperative wrapper (`ensure_boot_import`, `install_refresh`), so §9's
// merge matrix is plain unit tests with no disk (house rule, A1).

/// The machine-local Claude settings file carrying the `SessionStart` hook —
/// gitignored (the absolute exec path belongs out of git, §5.3/D6).
const SETTINGS_REL: &str = ".claude/settings.local.json";

/// The `SessionStart` matcher token — fires on a fresh session and on `/clear`
/// (`clear` firing a `SessionStart` hook is already witnessed; the OR-token is
/// confirmed live at closure, §6/PHASE-06).
const SESSION_MATCHER: &str = "startup|clear";

// ---------------------------------------------------------------------------
// The harness seam (R2) — enum + match, one local id per wired harness.
// ---------------------------------------------------------------------------

/// A wired harness. A third harness is a new arm, no framework (design D8).
/// Identity-unification with `skills::Agent` is deferred debt until SL-012 frees
/// `skills.rs` (the §3 concurrency gate forbids touching it now).
#[derive(Debug, PartialEq, Eq)]
enum Harness {
    Claude,
    Codex,
}

/// Parse an explicit `--agent` token. Unlike `skills::parse_agent` (which
/// delegates unknowns to `npx`), boot wires only the harnesses it knows, so an
/// unknown name is an error, not a passthrough.
fn parse_harness(s: &str) -> anyhow::Result<Harness> {
    if s.eq_ignore_ascii_case("claude") {
        Ok(Harness::Claude)
    } else if s.eq_ignore_ascii_case("codex") {
        Ok(Harness::Codex)
    } else {
        bail!("Unknown harness '{s}'. Known harnesses: claude, codex.")
    }
}

fn harness_label(h: &Harness) -> &'static str {
    match h {
        Harness::Claude => "claude",
        Harness::Codex => "codex",
    }
}

/// The committed file each harness `@`-imports the snapshot from. **One file per
/// harness** (review fix #1): Claude reads `CLAUDE.md`, codex `AGENTS.md` — never
/// both for one agent, else the snapshot would inline twice. This repo's
/// `CLAUDE.md → AGENTS.md` symlink makes the union dedup to a single inode.
fn import_targets(h: &Harness, root: &Path) -> Vec<PathBuf> {
    match h {
        Harness::Claude => vec![root.join("CLAUDE.md")],
        Harness::Codex => vec![root.join("AGENTS.md")],
    }
}

/// Resolve target harnesses: explicit `--agent` wins; else auto-detect by marker
/// (`.claude/` → Claude; `.codex/`, or an `AGENTS.md` that is not merely Claude's
/// inode-alias → Codex); ≥1 required (mirrors `skills::resolve_agents`). A repo
/// with `.claude/` and a separate-inode `AGENTS.md` wires both; this repo's
/// `AGENTS.md` symlink onto `CLAUDE.md` is Claude's import target via one inode,
/// not a codex surface, so it stays Claude-only (SL-063 §3.1).
fn resolve_harnesses(explicit: &[String], root: &Path) -> anyhow::Result<Vec<Harness>> {
    if !explicit.is_empty() {
        return explicit.iter().map(|s| parse_harness(s)).collect();
    }
    let claude = root.join(".claude").exists();
    let mut found = Vec::new();
    if claude {
        found.push(Harness::Claude);
    }
    // AGENTS.md is "merely Claude's alias" only when Claude is detected AND
    // AGENTS.md resolves to CLAUDE.md's inode (this repo's symlink) — then it is
    // Claude's import target, not a codex surface, so it does not trigger codex.
    // Otherwise a present AGENTS.md is a real codex surface — including a lone
    // symlinked pair with no `.claude/`, where alias-suppression is off because
    // Claude is not detected (SL-063 §3.1).
    let agents = root.join("AGENTS.md");
    let agents_is_claude_alias = claude
        && agents.exists()
        && resolve_target(&agents) == resolve_target(&root.join("CLAUDE.md"));
    if root.join(".codex").exists() || (agents.exists() && !agents_is_claude_alias) {
        found.push(Harness::Codex);
    }
    if found.is_empty() {
        bail!(
            "No --agent given and no .claude/ or .codex/ (or AGENTS.md) found. \
             Pass --agent <claude|codex>."
        );
    }
    Ok(found)
}

// ---------------------------------------------------------------------------
// Shared `@`-import wiring (harness-agnostic): pure plan + imperative apply.
// ---------------------------------------------------------------------------

/// The reportable result of wiring the import ref into one committed file.
#[derive(Debug, PartialEq, Eq)]
enum RefOutcome {
    /// The file was absent and created carrying just the ref.
    Created(PathBuf),
    /// The ref was prepended ahead of existing content.
    Added(PathBuf),
    /// The ref was already present — no write.
    Present(PathBuf),
}

/// Pure decision for one target's current content. Returns the action and the
/// new file body to write (`None` when nothing should be written). Idempotent:
/// a body already carrying the ref line plans no write.
fn plan_boot_import(existing: Option<&str>, reference: &str) -> (RefAction, Option<String>) {
    match existing {
        None => (RefAction::Create, Some(format!("{reference}\n"))),
        Some(content) if content.lines().any(|l| l.trim() == reference) => {
            (RefAction::Present, None)
        }
        Some(content) => (RefAction::Add, Some(format!("{reference}\n\n{content}"))),
    }
}

/// The pure action `plan_boot_import` chose; the wrapper pairs it with the path.
enum RefAction {
    Create,
    Add,
    Present,
}

/// Resolve a target to the path actually written: its canonical real path when it
/// exists (so a symlink target — `CLAUDE.md → AGENTS.md` — is updated through to
/// the one inode, never replaced by a regular file), else the path as given (a
/// to-be-created file has no inode to canonicalize). The dedup key is this same
/// resolved path, so two views of one inode collapse to a single write (A5/§5.5).
fn resolve_target(target: &Path) -> PathBuf {
    fs::canonicalize(target).unwrap_or_else(|_| target.to_path_buf())
}

/// Idempotent prepend of `reference` into each committed `target`: create the
/// file when absent, dedup same-inode targets to one write, preserve existing
/// content. `dry_run` computes outcomes but writes nothing (A7).
fn ensure_boot_import(
    targets: &[PathBuf],
    reference: &str,
    dry_run: bool,
) -> anyhow::Result<Vec<RefOutcome>> {
    let mut seen = BTreeSet::new();
    let mut outcomes = Vec::new();
    for target in targets {
        let resolved = resolve_target(target);
        if !seen.insert(resolved.clone()) {
            continue;
        }
        let existing = fs::read_to_string(&resolved).ok();
        let (action, new_body) = plan_boot_import(existing.as_deref(), reference);
        if let (Some(body), false) = (&new_body, dry_run) {
            if let Some(parent) = resolved.parent() {
                fs::create_dir_all(parent)
                    .with_context(|| format!("Failed to create {}", parent.display()))?;
            }
            fsutil::write_atomic(&resolved, body.as_bytes())?;
        }
        outcomes.push(match action {
            RefAction::Create => RefOutcome::Created(resolved),
            RefAction::Add => RefOutcome::Added(resolved),
            RefAction::Present => RefOutcome::Present(resolved),
        });
    }
    Ok(outcomes)
}

// ---------------------------------------------------------------------------
// Claude SessionStart hook merge: pure plan + imperative apply.
// ---------------------------------------------------------------------------

/// What the hook merge did, for reporting. The carried string is the command.
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum RefreshOutcome {
    /// No doctrine hook existed; one was appended.
    Wired(String),
    /// A doctrine hook existed with a stale command; it was refreshed.
    Refreshed(String),
    /// The settings file was malformed (or oddly shaped); left untouched, snippet
    /// printed by the caller — never clobbered (§5.3/D3).
    PrintedFallback,
    /// Nothing to do: the doctrine hook is already current, or codex (import-only).
    None,
}

/// A planned merge: the outcome plus the new settings JSON to write (`None` when
/// no write is needed).
struct HookPlan {
    outcome: RefreshOutcome,
    new_json: Option<String>,
}

fn printed_fallback() -> HookPlan {
    HookPlan {
        outcome: RefreshOutcome::PrintedFallback,
        new_json: None,
    }
}

/// The hook command for `exec`: `<exec> boot`. A **single** space-free argument
/// (`boot`) — the invariant the ownership match leans on (see
/// `is_doctrine_boot_command`).
fn boot_command(exec: &Path) -> String {
    format!("{} boot", exec.display())
}

/// The canonical hook entry doctrine writes for `spec` — the matcher is the
/// spec's own (not hardcoded), so a `SubagentStart` spec emits its agent-type
/// matcher while a `SessionStart` spec emits `startup|clear`.
fn desired_entry(spec: &HookSpec) -> Value {
    serde_json::json!({
        "matcher": spec.matcher,
        "hooks": [ { "type": "command", "command": spec.command } ],
    })
}

/// Whether `cmd` is doctrine's own `boot` hook — robust to spaces in the exec
/// path (Charge VII). The hook command is always `<program> boot`: a single
/// space-free trailing arg. So splitting on the **last** whitespace cleanly
/// separates the (possibly space-bearing) program from the `boot` arg — no shell
/// tokeniser needed while the command keeps that shape. Ours iff the trailing arg
/// is `boot` and the program's file name is `doctrine`. A foreign hook
/// (`tool boot`, `/x/doctrine-helper run`) never matches. NOTE: if the hook ever
/// grows a second/spaced arg, this must move to a real shell-word split.
fn is_doctrine_boot_command(cmd: &str) -> bool {
    let Some((program, arg)) = cmd.trim().rsplit_once(char::is_whitespace) else {
        return false;
    };
    arg == "boot" && Path::new(program.trim_end()).file_name() == Some(OsStr::new("doctrine"))
}

/// The hook command for `exec`: `<exec> memory sync`. TWO trailing args, so the
/// single-arg `rsplit_once` shape `is_doctrine_boot_command` leans on does NOT
/// apply — ownership matches by suffix-strip (see `is_doctrine_sync_command`).
fn sync_command(exec: &Path) -> String {
    format!("{} memory sync", exec.display())
}

/// Whether `cmd` is doctrine's own `memory sync` hook. The command is
/// `<program> memory sync` where `program` may bear spaces, so strip the fixed
/// ` memory sync` suffix and check the remaining program's file name is
/// `doctrine`. Disjoint from `is_doctrine_boot_command` (trailing arg `boot`):
/// neither predicate matches the other's command, so the two `SessionStart`
/// entries never clobber one another.
fn is_doctrine_sync_command(cmd: &str) -> bool {
    let Some(program) = cmd.trim().strip_suffix(" memory sync") else {
        return false;
    };
    Path::new(program.trim_end()).file_name() == Some(OsStr::new("doctrine"))
}

/// The `SubagentStart` stamp hook command for `exec`:
/// `<exec> worktree marker --stamp-subagent` (SL-056 PHASE-10). Multi-arg, so
/// ownership matches by suffix-strip (see `is_doctrine_stamp_command`).
fn stamp_subagent_command(exec: &Path) -> String {
    format!("{} worktree marker --stamp-subagent", exec.display())
}

/// Whether `cmd` is doctrine's own `worktree marker --stamp-subagent` hook.
/// Strip the fixed ` worktree marker --stamp-subagent` suffix and check the
/// remaining program's file name is `doctrine`. Disjoint from the boot/sync
/// predicates (neither owns the other's command), so the `SubagentStart` entry
/// never clobbers the `SessionStart` ones.
fn is_doctrine_stamp_command(cmd: &str) -> bool {
    let Some(program) = cmd.trim().strip_suffix(" worktree marker --stamp-subagent") else {
        return false;
    };
    Path::new(program.trim_end()).file_name() == Some(OsStr::new("doctrine"))
}

/// A `SessionStart` hook doctrine owns: its canonical `command` plus the predicate
/// that recognizes a prior (possibly stale) copy in foreign settings JSON. The
/// merge core (`plan_hook`/`find_owned`/`fallback_for`/`install_claude_hook`) is
/// generic over this; `boot install` and `memory sync install` are the two thin
/// callers (no-parallel-impl), differing only in command string + ownership.
pub(crate) struct HookSpec {
    command: String,
    is_ours: fn(&str) -> bool,
    /// The Claude hooks event this spec wires under (`hooks.<event>`).
    event: &'static str,
    /// The matcher token for this spec's entry.
    matcher: &'static str,
}

impl HookSpec {
    /// The `<exec> boot` hook (SL-011) — a `SessionStart` entry.
    fn boot(exec: &Path) -> Self {
        Self {
            command: boot_command(exec),
            is_ours: is_doctrine_boot_command,
            event: "SessionStart",
            matcher: SESSION_MATCHER,
        }
    }

    /// The `<exec> memory sync` hook (SL-018) — a SEPARATE `SessionStart` entry.
    pub(crate) fn sync(exec: &Path) -> Self {
        Self {
            command: sync_command(exec),
            is_ours: is_doctrine_sync_command,
            event: "SessionStart",
            matcher: SESSION_MATCHER,
        }
    }

    /// The `<exec> worktree marker --stamp-subagent` hook (SL-056 PHASE-11) — a
    /// `SubagentStart` entry, matcher-scoped to the dispatch-worker agent type.
    /// The matcher is sourced from `crate::worktree::DISPATCH_WORKER_AGENT_TYPE`,
    /// never re-spelled (a drift test pins the equality).
    pub(crate) fn stamp_subagent(exec: &Path) -> Self {
        Self {
            command: stamp_subagent_command(exec),
            is_ours: is_doctrine_stamp_command,
            event: "SubagentStart",
            matcher: crate::worktree::DISPATCH_WORKER_AGENT_TYPE,
        }
    }
}

/// Where a doctrine-owned `SessionStart` hook sits, relative to a desired command.
enum Owned {
    /// Present and already current — no write.
    Current,
    /// Present but stale — refresh the entry at `(entry_idx, hook_idx)`.
    Stale(usize, usize),
    /// Absent — append a fresh entry.
    Absent,
}

fn find_owned(arr: &[Value], desired_cmd: &str, is_ours: fn(&str) -> bool) -> Owned {
    for (ei, entry) in arr.iter().enumerate() {
        let Some(inner) = entry.get("hooks").and_then(Value::as_array) else {
            continue;
        };
        for (hi, hook) in inner.iter().enumerate() {
            let Some(cmd) = hook.get("command").and_then(Value::as_str) else {
                continue;
            };
            if is_ours(cmd) {
                return if cmd == desired_cmd {
                    Owned::Current
                } else {
                    Owned::Stale(ei, hi)
                };
            }
        }
    }
    Owned::Absent
}

/// Navigate (creating as needed) to the `hooks.<event>` array. Returns `None` —
/// caller treats as `PrintedFallback` — when `hooks` or `<event>` exists but is
/// the wrong JSON type, so a malformed-but-valid structure is never clobbered.
/// Generic over the event so the same merge core wires `SessionStart` (boot/sync)
/// and `SubagentStart` (stamp) without branching.
fn hook_array_mut<'a>(value: &'a mut Value, event: &str) -> Option<&'a mut Vec<Value>> {
    let obj = value.as_object_mut()?;
    let hooks = obj
        .entry("hooks")
        .or_insert_with(|| Value::Object(Map::new()));
    let entries = hooks
        .as_object_mut()?
        .entry(event)
        .or_insert_with(|| Value::Array(Vec::new()));
    entries.as_array_mut()
}

fn set_command(arr: &mut [Value], ei: usize, hi: usize, command: &str) -> Option<()> {
    let hook = arr
        .get_mut(ei)?
        .get_mut("hooks")?
        .as_array_mut()?
        .get_mut(hi)?
        .as_object_mut()?;
    hook.insert("command".to_string(), Value::String(command.to_string()));
    Some(())
}

/// `boot`'s thin caller over the generic merge core (`plan_hook`) — exercised by
/// boot's hook test matrix (the production refresh path goes via
/// `install_claude_hook`, so this wrapper is test-only).
#[cfg(test)]
fn plan_session_hook(existing_json: Option<&str>, exec: &Path) -> HookPlan {
    plan_hook(existing_json, &HookSpec::boot(exec))
}

/// The generic merge core: plan `spec`'s `SessionStart` hook into existing
/// settings `JSON`. Preserves every foreign hook and unrelated key (mutates a
/// `serde_json::Value` at the narrow path — never a typed round-trip that could
/// drop unknown keys). Malformed JSON or an unexpectedly-typed
/// `hooks`/`SessionStart` → fail soft. Both `boot install` and `memory sync
/// install` ride this — see `HookSpec`.
fn plan_hook(existing_json: Option<&str>, spec: &HookSpec) -> HookPlan {
    let command = spec.command.clone();
    let mut value: Value = match existing_json.map(str::trim) {
        None | Some("") => Value::Object(Map::new()),
        Some(text) => match serde_json::from_str(text) {
            Ok(parsed) => parsed,
            Err(_) => return printed_fallback(),
        },
    };
    let Some(arr) = hook_array_mut(&mut value, spec.event) else {
        return printed_fallback();
    };
    let outcome = match find_owned(arr, &command, spec.is_ours) {
        Owned::Current => {
            return HookPlan {
                outcome: RefreshOutcome::None,
                new_json: None,
            };
        }
        Owned::Stale(ei, hi) => {
            if set_command(arr, ei, hi, &command).is_none() {
                return printed_fallback();
            }
            RefreshOutcome::Refreshed(command.clone())
        }
        Owned::Absent => {
            arr.push(desired_entry(spec));
            RefreshOutcome::Wired(command.clone())
        }
    };
    match serde_json::to_string_pretty(&value) {
        Ok(json) => HookPlan {
            outcome,
            new_json: Some(json),
        },
        Err(_) => printed_fallback(),
    }
}

/// The snippet printed when the settings file can't be merged automatically —
/// generic over a `HookSpec`.
pub(crate) fn fallback_for(spec: &HookSpec) -> String {
    let entry = desired_entry(spec);
    serde_json::to_string_pretty(&entry).unwrap_or_else(|_| spec.command.clone())
}

/// `boot install`'s fallback snippet over the generic `fallback_for`.
fn fallback_snippet(exec: &Path) -> String {
    fallback_for(&HookSpec::boot(exec))
}

/// Imperative per-harness refresh. Claude merges the `SessionStart` hook into
/// `.claude/settings.local.json` (writes only on change, `!dry_run`) AND sets
/// `worktree.baseRef="head"` (SL-064 §8 — so the claude `/dispatch` arm's isolated
/// worker forks the orchestrator session HEAD, base==B by placement); codex is
/// import-only → no hook, no baseRef (its `AGENTS.md` ref is the shared
/// `ensure_boot_import` work). A malformed settings file is left untouched
/// (`PrintedFallback`).
fn install_refresh(
    h: &Harness,
    root: &Path,
    exec: &Path,
    dry_run: bool,
) -> anyhow::Result<RefreshReport> {
    match h {
        Harness::Codex => Ok(RefreshReport {
            hook: RefreshOutcome::None,
            baseref: BaseRefOutcome::NotApplicable,
        }),
        Harness::Claude => {
            // The hook merge writes first; the baseRef step reads the SAME settings
            // file AFTER, so its plan merges onto the just-written content (two
            // ordered atomic writes — design §8.3 risk note). The baseRef write
            // rides BESIDE the owner-locked HookSpec merge core, never through it.
            let hook = install_claude_hook(root, &HookSpec::boot(exec), dry_run)?;
            let baseref = install_baseref(root, dry_run)?;
            Ok(RefreshReport { hook, baseref })
        }
    }
}

/// The combined Claude refresh outcome: the `SessionStart` hook merge plus the
/// `worktree.baseRef` set (SL-064 §8). Codex carries `None`/`NotApplicable`.
struct RefreshReport {
    hook: RefreshOutcome,
    baseref: BaseRefOutcome,
}

/// What the `worktree.baseRef="head"` install did, for reporting (SL-064 §8.3).
/// Mirrors `RefreshOutcome` but for the distinct baseRef concern.
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum BaseRefOutcome {
    /// `worktree.baseRef` was absent; `"head"` was written.
    Set,
    /// Already `"head"` — no write (idempotent).
    AlreadyHead,
    /// Present with a NON-`head` value; left untouched, reported (no clobber —
    /// a per-operator deliberate override is respected, §8.3).
    Conflict(String),
    /// The settings file (or its `worktree` key) is malformed — left untouched.
    /// The hook leg flags the malformed file loudly, so this stays silent.
    PrintedFallback,
    /// Codex — no worktree baseRef concern.
    NotApplicable,
}

/// A planned baseRef set: the outcome plus the new settings JSON (`None` when no
/// write is needed). Mirrors `HookPlan`.
struct BaseRefPlan {
    outcome: BaseRefOutcome,
    new_json: Option<String>,
}

/// PURE planner for `worktree.baseRef="head"` in settings `JSON` (SL-064 §8.3).
/// Sets the nested `worktree.baseRef` key WITHOUT disturbing `hooks` or any other
/// `worktree` key (mutates a `serde_json::Value` at the narrow path). Absent ⇒
/// write `"head"`; already `"head"` ⇒ no-op; a non-`head` value or a malformed
/// file/`worktree` type ⇒ leave untouched (no clobber). Rides BESIDE the hook
/// merge core — it never touches `plan_hook`.
fn plan_baseref(existing_json: Option<&str>) -> BaseRefPlan {
    let mut value: Value = match existing_json.map(str::trim) {
        None | Some("") => Value::Object(Map::new()),
        Some(text) => match serde_json::from_str(text) {
            Ok(parsed) => parsed,
            Err(_) => {
                return BaseRefPlan {
                    outcome: BaseRefOutcome::PrintedFallback,
                    new_json: None,
                };
            }
        },
    };
    let Some(obj) = value.as_object_mut() else {
        return BaseRefPlan {
            outcome: BaseRefOutcome::PrintedFallback,
            new_json: None,
        };
    };
    let worktree = obj
        .entry("worktree")
        .or_insert_with(|| Value::Object(Map::new()));
    let Some(wt) = worktree.as_object_mut() else {
        // `worktree` present but not an object ⇒ malformed; never clobber.
        return BaseRefPlan {
            outcome: BaseRefOutcome::PrintedFallback,
            new_json: None,
        };
    };
    match wt.get("baseRef") {
        Some(Value::String(s)) if s == "head" => {
            return BaseRefPlan {
                outcome: BaseRefOutcome::AlreadyHead,
                new_json: None,
            };
        }
        Some(Value::String(s)) => {
            return BaseRefPlan {
                outcome: BaseRefOutcome::Conflict(s.clone()),
                new_json: None,
            };
        }
        Some(_non_string) => {
            return BaseRefPlan {
                outcome: BaseRefOutcome::Conflict("(non-string)".to_string()),
                new_json: None,
            };
        }
        None => {}
    }
    wt.insert("baseRef".to_string(), Value::String("head".to_string()));
    match serde_json::to_string_pretty(&value) {
        Ok(json) => BaseRefPlan {
            outcome: BaseRefOutcome::Set,
            new_json: Some(json),
        },
        Err(_) => BaseRefPlan {
            outcome: BaseRefOutcome::PrintedFallback,
            new_json: None,
        },
    }
}

/// Set `worktree.baseRef="head"` in `.claude/settings.local.json`, writing only on
/// change (unless `dry_run`). Reads → [`plan_baseref`] → atomic write, mirroring
/// [`install_claude_hook`]. Rides beside the hook installer in the Claude arm.
fn install_baseref(root: &Path, dry_run: bool) -> anyhow::Result<BaseRefOutcome> {
    let path = root.join(SETTINGS_REL);
    let existing = fs::read_to_string(&path).ok();
    let plan = plan_baseref(existing.as_deref());
    if let (Some(json), false) = (&plan.new_json, dry_run) {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)
                .with_context(|| format!("Failed to create {}", parent.display()))?;
        }
        fsutil::write_atomic(&path, json.as_bytes())?;
    }
    Ok(plan.outcome)
}

/// Merge a `HookSpec`'s `SessionStart` hook into `.claude/settings.local.json`,
/// writing only on change (unless `dry_run`). The generic Claude installer behind
/// both `boot install`'s refresh and `memory sync install` (SL-018).
pub(crate) fn install_claude_hook(
    root: &Path,
    spec: &HookSpec,
    dry_run: bool,
) -> anyhow::Result<RefreshOutcome> {
    let path = root.join(SETTINGS_REL);
    let existing = fs::read_to_string(&path).ok();
    let plan = plan_hook(existing.as_deref(), spec);
    if let (Some(json), false) = (&plan.new_json, dry_run) {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)
                .with_context(|| format!("Failed to create {}", parent.display()))?;
        }
        fsutil::write_atomic(&path, json.as_bytes())?;
    }
    Ok(plan.outcome)
}

// ---------------------------------------------------------------------------
// `doctrine boot install` orchestration.
// ---------------------------------------------------------------------------

/// `doctrine boot install` — resolve harnesses, union + dedup their import
/// targets, prepend the `@`-import **once**, then refresh each harness, isolating
/// a single harness's failure (it is printed; the others still run — A9/§5.5).
pub(crate) fn run_install(
    path: Option<PathBuf>,
    agents: &[String],
    dry_run: bool,
    yes: bool,
) -> anyhow::Result<()> {
    let root = root::find(path, &root::default_markers())?;
    let exec = std::env::current_exe().context("Failed to resolve the doctrine executable path")?;
    let harnesses = resolve_harnesses(agents, &root)?;

    if !yes && !dry_run {
        let label: Vec<&str> = harnesses.iter().map(harness_label).collect();
        let proceed = install::prompt_confirm(&format!(
            "Wire doctrine boot ({}) into {}? [y/N] ",
            label.join(", "),
            root.display()
        ))?;
        if !proceed {
            writeln!(io::stdout(), "Aborted.")?;
            return Ok(());
        }
    }
    wire(&root, &exec, &harnesses, dry_run)
}

/// The exec-injected orchestration core: the shell (`run_install`) resolves
/// `current_exe()` and prompts; this does the work — union + dedup import
/// targets, prepend the `@`-import **once**, then refresh each harness, isolating
/// a single harness's failure (it is printed; the others still run — A9/§5.5).
fn wire(root: &Path, exec: &Path, harnesses: &[Harness], dry_run: bool) -> anyhow::Result<()> {
    let reference = format!("@{BOOT_REL}");
    let targets: Vec<PathBuf> = harnesses
        .iter()
        .flat_map(|h| import_targets(h, root))
        .collect();

    let mut stdout = io::stdout();
    let tag = if dry_run { "[dry-run] " } else { "" };

    for outcome in ensure_boot_import(&targets, &reference, dry_run)? {
        let (verb, target) = match &outcome {
            RefOutcome::Created(p) => ("created", p),
            RefOutcome::Added(p) => ("added ref", p),
            RefOutcome::Present(p) => ("ref present", p),
        };
        writeln!(stdout, "  {tag}{verb}: {}", target.display())?;
    }

    for h in harnesses {
        match install_refresh(h, root, exec, dry_run) {
            Ok(report) => {
                match report.hook {
                    RefreshOutcome::Wired(cmd) => {
                        writeln!(stdout, "  {tag}{}: wired hook: {cmd}", harness_label(h))?;
                    }
                    RefreshOutcome::Refreshed(cmd) => {
                        writeln!(stdout, "  {tag}{}: refreshed hook: {cmd}", harness_label(h))?;
                    }
                    RefreshOutcome::PrintedFallback => {
                        writeln!(
                            stdout,
                            "  {}: {SETTINGS_REL} is malformed — add this hook manually:",
                            harness_label(h)
                        )?;
                        writeln!(stdout, "{}", fallback_snippet(exec))?;
                    }
                    RefreshOutcome::None => {}
                }
                // baseRef leg (SL-064 §8): report a fresh set or a respected
                // non-head override. AlreadyHead / PrintedFallback / NotApplicable
                // stay silent (no-op, or the hook leg already flagged a bad file).
                match report.baseref {
                    BaseRefOutcome::Set => {
                        writeln!(
                            stdout,
                            "  {tag}{}: set worktree.baseRef=head",
                            harness_label(h)
                        )?;
                    }
                    BaseRefOutcome::Conflict(value) => {
                        writeln!(
                            stdout,
                            "  {}: worktree.baseRef is '{value}', not 'head' — left as-is (claude isolated-worker base==B needs 'head'; set it manually if intended)",
                            harness_label(h)
                        )?;
                    }
                    BaseRefOutcome::AlreadyHead
                    | BaseRefOutcome::PrintedFallback
                    | BaseRefOutcome::NotApplicable => {}
                }
            }
            Err(e) => writeln!(stdout, "  {}: refresh failed: {e:#}", harness_label(h))?,
        }
    }
    Ok(())
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    fn headings() -> Vec<&'static str> {
        boot_sequence().iter().map(|(h, _)| *h).collect()
    }

    // --- VT-1: section order (ExecPath last) ---

    #[test]
    fn boot_sequence_orders_exec_path_last() {
        let seq = boot_sequence();
        // the build-volatile section is last; the four governance sections precede it.
        let last = seq.last().expect("non-empty sequence");
        assert!(
            matches!(last.1, SourceKind::ExecPath),
            "ExecPath must be last"
        );
        assert!(
            seq[..seq.len() - 1]
                .iter()
                .all(|(_, k)| !matches!(k, SourceKind::ExecPath)),
            "ExecPath must appear exactly once, at the tail",
        );
    }

    // --- VT-1: section order (Active Policies immediately after Accepted ADRs — EX-1) ---

    #[test]
    fn boot_sequence_orders_active_policies_after_accepted_adrs() {
        let h = headings();
        let adrs = h
            .iter()
            .position(|h| *h == "Accepted ADRs")
            .expect("Accepted ADRs present");
        let policies = h
            .iter()
            .position(|h| *h == "Active Policies")
            .expect("Active Policies present");
        let memory = h
            .iter()
            .position(|h| *h == "Memory")
            .expect("Memory present");
        assert_eq!(
            policies,
            adrs + 1,
            "Active Policies must sit immediately after Accepted ADRs"
        );
        assert!(policies < memory, "Active Policies must precede Memory");
    }

    // --- VT-2: section order (Active Standards between Active Policies and Memory) ---

    #[test]
    fn boot_sequence_orders_active_standards_after_active_policies() {
        let h = headings();
        let pos = |needle: &str| {
            h.iter()
                .position(|x| *x == needle)
                .unwrap_or_else(|| panic!("{needle} present"))
        };
        let adrs = pos("Accepted ADRs");
        let policies = pos("Active Policies");
        let standards = pos("Active Standards");
        let memory = pos("Memory");
        assert!(adrs < policies, "ADRs precede Policies");
        assert_eq!(
            standards,
            policies + 1,
            "Active Standards must sit immediately after Active Policies"
        );
        assert!(standards < memory, "Active Standards must precede Memory");
    }

    // --- VT-1: render determinism + structure ---

    #[test]
    fn render_boot_is_byte_deterministic_and_structured() {
        let sections = vec![
            Section {
                heading: "Alpha".into(),
                body: "one".into(),
            },
            Section {
                heading: "Beta".into(),
                body: "two".into(),
            },
        ];
        let once = render_boot(&sections);
        assert_eq!(
            once,
            render_boot(&sections),
            "repeated render must be byte-identical"
        );
        assert_eq!(
            once,
            format!("{BOOT_HEADER}\n# Doctrine Boot Context\n\n## Alpha\none\n\n## Beta\ntwo\n"),
        );
    }

    // --- VT-1: missing source renders a marker, not a crash ---

    #[test]
    fn produce_markers_a_non_exec_source_and_carries_the_exec_path() {
        let root = Path::new("/r");
        let exec = Path::new("/abs/target/debug/doctrine");

        let memo = produce("Memory", &SourceKind::Memories, root, exec);
        assert_eq!(memo.body, "<!-- Memory: not yet populated -->");

        let invoke = produce("Invoking doctrine", &SourceKind::ExecPath, root, exec);
        assert_eq!(invoke.body, "/abs/target/debug/doctrine");
    }

    // --- VT-2: produce(Static) reads the embed; a missing asset → marker ---

    #[test]
    fn produce_static_reads_the_embed_and_markers_a_missing_asset() {
        let root = Path::new("/r");
        let exec = Path::new("/abs/target/debug/doctrine");

        let digest = produce(
            "Routing & Process",
            &SourceKind::Static("routing-process.md"),
            root,
            exec,
        );
        assert!(
            digest.body.contains("Route before you act"),
            "embedded digest projected:\n{}",
            digest.body
        );

        let missing = produce(
            "Routing & Process",
            &SourceKind::Static("no-such-asset.md"),
            root,
            exec,
        );
        assert_eq!(
            missing.body, "<!-- Routing & Process: not yet populated -->",
            "a missing embed asset is a marker, never a crash",
        );
    }

    // --- VT-2: produce(Governance) reads disk; absent → marker ---

    #[test]
    fn produce_governance_reads_disk_or_markers_when_absent() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();
        let exec = Path::new("/abs/target/debug/doctrine");

        let absent = produce("Governance (project)", &SourceKind::Governance, root, exec);
        assert_eq!(
            absent.body, "<!-- Governance (project): not yet populated -->",
            "absent governance.md → marker",
        );

        let gov = root.join(GOVERNANCE_REL);
        fs::create_dir_all(gov.parent().unwrap()).unwrap();
        fs::write(
            &gov,
            "# Governance (project)\n\nlive once, in the prefix.\n",
        )
        .unwrap();
        let present = produce("Governance (project)", &SourceKind::Governance, root, exec);
        assert!(
            present.body.contains("live once, in the prefix."),
            "governance.md body projected:\n{}",
            present.body
        );
    }

    // --- VT-2: write_if_changed ---

    #[test]
    fn write_if_changed_writes_on_new_and_change_noops_on_equal() {
        let dir = tempfile::tempdir().unwrap();
        // nested path proves the parent dir is created.
        let path = dir.path().join(".doctrine/state/boot.md");

        assert!(write_if_changed(&path, "v1").unwrap(), "new file → wrote");
        assert_eq!(fs::read_to_string(&path).unwrap(), "v1");

        assert!(
            !write_if_changed(&path, "v1").unwrap(),
            "byte-equal → no-op"
        );
        assert!(write_if_changed(&path, "v2").unwrap(), "changed → wrote");
        assert_eq!(fs::read_to_string(&path).unwrap(), "v2");
    }

    // --- VT-3: in-process integration (no spawn — dodges stale CARGO_BIN_EXE) ---

    #[test]
    fn regenerate_emits_header_all_headings_exec_path_then_noops() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();
        let exec = Path::new("/abs/target/debug/doctrine");

        assert!(regenerate(root, exec).unwrap(), "first regenerate writes");
        let snapshot = fs::read_to_string(root.join(BOOT_REL)).unwrap();

        assert!(snapshot.starts_with(BOOT_HEADER), "header present");
        for heading in headings() {
            assert!(
                snapshot.contains(&format!("## {heading}")),
                "heading {heading} present"
            );
        }
        assert!(
            snapshot.contains("/abs/target/debug/doctrine"),
            "exec path present"
        );

        assert!(
            !regenerate(root, exec).unwrap(),
            "second regenerate is a no-op write"
        );
    }

    // --- VT-1: section_or_marker — real rows, else the benign marker ---

    #[test]
    fn section_or_marker_keeps_rows_and_falls_back_on_empty_or_error() {
        assert_eq!(
            section_or_marker("Accepted ADRs", Ok("a\nb\n".into())),
            "a\nb"
        );
        assert_eq!(
            section_or_marker("Accepted ADRs", Ok("  \n".into())),
            "<!-- Accepted ADRs: not yet populated -->",
            "empty listing → marker",
        );
        assert_eq!(
            section_or_marker("Memory", Err(anyhow::anyhow!("boom"))),
            "<!-- Memory: not yet populated -->",
            "producer error → marker, never a crash",
        );
    }

    // --- VT-3: real producers — accepted ADRs (filtered) + memory pointers ---

    #[test]
    fn regenerate_projects_accepted_adrs_and_memory_pointers() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();
        let exec = Path::new("/abs/target/debug/doctrine");

        // two ADRs, only the first accepted.
        adr::run_new(Some(root.to_path_buf()), Some("Use Rust".into()), None).unwrap();
        adr::run_new(Some(root.to_path_buf()), Some("Adopt CI".into()), None).unwrap();
        adr::run_status(Some(root.to_path_buf()), 1, adr::AdrStatus::Accepted).unwrap();

        // one memory pointer.
        memory::run_record(
            Some(root.to_path_buf()),
            &memory::RecordArgs {
                title: "Boot pointer note",
                memory_type: memory::MemoryType::Signpost,
                key: None,
                status: memory::Status::Active,
                summary: None,
                tags: &[],
                paths: &[],
                globs: &[],
                commands: &[],
                repo: None,
                global: false,
            },
        )
        .unwrap();

        assert!(regenerate(root, exec).unwrap());
        let snap = fs::read_to_string(root.join(BOOT_REL)).unwrap();

        // SL-025: the ADR section now renders via the migrated spine path —
        // prefixed ADR- ids + a header row.
        assert!(
            snap.contains("ADR-001 │ accepted"),
            "accepted ADR row projected with prefixed id:\n{snap}"
        );
        assert!(
            snap.lines()
                .any(|l| l.starts_with("id") && l.contains("status")),
            "ADR section carries the header row:\n{snap}"
        );
        assert!(
            !snap.contains("adopt-ci"),
            "non-accepted ADR filtered:\n{snap}"
        );
        assert!(
            snap.contains("Boot pointer note"),
            "memory pointer projected:\n{snap}"
        );
        assert!(!snap.contains("<!-- Accepted ADRs:"), "ADR marker replaced");
        assert!(!snap.contains("<!-- Memory:"), "memory marker replaced");
    }

    // --- VT-1/VT-2: real producer — required policies (filtered) + empty marker ---

    #[test]
    fn regenerate_projects_required_policies_filtered() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();
        let exec = Path::new("/abs/target/debug/doctrine");

        // four policies, one per status — only the `required` one is in force.
        policy::run_new(
            Some(root.to_path_buf()),
            Some("Commit cadence".into()),
            None,
        )
        .unwrap();
        policy::run_new(
            Some(root.to_path_buf()),
            Some("Branch hygiene".into()),
            None,
        )
        .unwrap();
        policy::run_new(Some(root.to_path_buf()), Some("Old rule".into()), None).unwrap();
        policy::run_new(Some(root.to_path_buf()), Some("Dead rule".into()), None).unwrap();
        policy::run_status(Some(root.to_path_buf()), 1, policy::PolicyStatus::Required).unwrap();
        policy::run_status(
            Some(root.to_path_buf()),
            3,
            policy::PolicyStatus::Deprecated,
        )
        .unwrap();
        policy::run_status(Some(root.to_path_buf()), 4, policy::PolicyStatus::Retired).unwrap();
        // policy 2 stays `draft`.

        assert!(regenerate(root, exec).unwrap());
        let snap = fs::read_to_string(root.join(BOOT_REL)).unwrap();

        // the required policy projects with a prefixed POL- id + header row.
        assert!(
            snap.contains("POL-001 │ required"),
            "required policy row projected with prefixed id:\n{snap}"
        );
        // draft / deprecated / retired are all absent — boot is the in-force view.
        // scope the negative checks to the Active Policies section body only.
        let section = snap
            .split_once("## Active Policies\n")
            .map(|(_, tail)| tail.split_once("\n## ").map_or(tail, |(body, _)| body))
            .expect("Active Policies section present");
        for hidden in ["branch-hygiene", "old-rule", "dead-rule"] {
            assert!(
                !section.contains(hidden),
                "non-required policy {hidden} must not project:\n{section}"
            );
        }
        assert!(
            !snap.contains("<!-- Active Policies:"),
            "policy marker replaced when a required policy exists"
        );
    }

    // --- VT-3/VT-4: real producer — in-force standards (default + required), the
    // first kind with a two-element in-force set. A one-char edit dropping a
    // status from the GovRows set turns this red (VT-4 — the gate bites). ---

    #[test]
    fn regenerate_projects_in_force_standards_filtered() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();
        let exec = Path::new("/abs/target/debug/doctrine");

        // five standards, one per status — only `default` + `required` are in force.
        standard::run_new(Some(root.to_path_buf()), Some("A draft rule".into()), None).unwrap();
        standard::run_new(
            Some(root.to_path_buf()),
            Some("A default rule".into()),
            None,
        )
        .unwrap();
        standard::run_new(
            Some(root.to_path_buf()),
            Some("A required rule".into()),
            None,
        )
        .unwrap();
        standard::run_new(Some(root.to_path_buf()), Some("An old rule".into()), None).unwrap();
        standard::run_new(Some(root.to_path_buf()), Some("A dead rule".into()), None).unwrap();
        standard::run_status(
            Some(root.to_path_buf()),
            2,
            standard::StandardStatus::Default,
        )
        .unwrap();
        standard::run_status(
            Some(root.to_path_buf()),
            3,
            standard::StandardStatus::Required,
        )
        .unwrap();
        standard::run_status(
            Some(root.to_path_buf()),
            4,
            standard::StandardStatus::Deprecated,
        )
        .unwrap();
        standard::run_status(
            Some(root.to_path_buf()),
            5,
            standard::StandardStatus::Retired,
        )
        .unwrap();
        // standard 1 stays `draft`.

        assert!(regenerate(root, exec).unwrap());
        let snap = fs::read_to_string(root.join(BOOT_REL)).unwrap();

        // both in-force standards project with prefixed STD- ids.
        assert!(
            snap.contains("STD-002 │ default"),
            "default standard row projected with prefixed id:\n{snap}"
        );
        assert!(
            snap.contains("STD-003 │ required"),
            "required standard row projected with prefixed id:\n{snap}"
        );
        // draft / deprecated / retired are absent — boot is the in-force view.
        // scope the negative checks to the Active Standards section body only.
        let section = snap
            .split_once("## Active Standards\n")
            .map(|(_, tail)| tail.split_once("\n## ").map_or(tail, |(body, _)| body))
            .expect("Active Standards section present");
        for hidden in ["a-draft-rule", "an-old-rule", "a-dead-rule"] {
            assert!(
                !section.contains(hidden),
                "out-of-force standard {hidden} must not project:\n{section}"
            );
        }
        assert!(
            !snap.contains("<!-- Active Standards:"),
            "standard marker replaced when an in-force standard exists"
        );
    }

    #[test]
    fn regenerate_empty_policy_corpus_renders_marker() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();
        let exec = Path::new("/abs/target/debug/doctrine");

        // a single draft policy — zero `required`, so the section is empty-in-force.
        policy::run_new(Some(root.to_path_buf()), Some("Just a draft".into()), None).unwrap();

        assert!(regenerate(root, exec).unwrap());
        let snap = fs::read_to_string(root.join(BOOT_REL)).unwrap();
        assert!(
            snap.contains(
                "<!-- No active policies yet. See mem.signpost.doctrine.policies-standards -->"
            ),
            "zero required policies → governance nudge, never a partial/stale row:\n{snap}"
        );
        assert!(
            !snap.contains("just-a-draft"),
            "draft policy must not leak into the in-force section:\n{snap}"
        );
    }

    /// VT-3 (SL-025 §5.1 / C-4): boot's memory section is rendered ACTIVE-ONLY via
    /// an explicit predicate, DECOUPLED from the CLI `memory list` default. The
    /// fixture spans active + draft + each hidden lifecycle state. Boot must show
    /// ONLY the active memory (no `draft` leak into agent context), while the CLI
    /// list (`memory::list_rows` default) hides the four terminal states but KEEPS
    /// draft — two different visibility rules, both asserted here.
    #[test]
    fn boot_memory_section_is_active_only_decoupled_from_the_list_default() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();
        let exec = Path::new("/abs/target/debug/doctrine");

        // an active + a draft via the real record path (born-frame capture is fine
        // in a non-git temp dir — anchor `none`, unscoped).
        let rec = |title: &str, status: memory::Status| {
            memory::run_record(
                Some(root.to_path_buf()),
                &memory::RecordArgs {
                    title,
                    memory_type: memory::MemoryType::Signpost,
                    key: None,
                    status,
                    summary: None,
                    tags: &[],
                    paths: &[],
                    globs: &[],
                    commands: &[],
                    repo: None,
                    global: false,
                },
            )
            .unwrap();
        };
        rec("Active note", memory::Status::Active);
        rec("Draft note", memory::Status::Draft);
        rec("Superseded note", memory::Status::Superseded);
        rec("Retracted note", memory::Status::Retracted);
        rec("Archived note", memory::Status::Archived);
        rec("Quarantined note", memory::Status::Quarantined);

        // boot section: ACTIVE ONLY — no draft, no terminal.
        assert!(regenerate(root, exec).unwrap());
        let snap = fs::read_to_string(root.join(BOOT_REL)).unwrap();
        assert!(snap.contains("Active note"), "boot shows active:\n{snap}");
        for leaked in [
            "Draft note",
            "Superseded note",
            "Retracted note",
            "Archived note",
            "Quarantined note",
        ] {
            assert!(
                !snap.contains(leaked),
                "boot must not leak {leaked}:\n{snap}"
            );
        }

        // CLI list DEFAULT: hides the four terminal but KEEPS draft (decoupled rule).
        let listed = memory::list_rows(root, None, crate::listing::ListArgs::default()).unwrap();
        assert!(
            listed.contains("Active note"),
            "list shows active:\n{listed}"
        );
        assert!(listed.contains("Draft note"), "list KEEPS draft:\n{listed}");
        for hidden in [
            "Superseded note",
            "Retracted note",
            "Archived note",
            "Quarantined note",
        ] {
            assert!(
                !listed.contains(hidden),
                "list hides terminal {hidden}:\n{listed}"
            );
        }
    }

    /// VT-1 (SL-069): the Memory section renders ONLY signpost-type memories —
    /// concepts, patterns, facts, and other types are excluded. This ensures the
    /// boot snapshot stays compact and carries only navigational pointers.
    #[test]
    fn boot_memory_section_is_signpost_only_excludes_other_types() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();
        let exec = Path::new("/abs/target/debug/doctrine");

        let rec = |title: &str, ty: memory::MemoryType| {
            memory::run_record(
                Some(root.to_path_buf()),
                &memory::RecordArgs {
                    title,
                    memory_type: ty,
                    key: None,
                    status: memory::Status::Active,
                    summary: None,
                    tags: &[],
                    paths: &[],
                    globs: &[],
                    commands: &[],
                    repo: None,
                    global: false,
                },
            )
            .unwrap();
        };
        rec("Signpost pointer", memory::MemoryType::Signpost);
        rec("Concept note", memory::MemoryType::Concept);
        rec("Pattern note", memory::MemoryType::Pattern);
        rec("Fact note", memory::MemoryType::Fact);

        assert!(regenerate(root, exec).unwrap());
        let snap = fs::read_to_string(root.join(BOOT_REL)).unwrap();
        assert!(
            snap.contains("Signpost pointer"),
            "signpost-type memory projected:\n{snap}"
        );
        for excluded in ["Concept note", "Pattern note", "Fact note"] {
            assert!(
                !snap.contains(excluded),
                "non-signpost {excluded} must not appear in boot:\n{snap}"
            );
        }
    }

    // --- VT-3: integration — routing digest (embed) + governance body (disk) ---

    #[test]
    fn regenerate_projects_routing_digest_and_governance_body() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();
        let exec = Path::new("/abs/target/debug/doctrine");

        // seed the user-owned governance layer.
        let gov = root.join(GOVERNANCE_REL);
        fs::create_dir_all(gov.parent().unwrap()).unwrap();
        fs::write(&gov, "# Governance (project)\n\npoint at doc/spec.md\n").unwrap();

        assert!(regenerate(root, exec).unwrap());
        let snap = fs::read_to_string(root.join(BOOT_REL)).unwrap();

        assert!(
            snap.contains("Route before you act"),
            "routing digest (embed) projected:\n{snap}"
        );
        // ADR-005 / SL-023 PHASE-03 (VT-2): the reference-forms rules + the
        // reference-docs pointer ride the push digest.
        assert!(
            snap.contains("Reference forms"),
            "reference-forms block projected onto the snapshot:\n{snap}"
        );
        assert!(
            snap.contains("Reference docs") && snap.contains("using-doctrine.md"),
            "reference-docs pointer projected onto the snapshot:\n{snap}"
        );
        assert!(
            snap.contains("point at doc/spec.md"),
            "governance body (disk) projected:\n{snap}"
        );
        assert!(
            !snap.contains("<!-- Routing & Process:"),
            "routing marker replaced"
        );
        assert!(
            !snap.contains("<!-- Governance (project):"),
            "governance marker replaced"
        );
    }

    // =======================================================================
    // PHASE-05 — `boot --check` disk sentry
    // =======================================================================

    // --- VT-1: a fully-populated, in-sync file reports clean ---

    #[test]
    fn boot_check_reports_clean_when_populated_and_in_sync() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();
        let exec = Path::new("/abs/target/debug/doctrine");

        // populate every disk/store-backed source so no section markers.
        let gov = root.join(GOVERNANCE_REL);
        fs::create_dir_all(gov.parent().unwrap()).unwrap();
        fs::write(&gov, "# Governance\n\npoint at doc/spec.md\n").unwrap();
        adr::run_new(Some(root.to_path_buf()), Some("Use Rust".into()), None).unwrap();
        adr::run_status(Some(root.to_path_buf()), 1, adr::AdrStatus::Accepted).unwrap();
        policy::run_new(
            Some(root.to_path_buf()),
            Some("Commit cadence".into()),
            None,
        )
        .unwrap();
        policy::run_status(Some(root.to_path_buf()), 1, policy::PolicyStatus::Required).unwrap();
        standard::run_new(
            Some(root.to_path_buf()),
            Some("Two-space indent".into()),
            None,
        )
        .unwrap();
        standard::run_status(
            Some(root.to_path_buf()),
            1,
            standard::StandardStatus::Required,
        )
        .unwrap();
        memory::run_record(
            Some(root.to_path_buf()),
            &memory::RecordArgs {
                title: "Boot pointer note",
                memory_type: memory::MemoryType::Signpost,
                key: None,
                status: memory::Status::Active,
                summary: None,
                tags: &[],
                paths: &[],
                globs: &[],
                commands: &[],
                repo: None,
                global: false,
            },
        )
        .unwrap();

        assert!(regenerate(root, exec).unwrap(), "seed the on-disk snapshot");
        let report = boot_check(root, exec);
        assert!(
            report.is_clean(),
            "fully populated + in sync → clean: stale={}, markers={:?}",
            report.stale,
            report.marker_sections
        );
    }

    // --- VT-1: an edited / absent on-disk file trips stale ---

    #[test]
    fn boot_check_flags_disk_staleness() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();
        let exec = Path::new("/abs/target/debug/doctrine");

        // absent file: recompute differs from nothing → stale.
        assert!(boot_check(root, exec).stale, "absent boot.md → stale");

        // a hand-edited file diverges from the recompute → stale.
        regenerate(root, exec).unwrap();
        let dest = root.join(BOOT_REL);
        fs::write(&dest, "# tampered\n").unwrap();
        assert!(boot_check(root, exec).stale, "edited boot.md → stale");

        // regenerate restores byte-equality → clean.
        regenerate(root, exec).unwrap();
        assert!(!boot_check(root, exec).stale, "regenerated → not stale");
    }

    // --- VT-1: unpopulated sources surface as marker_sections ---

    #[test]
    fn boot_check_tallies_marker_sections() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();
        let exec = Path::new("/abs/target/debug/doctrine");

        // bare root: no ADRs, no memory, no governance.md → those sections marker.
        regenerate(root, exec).unwrap();
        let bare = boot_check(root, exec);
        assert!(!bare.stale, "freshly written → not stale");
        assert!(
            bare.marker_sections
                .contains(&"Governance (project)".to_string()),
            "absent governance.md is an unpopulated section: {:?}",
            bare.marker_sections
        );
        // the exec-path section is always populated — never a marker.
        assert!(
            !bare
                .marker_sections
                .contains(&"Invoking doctrine".to_string()),
            "exec path is always populated: {:?}",
            bare.marker_sections
        );
        assert!(!bare.is_clean(), "marker sections present → not clean");

        // seed governance.md → that section is no longer a marker.
        let gov = root.join(GOVERNANCE_REL);
        fs::create_dir_all(gov.parent().unwrap()).unwrap();
        fs::write(&gov, "# Governance\n\npoint at doc/spec.md\n").unwrap();
        regenerate(root, exec).unwrap();
        let seeded = boot_check(root, exec);
        assert!(
            !seeded
                .marker_sections
                .contains(&"Governance (project)".to_string()),
            "seeded governance.md drops the marker: {:?}",
            seeded.marker_sections
        );
    }

    /// VT-3 (SL-069): `boot --check` reports empty Policies and Standards
    /// sections as unpopulated (governance nudge, not generic marker).
    #[test]
    fn boot_check_reports_empty_governance_as_unpopulated() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();
        let exec = Path::new("/abs/target/debug/doctrine");

        // fresh root: zero policies, zero standards → both sections emit nudges.
        regenerate(root, exec).unwrap();
        let report = boot_check(root, exec);
        assert!(
            report
                .marker_sections
                .contains(&"Active Policies".to_string()),
            "empty policies reported: {:?}",
            report.marker_sections
        );
        assert!(
            report
                .marker_sections
                .contains(&"Active Standards".to_string()),
            "empty standards reported: {:?}",
            report.marker_sections
        );
        assert!(report.marker_sections.contains(&"Memory".to_string()));
    }

    /// VT-4 (SL-069): `boot --check` does NOT report Policies/Standards as
    /// unpopulated when they carry in-force rows (populated governance).
    #[test]
    fn boot_check_populated_governance_does_not_warn() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();
        let exec = Path::new("/abs/target/debug/doctrine");

        // seed one required policy + one required standard.
        policy::run_new(
            Some(root.to_path_buf()),
            Some("Commit cadence".into()),
            None,
        )
        .unwrap();
        policy::run_status(Some(root.to_path_buf()), 1, policy::PolicyStatus::Required).unwrap();
        standard::run_new(
            Some(root.to_path_buf()),
            Some("Two-space indent".into()),
            None,
        )
        .unwrap();
        standard::run_status(
            Some(root.to_path_buf()),
            1,
            standard::StandardStatus::Required,
        )
        .unwrap();

        regenerate(root, exec).unwrap();
        let report = boot_check(root, exec);
        assert!(
            !report
                .marker_sections
                .contains(&"Active Policies".to_string()),
            "populated Policies → not a marker: {:?}",
            report.marker_sections
        );
        assert!(
            !report
                .marker_sections
                .contains(&"Active Standards".to_string()),
            "populated Standards → not a marker: {:?}",
            report.marker_sections
        );
        // Memory is still a marker (no signpost memories created).
        assert!(report.marker_sections.contains(&"Memory".to_string()));
    }

    // --- determinism: no clock — repeated checks are byte-identical ---

    #[test]
    fn boot_check_is_deterministic() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();
        let exec = Path::new("/abs/target/debug/doctrine");
        regenerate(root, exec).unwrap();
        assert_eq!(
            boot_check(root, exec),
            boot_check(root, exec),
            "no clock/rng → identical reports"
        );
    }

    // =======================================================================
    // PHASE-04 — harness seam + `boot install`
    // =======================================================================

    const REF: &str = "@.doctrine/state/boot.md";

    fn session_entries(json: &str) -> Vec<Value> {
        let value: Value = serde_json::from_str(json).expect("valid JSON");
        value
            .get("hooks")
            .and_then(|h| h.get("SessionStart"))
            .and_then(Value::as_array)
            .expect("SessionStart array")
            .clone()
    }

    fn commands(json: &str) -> Vec<String> {
        session_entries(json)
            .iter()
            .filter_map(|e| e.get("hooks").and_then(Value::as_array))
            .flatten()
            .filter_map(|h| h.get("command").and_then(Value::as_str))
            .map(str::to_string)
            .collect()
    }

    // --- T3: parse / resolve / import_targets ---

    #[test]
    fn parse_harness_known_and_unknown() {
        assert_eq!(parse_harness("claude").unwrap(), Harness::Claude);
        assert_eq!(parse_harness("CODEX").unwrap(), Harness::Codex);
        assert!(
            parse_harness("cursor")
                .unwrap_err()
                .to_string()
                .contains("Unknown harness")
        );
    }

    #[test]
    fn import_targets_is_one_file_per_harness() {
        let root = Path::new("/r");
        assert_eq!(
            import_targets(&Harness::Claude, root),
            vec![root.join("CLAUDE.md")]
        );
        assert_eq!(
            import_targets(&Harness::Codex, root),
            vec![root.join("AGENTS.md")]
        );
    }

    #[test]
    fn resolve_harnesses_explicit_wins() {
        let root = Path::new("/r");
        let got = resolve_harnesses(&["codex".into(), "claude".into()], root).unwrap();
        assert_eq!(got, vec![Harness::Codex, Harness::Claude]);
    }

    #[test]
    fn resolve_harnesses_auto_detects_by_marker() {
        // Case 1 — bare AGENTS.md, no .claude → codex (a lone codex surface).
        let bare = tempfile::tempdir().unwrap();
        fs::write(bare.path().join("AGENTS.md"), "x").unwrap();
        assert_eq!(
            resolve_harnesses(&[], bare.path()).unwrap(),
            vec![Harness::Codex]
        );

        // Case 2 — .claude/ + separate-inode AGENTS.md → both. The SL-063 fix: a
        // genuine codex surface beside Claude must be wired, not suppressed.
        let split = tempfile::tempdir().unwrap();
        fs::create_dir(split.path().join(".claude")).unwrap();
        fs::write(split.path().join("AGENTS.md"), "real codex surface").unwrap();
        assert_eq!(
            resolve_harnesses(&[], split.path()).unwrap(),
            vec![Harness::Claude, Harness::Codex]
        );

        // Case 3 — .claude/ + AGENTS.md symlink→CLAUDE.md → Claude only. This repo's
        // pattern: AGENTS.md is merely Claude's inode-alias, not a codex surface.
        let alias = tempfile::tempdir().unwrap();
        fs::create_dir(alias.path().join(".claude")).unwrap();
        fs::write(alias.path().join("CLAUDE.md"), "real").unwrap();
        std::os::unix::fs::symlink(
            alias.path().join("CLAUDE.md"),
            alias.path().join("AGENTS.md"),
        )
        .unwrap();
        assert_eq!(
            resolve_harnesses(&[], alias.path()).unwrap(),
            vec![Harness::Claude]
        );

        // Case 4 — .codex/ + .claude/ → both (the explicit codex marker path).
        let codex_marker = tempfile::tempdir().unwrap();
        fs::create_dir(codex_marker.path().join(".claude")).unwrap();
        fs::create_dir(codex_marker.path().join(".codex")).unwrap();
        assert_eq!(
            resolve_harnesses(&[], codex_marker.path()).unwrap(),
            vec![Harness::Claude, Harness::Codex]
        );

        // Case 5 — CLAUDE↔AGENTS symlink pair, NO .claude/ → codex. The adversarial
        // edge: alias-suppression is gated on Claude-detected, so a lone symlinked
        // pair (no .claude/ for Claude to claim) still wires the present AGENTS.md.
        let lone_pair = tempfile::tempdir().unwrap();
        fs::write(lone_pair.path().join("CLAUDE.md"), "real").unwrap();
        std::os::unix::fs::symlink(
            lone_pair.path().join("CLAUDE.md"),
            lone_pair.path().join("AGENTS.md"),
        )
        .unwrap();
        assert_eq!(
            resolve_harnesses(&[], lone_pair.path()).unwrap(),
            vec![Harness::Codex]
        );
    }

    #[test]
    fn resolve_harnesses_errors_when_none() {
        let dir = tempfile::tempdir().unwrap();
        let err = resolve_harnesses(&[], dir.path()).unwrap_err();
        assert!(err.to_string().contains("No --agent given"));
    }

    // --- T1: plan_boot_import (pure) ---

    #[test]
    fn plan_boot_import_creates_adds_and_is_idempotent() {
        // absent → create with just the ref.
        let (action, body) = plan_boot_import(None, REF);
        assert!(matches!(action, RefAction::Create));
        assert_eq!(body.unwrap(), format!("{REF}\n"));

        // present without the ref → prepend ahead of existing content.
        let (action, body) = plan_boot_import(Some("# Title\n\nbody\n"), REF);
        assert!(matches!(action, RefAction::Add));
        assert_eq!(body.unwrap(), format!("{REF}\n\n# Title\n\nbody\n"));

        // already present → no write.
        let (action, body) = plan_boot_import(Some("@.doctrine/state/boot.md\n\nrest\n"), REF);
        assert!(matches!(action, RefAction::Present));
        assert!(body.is_none());
    }

    // --- T1: ensure_boot_import (imperative) ---

    #[test]
    fn ensure_boot_import_creates_prepends_and_is_idempotent() {
        let dir = tempfile::tempdir().unwrap();
        let target = dir.path().join("CLAUDE.md");

        let out = ensure_boot_import(&[target.clone()], REF, false).unwrap();
        assert_eq!(out, vec![RefOutcome::Created(target.clone())]);
        assert_eq!(fs::read_to_string(&target).unwrap(), format!("{REF}\n"));

        // a second run is a no-op (Present), content unchanged. The file now
        // exists, so the outcome carries the canonicalized real path (on macOS
        // tempdir's /var resolves to /private/var) — compare against the same
        // resolution the code applies, not the raw tempdir path.
        let out = ensure_boot_import(&[target.clone()], REF, false).unwrap();
        assert_eq!(out, vec![RefOutcome::Present(resolve_target(&target))]);
        assert_eq!(fs::read_to_string(&target).unwrap(), format!("{REF}\n"));
    }

    #[test]
    fn ensure_boot_import_preserves_existing_content() {
        let dir = tempfile::tempdir().unwrap();
        let target = dir.path().join("AGENTS.md");
        fs::write(&target, "# Existing\n\nkeep me\n").unwrap();

        // file pre-exists, so the outcome carries the canonicalized real path
        // (macOS /var → /private/var); resolve the expected path to match.
        let out = ensure_boot_import(&[target.clone()], REF, false).unwrap();
        assert_eq!(out, vec![RefOutcome::Added(resolve_target(&target))]);
        let body = fs::read_to_string(&target).unwrap();
        assert_eq!(body, format!("{REF}\n\n# Existing\n\nkeep me\n"));
    }

    #[test]
    fn ensure_boot_import_dedups_same_inode_to_one_write() {
        let dir = tempfile::tempdir().unwrap();
        let agents = dir.path().join("AGENTS.md");
        let claude = dir.path().join("CLAUDE.md");
        fs::write(&agents, "real\n").unwrap();
        std::os::unix::fs::symlink(&agents, &claude).unwrap();

        // union order is [CLAUDE.md, AGENTS.md]; both canonicalize to one inode.
        let out = ensure_boot_import(&[claude.clone(), agents.clone()], REF, false).unwrap();
        assert_eq!(out.len(), 1, "same inode → exactly one outcome");

        let body = fs::read_to_string(&agents).unwrap();
        assert_eq!(body.matches(REF).count(), 1, "ref written exactly once");
        // the symlink survives (we wrote through to the real file, not over the link).
        assert!(
            fs::symlink_metadata(&claude)
                .unwrap()
                .file_type()
                .is_symlink()
        );
        assert_eq!(fs::read_to_string(&claude).unwrap(), body);
    }

    #[test]
    fn ensure_boot_import_dry_run_writes_nothing() {
        let dir = tempfile::tempdir().unwrap();
        let target = dir.path().join("CLAUDE.md");

        let out = ensure_boot_import(&[target.clone()], REF, true).unwrap();
        assert_eq!(out, vec![RefOutcome::Created(target.clone())]);
        assert!(!target.exists(), "dry-run must not write");
    }

    // --- T2: is_doctrine_boot_command (ownership, spaced paths) ---

    #[test]
    fn ownership_match_survives_spaces_and_rejects_foreign() {
        assert!(is_doctrine_boot_command("/usr/bin/doctrine boot"));
        assert!(is_doctrine_boot_command("doctrine boot"));
        // spaced exec path — the Charge VII case.
        assert!(is_doctrine_boot_command("/nix/store/a b/doctrine boot"));
        // foreign hooks never match.
        assert!(!is_doctrine_boot_command("/usr/bin/tool boot"));
        assert!(!is_doctrine_boot_command("/x/doctrine-helper run"));
        assert!(!is_doctrine_boot_command("/x/doctrine check"));
        assert!(!is_doctrine_boot_command("doctrine"));
    }

    // --- T2: plan_session_hook (pure merge matrix) ---

    #[test]
    fn plan_session_hook_wires_into_empty_or_missing() {
        let exec = Path::new("/abs/doctrine");
        for existing in [None, Some(""), Some("{}")] {
            let plan = plan_session_hook(existing, exec);
            assert!(matches!(plan.outcome, RefreshOutcome::Wired(_)));
            let json = plan.new_json.unwrap();
            assert_eq!(commands(&json), vec!["/abs/doctrine boot".to_string()]);
            assert!(json.contains(SESSION_MATCHER));
        }
    }

    #[test]
    fn plan_session_hook_noops_when_current() {
        let exec = Path::new("/abs/doctrine");
        let wired = plan_session_hook(None, exec).new_json.unwrap();
        let again = plan_session_hook(Some(&wired), exec);
        assert!(matches!(again.outcome, RefreshOutcome::None));
        assert!(again.new_json.is_none(), "current hook → no rewrite");
    }

    #[test]
    fn plan_session_hook_refreshes_on_path_change_preserving_foreign() {
        let old_exec = Path::new("/old/doctrine");
        let new_exec = Path::new("/new path/doctrine"); // spaced new path

        // start from a settings file carrying BOTH a foreign hook and our stale one.
        let seeded = serde_json::to_string_pretty(&serde_json::json!({
            "model": "opus",
            "hooks": {
                "SessionStart": [
                    { "matcher": "startup", "hooks": [ { "type": "command", "command": "/usr/bin/notify start" } ] },
                    { "matcher": SESSION_MATCHER, "hooks": [ { "type": "command", "command": boot_command(old_exec) } ] }
                ]
            }
        })).unwrap();

        let plan = plan_session_hook(Some(&seeded), new_exec);
        assert!(matches!(plan.outcome, RefreshOutcome::Refreshed(_)));
        let json = plan.new_json.unwrap();

        let cmds = commands(&json);
        assert!(
            cmds.contains(&"/usr/bin/notify start".to_string()),
            "foreign hook preserved"
        );
        assert!(
            cmds.contains(&"/new path/doctrine boot".to_string()),
            "command refreshed"
        );
        assert!(
            !cmds.contains(&"/old/doctrine boot".to_string()),
            "stale command gone"
        );
        assert!(json.contains("\"model\""), "unrelated key preserved");
    }

    #[test]
    fn plan_session_hook_fails_soft_on_malformed_json() {
        let plan = plan_session_hook(Some("{ not json"), Path::new("/abs/doctrine"));
        assert!(matches!(plan.outcome, RefreshOutcome::PrintedFallback));
        assert!(plan.new_json.is_none(), "malformed → never clobber");
    }

    // --- T2: install_refresh (imperative — dry_run + codex) ---

    #[test]
    fn install_refresh_writes_settings_and_respects_dry_run() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();
        let exec = Path::new("/abs/doctrine");
        let settings = root.join(SETTINGS_REL);

        // dry-run plans a wire but writes nothing.
        let out = install_refresh(&Harness::Claude, root, exec, true).unwrap();
        assert!(matches!(out.hook, RefreshOutcome::Wired(_)));
        assert!(matches!(out.baseref, BaseRefOutcome::Set));
        assert!(!settings.exists(), "dry-run must not write settings");

        // real run creates the file with the hook AND the baseRef key.
        let out = install_refresh(&Harness::Claude, root, exec, false).unwrap();
        assert!(matches!(out.hook, RefreshOutcome::Wired(_)));
        assert!(matches!(out.baseref, BaseRefOutcome::Set));
        let json = fs::read_to_string(&settings).unwrap();
        assert_eq!(commands(&json), vec!["/abs/doctrine boot".to_string()]);
        let parsed: Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["worktree"]["baseRef"], Value::String("head".into()));

        // codex is import-only — no hook, no file, no baseRef.
        let out = install_refresh(&Harness::Codex, root, exec, false).unwrap();
        assert!(matches!(out.hook, RefreshOutcome::None));
        assert!(matches!(out.baseref, BaseRefOutcome::NotApplicable));
    }

    // --- SL-064 PHASE-08 T4 / VT-1: worktree.baseRef="head" installer ---

    #[test]
    fn plan_baseref_writes_head_when_absent() {
        // Empty/absent settings ⇒ Set, with the nested key written.
        let plan = plan_baseref(None);
        assert!(matches!(plan.outcome, BaseRefOutcome::Set));
        let json = plan.new_json.expect("a write is planned");
        let parsed: Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["worktree"]["baseRef"], Value::String("head".into()));
    }

    #[test]
    fn plan_baseref_idempotent_when_already_head() {
        let existing = r#"{"worktree":{"baseRef":"head"}}"#;
        let plan = plan_baseref(Some(existing));
        assert!(matches!(plan.outcome, BaseRefOutcome::AlreadyHead));
        assert!(plan.new_json.is_none(), "no write when already head");
    }

    #[test]
    fn plan_baseref_conflict_reports_no_clobber() {
        // A deliberate non-head override is respected — reported, never clobbered.
        let existing = r#"{"worktree":{"baseRef":"trunk"}}"#;
        let plan = plan_baseref(Some(existing));
        match plan.outcome {
            BaseRefOutcome::Conflict(v) => assert_eq!(v, "trunk"),
            other => panic!("expected Conflict, got {other:?}"),
        }
        assert!(plan.new_json.is_none(), "conflict must not write");
    }

    #[test]
    fn plan_baseref_preserves_hooks_and_other_worktree_keys() {
        // A realistic settings file: boot+stamp hooks plus an unrelated worktree
        // key. The baseRef set must leave ALL of them intact.
        let existing = r#"{
          "hooks": {
            "SessionStart": [
              {"matcher":"startup|clear","hooks":[{"type":"command","command":"/abs/doctrine boot"}]}
            ],
            "SubagentStart": [
              {"matcher":"dispatch-worker","hooks":[{"type":"command","command":"/abs/doctrine worktree marker --stamp-subagent"}]}
            ]
          },
          "worktree": {"autoFetch": true}
        }"#;
        let plan = plan_baseref(Some(existing));
        assert!(matches!(plan.outcome, BaseRefOutcome::Set));
        let parsed: Value = serde_json::from_str(&plan.new_json.unwrap()).unwrap();
        // baseRef added...
        assert_eq!(parsed["worktree"]["baseRef"], Value::String("head".into()));
        // ...the sibling worktree key survives...
        assert_eq!(parsed["worktree"]["autoFetch"], Value::Bool(true));
        // ...and BOTH hook entries are intact.
        assert_eq!(
            parsed["hooks"]["SessionStart"][0]["hooks"][0]["command"],
            Value::String("/abs/doctrine boot".into())
        );
        assert_eq!(
            parsed["hooks"]["SubagentStart"][0]["hooks"][0]["command"],
            Value::String("/abs/doctrine worktree marker --stamp-subagent".into())
        );
    }

    #[test]
    fn plan_baseref_malformed_left_untouched() {
        let plan = plan_baseref(Some("{ not json"));
        assert!(matches!(plan.outcome, BaseRefOutcome::PrintedFallback));
        assert!(
            plan.new_json.is_none(),
            "malformed file must not be clobbered"
        );
    }

    // --- T6 (SL-018): the generalized seam carries a SEPARATE `memory sync` hook.

    #[test]
    fn sync_ownership_is_disjoint_from_boot() {
        // sync predicate recognises its own command (spaced exec included)...
        assert!(is_doctrine_sync_command("/usr/bin/doctrine memory sync"));
        assert!(is_doctrine_sync_command("doctrine memory sync"));
        assert!(is_doctrine_sync_command(
            "/nix/store/a b/doctrine memory sync"
        ));
        // ...and rejects foreign / boot commands.
        assert!(!is_doctrine_sync_command("/usr/bin/tool memory sync"));
        assert!(!is_doctrine_sync_command("/abs/doctrine boot"));
        assert!(!is_doctrine_sync_command("/abs/doctrine memory"));
        // the two predicates never cross: neither owns the other's command.
        assert!(!is_doctrine_boot_command("/abs/doctrine memory sync"));
        assert!(!is_doctrine_sync_command("/abs/doctrine boot"));
    }

    #[test]
    fn install_claude_hook_wires_boot_and_sync_as_two_entries() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();
        let exec = Path::new("/abs/doctrine");

        // wire boot, then sync — two independent SessionStart entries.
        install_claude_hook(root, &HookSpec::boot(exec), false).unwrap();
        let out = install_claude_hook(root, &HookSpec::sync(exec), false).unwrap();
        assert!(matches!(out, RefreshOutcome::Wired(_)));

        let json = fs::read_to_string(root.join(SETTINGS_REL)).unwrap();
        let cmds = commands(&json);
        assert!(
            cmds.contains(&"/abs/doctrine boot".to_string()),
            "boot kept"
        );
        assert!(
            cmds.contains(&"/abs/doctrine memory sync".to_string()),
            "sync wired"
        );

        // re-running sync is a no-op; the boot entry is untouched.
        let again = install_claude_hook(root, &HookSpec::sync(exec), false).unwrap();
        assert!(matches!(again, RefreshOutcome::None));
        let json = fs::read_to_string(root.join(SETTINGS_REL)).unwrap();
        assert_eq!(
            commands(&json),
            vec![
                "/abs/doctrine boot".to_string(),
                "/abs/doctrine memory sync".to_string(),
            ],
            "two entries, neither duplicated nor clobbered"
        );
    }

    #[test]
    fn install_claude_hook_sync_respects_dry_run() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();
        let out =
            install_claude_hook(root, &HookSpec::sync(Path::new("/abs/doctrine")), true).unwrap();
        assert!(matches!(out, RefreshOutcome::Wired(_)));
        assert!(
            !root.join(SETTINGS_REL).exists(),
            "dry-run must not write settings"
        );
    }

    /// Entries under an arbitrary `hooks.<event>` key (`None` if absent).
    fn event_entries(json: &str, event: &str) -> Option<Vec<Value>> {
        let value: Value = serde_json::from_str(json).expect("valid JSON");
        value
            .get("hooks")
            .and_then(|h| h.get(event))
            .and_then(Value::as_array)
            .cloned()
    }

    // VT-3: the stamp hook merges into a settings file that already carries
    // unrelated SessionStart hooks — the SessionStart entries are untouched and a
    // NEW SubagentStart array with the matcher-scoped entry is appended.
    // Reinstall is idempotent (no dup).
    #[test]
    fn install_claude_stamp_hook_appends_subagentstart_leaves_sessionstart_intact() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();
        let exec = Path::new("/abs/doctrine");

        // Pre-existing SessionStart hooks (boot + a foreign hook).
        install_claude_hook(root, &HookSpec::boot(exec), false).unwrap();
        let settings_path = root.join(SETTINGS_REL);
        let seeded = fs::read_to_string(&settings_path).unwrap();
        let mut value: Value = serde_json::from_str(&seeded).unwrap();
        hook_array_mut(&mut value, "SessionStart")
            .unwrap()
            .push(serde_json::json!({
                "matcher": "startup",
                "hooks": [ { "type": "command", "command": "/usr/bin/foreign hook" } ],
            }));
        fs::write(
            &settings_path,
            serde_json::to_string_pretty(&value).unwrap(),
        )
        .unwrap();

        // Wire the stamp hook.
        let out = install_claude_hook(root, &HookSpec::stamp_subagent(exec), false).unwrap();
        assert!(matches!(out, RefreshOutcome::Wired(_)), "stamp wired");

        let json = fs::read_to_string(&settings_path).unwrap();

        // SessionStart entries untouched: boot + the foreign hook still present.
        let session = event_entries(&json, "SessionStart").expect("SessionStart kept");
        let session_cmds: Vec<String> = session
            .iter()
            .filter_map(|e| e.get("hooks").and_then(Value::as_array))
            .flatten()
            .filter_map(|h| h.get("command").and_then(Value::as_str))
            .map(str::to_string)
            .collect();
        assert_eq!(
            session_cmds,
            vec![
                "/abs/doctrine boot".to_string(),
                "/usr/bin/foreign hook".to_string(),
            ],
            "SessionStart untouched"
        );

        // A new SubagentStart array carries the matcher-scoped stamp entry.
        let sub = event_entries(&json, "SubagentStart").expect("SubagentStart created");
        assert_eq!(sub.len(), 1, "one SubagentStart entry");
        assert_eq!(
            sub[0].get("matcher").and_then(Value::as_str),
            Some(crate::worktree::DISPATCH_WORKER_AGENT_TYPE),
            "matcher-scoped to the dispatch-worker agent type"
        );
        let sub_cmd = sub[0]
            .get("hooks")
            .and_then(Value::as_array)
            .and_then(|a| a.first())
            .and_then(|h| h.get("command"))
            .and_then(Value::as_str);
        assert_eq!(
            sub_cmd,
            Some("/abs/doctrine worktree marker --stamp-subagent")
        );

        // Reinstall is idempotent — no duplicate SubagentStart entry.
        let again = install_claude_hook(root, &HookSpec::stamp_subagent(exec), false).unwrap();
        assert!(matches!(again, RefreshOutcome::None), "reinstall no-op");
        let json = fs::read_to_string(&settings_path).unwrap();
        assert_eq!(
            event_entries(&json, "SubagentStart").unwrap().len(),
            1,
            "no duplicate on reinstall"
        );
    }

    // Drift pin: the stamp matcher is the worktree dispatch-worker agent type,
    // not a re-spelled literal — if the const moves, this fails loudly.
    #[test]
    fn stamp_subagent_matcher_tracks_worktree_const() {
        let spec = HookSpec::stamp_subagent(Path::new("/abs/doctrine"));
        assert_eq!(spec.matcher, crate::worktree::DISPATCH_WORKER_AGENT_TYPE);
        assert_eq!(spec.event, "SubagentStart");
    }

    // The three ownership predicates are mutually disjoint — none owns another's
    // command, so the three entries never clobber one another.
    #[test]
    fn stamp_ownership_is_disjoint_from_boot_and_sync() {
        assert!(is_doctrine_stamp_command(
            "/abs/doctrine worktree marker --stamp-subagent"
        ));
        assert!(!is_doctrine_stamp_command("/abs/doctrine boot"));
        assert!(!is_doctrine_stamp_command("/abs/doctrine memory sync"));
        assert!(!is_doctrine_boot_command(
            "/abs/doctrine worktree marker --stamp-subagent"
        ));
        assert!(!is_doctrine_sync_command(
            "/abs/doctrine worktree marker --stamp-subagent"
        ));
        // a foreign stamp-shaped command is not ours.
        assert!(!is_doctrine_stamp_command(
            "/usr/bin/tool worktree marker --stamp-subagent"
        ));
    }

    // --- T4: `wire` orchestration (integration, tempdir). Exec is injected with
    //     a `doctrine` file name — the real verb resolves `current_exe()`, but the
    //     test binary is named `doctrine-<hash>`, which the ownership match would
    //     (correctly) not recognise as ours (mirrors PHASE-01 testing `regenerate`,
    //     not `run`). ---

    const FAKE_EXEC: &str = "/fake/doctrine";

    #[test]
    fn wire_adds_import_and_hook_then_is_idempotent() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();
        let exec = Path::new(FAKE_EXEC);

        wire(root, exec, &[Harness::Claude], false).unwrap();

        let claude_md = fs::read_to_string(root.join("CLAUDE.md")).unwrap();
        assert_eq!(claude_md.matches(REF).count(), 1, "import ref wired once");
        let settings = fs::read_to_string(root.join(SETTINGS_REL)).unwrap();
        assert_eq!(
            commands(&settings),
            vec![format!("{FAKE_EXEC} boot")],
            "hook wired once"
        );

        // re-run: import Present, hook current (None) — no duplication.
        wire(root, exec, &[Harness::Claude], false).unwrap();
        let claude_md = fs::read_to_string(root.join("CLAUDE.md")).unwrap();
        assert_eq!(
            claude_md.matches(REF).count(),
            1,
            "re-run does not duplicate ref"
        );
        let settings = fs::read_to_string(root.join(SETTINGS_REL)).unwrap();
        assert_eq!(
            commands(&settings).len(),
            1,
            "re-run does not duplicate hook"
        );
    }

    #[test]
    fn wire_dry_run_mutates_nothing() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();

        wire(root, Path::new(FAKE_EXEC), &[Harness::Claude], true).unwrap();
        assert!(!root.join("CLAUDE.md").exists(), "dry-run wrote no import");
        assert!(
            !root.join(SETTINGS_REL).exists(),
            "dry-run wrote no settings"
        );
    }

    #[test]
    fn wire_isolates_one_harness_failure() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();

        // force Claude's refresh to fail: a directory squatting the settings path
        // makes write_atomic's rename fail.
        fs::create_dir_all(root.join(SETTINGS_REL)).unwrap();

        // both harnesses; Claude refresh errs, codex import must still be wired and
        // the verb must not abort (A9).
        wire(
            root,
            Path::new(FAKE_EXEC),
            &[Harness::Claude, Harness::Codex],
            false,
        )
        .unwrap();

        let agents = fs::read_to_string(root.join("AGENTS.md")).unwrap();
        assert_eq!(
            agents.matches(REF).count(),
            1,
            "codex import survived the claude failure"
        );
    }
}
