shepherd-cli 6.5.6

The canonical shepherd command-line interface over the per-project registry, run artifacts, and sprint pipeline.
use std::{
    collections::{BTreeMap, BTreeSet},
    fs,
    path::{Component, Path, PathBuf},
};

use serde::Deserialize;
use serde_json::Value;

use crate::interface::CliError;

const HARNESSES: [&str; 3] = ["claude", "codex", "pi"];
const EVENTS: [&str; 8] = [
    "identity",
    "lifecycle",
    "dispatch",
    "guard",
    "report",
    "compaction",
    "failure",
    "host-limitations",
];
const ARTIFACTS: [&str; 6] = [
    ".shepherd/runs/v646/harness-parity.md",
    ".shepherd/runs/v656/lanes/harness-semantics/evidence/active-adapters.json",
    ".shepherd/runs/v656/lanes/harness-semantics/evidence/reports/claude-engineer.md",
    ".shepherd/runs/v656/lanes/harness-semantics/evidence/reports/codex-engineer.md",
    ".shepherd/runs/v656/lanes/harness-semantics/evidence/reports/pi-engineer.md",
    "hooks/tests/test_compaction_run_scope.sh",
];

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Matrix {
    schema: String,
    contract: String,
    harnesses: Vec<String>,
    events: Vec<Event>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Event {
    event: String,
    cells: BTreeMap<String, Cell>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Cell {
    status: String,
    native_input: String,
    native_result: String,
    adapter_translation: String,
    artifact_path: String,
    negative_control: String,
    #[serde(default)]
    limitations: Vec<Limitation>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Limitation {
    reason: String,
    citation: String,
}

pub(crate) fn validate(spec_path: &Path, root: &Path) -> Result<(), CliError> {
    let matrix: Matrix = serde_json::from_slice(&read_regular(spec_path)?)
        .map_err(|e| error(format!("invalid matrix JSON: {e}")))?;
    if matrix.schema != "shepherd.harness-semantics-matrix/1" {
        return Err(error("unsupported matrix schema"));
    }
    if matrix.contract != "fl03:shepherd@6.5.6" {
        return Err(error(format!(
            "matrix contract must be fl03:shepherd@6.5.6, got {}",
            matrix.contract
        )));
    }
    if matrix.harnesses != HARNESSES {
        return Err(error("harness axis must be exactly claude, codex, pi"));
    }
    if matrix.events.is_empty() {
        return Err(error("zero events are not a semantic matrix"));
    }
    let mut seen = BTreeSet::new();
    for event in &matrix.events {
        required(&event.event, "event")?;
        if !seen.insert(event.event.as_str()) {
            return Err(error(format!("duplicate event: {}", event.event)));
        }
        let observed = event
            .cells
            .keys()
            .map(String::as_str)
            .collect::<BTreeSet<_>>();
        let expected = HARNESSES.into_iter().collect::<BTreeSet<_>>();
        if let Some(missing) = expected.difference(&observed).next() {
            return Err(error(format!(
                "{}: missing harness cell: {missing}",
                event.event
            )));
        }
        if let Some(extra) = observed.difference(&expected).next() {
            return Err(error(format!(
                "{}: unexpected harness cell: {extra}",
                event.event
            )));
        }
        for harness in HARNESSES {
            validate_cell(root, &event.event, harness, &event.cells[harness])?;
        }
    }
    let expected = EVENTS.into_iter().collect::<BTreeSet<_>>();
    if seen != expected {
        return Err(error("event axis mismatch"));
    }
    validate_status_contract(root, &matrix)?;
    Ok(())
}

fn validate_cell(root: &Path, event: &str, harness: &str, cell: &Cell) -> Result<(), CliError> {
    if !matches!(
        cell.status.as_str(),
        "supported" | "limited" | "unsupported"
    ) {
        return Err(error(format!(
            "{event}/{harness}: invalid status {}",
            cell.status
        )));
    }
    for (name, value) in [
        ("native_input", &cell.native_input),
        ("native_result", &cell.native_result),
        ("adapter_translation", &cell.adapter_translation),
        ("artifact_path", &cell.artifact_path),
        ("negative_control", &cell.negative_control),
    ] {
        required(value, &format!("{event}/{harness}.{name}"))?;
    }
    for (field, value) in [
        ("native_input", &cell.native_input),
        ("native_result", &cell.native_result),
        ("negative_control", &cell.negative_control),
    ] {
        let expected = format!(
            ".shepherd/runs/v656/lanes/harness-semantics/evidence/active-adapters.json#matrix_evidence.{event}.{harness}.{field}"
        );
        if value != &expected {
            return Err(error(format!(
                "{event}/{harness}.{field} must reference owned raw evidence: {expected}"
            )));
        }
    }
    if cell.status != "supported" && cell.limitations.is_empty() {
        return Err(error(format!(
            "{event}/{harness}: {} cell requires limitations",
            cell.status
        )));
    }
    for (index, limitation) in cell.limitations.iter().enumerate() {
        required(
            &limitation.reason,
            &format!("{event}/{harness}.limitations[{index}].reason"),
        )?;
        required(
            &limitation.citation,
            &format!("{event}/{harness}.limitations[{index}].citation"),
        )?;
        require_source(
            root,
            &limitation.citation,
            &format!("{event}/{harness} limitation citation"),
        )?;
    }
    require_source(
        root,
        &cell.adapter_translation,
        &format!("{event}/{harness}.adapter_translation"),
    )?;
    require_source(
        root,
        &cell.artifact_path,
        &format!("{event}/{harness}.artifact_path"),
    )?;
    if !ARTIFACTS.contains(&cell.artifact_path.as_str()) {
        return Err(error(format!(
            "{event}/{harness}.artifact_path is not an owned evidence artifact: {}",
            cell.artifact_path
        )));
    }
    validate_fragment(
        root,
        &cell.native_input,
        &format!("{event}/{harness}.native_input"),
    )?;
    validate_fragment(
        root,
        &cell.native_result,
        &format!("{event}/{harness}.native_result"),
    )?;
    validate_fragment(
        root,
        &cell.negative_control,
        &format!("{event}/{harness}.negative_control"),
    )?;
    Ok(())
}

fn validate_status_contract(root: &Path, matrix: &Matrix) -> Result<(), CliError> {
    let manifest: Value = serde_json::from_slice(&read_regular(
        &root.join("plugins/shepherd/codex/hooks/hooks.json"),
    )?)
    .map_err(|e| error(format!("invalid Codex hooks manifest: {e}")))?;
    let hooks = manifest
        .get("hooks")
        .and_then(Value::as_object)
        .ok_or_else(|| error("Codex hooks manifest has no hook inventory"))?;
    let trusted_child_correlation =
        hooks.contains_key("SubagentStart") && hooks.contains_key("SubagentStop");

    for event in &matrix.events {
        for harness in HARNESSES {
            let expected = if event.event == "host-limitations" {
                "limited"
            } else if event.event == "compaction" {
                if harness == "claude" {
                    "supported"
                } else {
                    "unsupported"
                }
            } else if harness == "codex"
                && matches!(event.event.as_str(), "lifecycle" | "dispatch" | "report")
                && !trusted_child_correlation
            {
                "limited"
            } else {
                "supported"
            };
            let observed = event.cells[harness].status.as_str();
            if observed != expected {
                return Err(error(format!(
                    "{}:{harness} status must be {expected}, got {observed}",
                    event.event
                )));
            }
        }
    }
    Ok(())
}

fn validate_fragment(root: &Path, reference: &str, label: &str) -> Result<(), CliError> {
    let Some((source, fragment)) = reference.split_once('#') else {
        return Err(error(format!(
            "{label} must reference owned raw evidence with a JSON fragment: {reference}"
        )));
    };
    if !source.ends_with(".json") {
        return Err(error(format!(
            "{label} fragment must reference JSON: {reference}"
        )));
    }
    let mut current: Value = serde_json::from_slice(&read_regular(&source_path(root, source)?)?)
        .map_err(|e| error(format!("{label} contains invalid JSON: {e}")))?;
    for part in fragment.split('.') {
        if part.is_empty() {
            return Err(error(format!(
                "{label} fragment does not resolve: {reference}"
            )));
        }
        current = current
            .get(part)
            .cloned()
            .ok_or_else(|| error(format!("{label} fragment does not resolve: {reference}")))?;
    }
    if current.is_null() || current.as_str() == Some("") {
        return Err(error(format!(
            "{label} fragment resolves to empty evidence: {reference}"
        )));
    }
    Ok(())
}

fn require_source(root: &Path, reference: &str, label: &str) -> Result<(), CliError> {
    let path = source_path(root, reference.split_once('#').map_or(reference, |v| v.0))?;
    if !fs::symlink_metadata(&path).is_ok_and(|metadata| metadata.file_type().is_file()) {
        return Err(error(format!("{label} does not exist: {reference}")));
    }
    Ok(())
}
fn source_path(root: &Path, value: &str) -> Result<PathBuf, CliError> {
    let path = Path::new(value);
    if path.is_absolute() || !path.components().all(|c| matches!(c, Component::Normal(_))) {
        return Err(error(format!(
            "source path escapes repository root: {value}"
        )));
    }
    Ok(root.join(path))
}
fn read_regular(path: &Path) -> Result<Vec<u8>, CliError> {
    let metadata = fs::symlink_metadata(path)
        .map_err(|e| error(format!("cannot inspect {}: {e}", path.display())))?;
    if !metadata.file_type().is_file() {
        return Err(error(format!("not a regular file: {}", path.display())));
    }
    fs::read(path).map_err(|e| error(format!("cannot read {}: {e}", path.display())))
}
fn required(value: &str, label: &str) -> Result<(), CliError> {
    if value.trim().is_empty() {
        Err(error(format!("{label} must be a non-empty string")))
    } else {
        Ok(())
    }
}
fn error(message: impl Into<String>) -> CliError {
    CliError::message(format!("harness matrix: {}", message.into()))
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn source_paths_are_contained() {
        assert!(source_path(Path::new("/tmp/root"), "packages/a.json").is_ok());
        assert!(source_path(Path::new("/tmp/root"), "../a.json").is_err());
    }
    #[test]
    fn required_fields_reject_whitespace() {
        assert!(required(" ", "field").is_err());
        assert!(required("value", "field").is_ok());
    }
}