shepherd-core 6.6.0

The harness-agnostic shepherd engine: domain types, configuration schema, and run state. Knows nothing about any CLI, harness, or process.
use shepherd_core::{Harness, dispatch::*};

fn child_identity(event: &str) -> NormalizedIdentity {
    RawIdentity::new(
        Harness::Pi,
        event,
        "session-child",
        Some("agent-child"),
        Some("shepherd:coder"),
        None,
        None,
        None,
    )
    .normalize()
    .expect("fixture identity")
}

fn binding() -> DispatchBinding {
    DispatchBinding::new(
        Some(RunId::new("v645").expect("run")),
        Some(Role::Coder),
        Some(LaneId::new("l1-engine").expect("lane")),
        Some(AgentId::new("parent").expect("parent")),
        vec!["crates/core/src/dispatch/**".into()],
        None,
        [
            "read",
            "search",
            "shell",
            "skill-load",
            "write",
            "subagent-provider",
        ],
        "native",
        "1",
        None,
        60_000,
    )
    .expect("binding")
}

#[test]
fn child_start_and_resume_never_become_native_dispatch_requests() {
    let start =
        plan_lifecycle(&child_identity("SubagentStart"), Some(&binding())).expect("start plan");
    assert!(matches!(
        start,
        DispatchPlan::Blocked(DispatchError::BrokerRequired)
    ));

    let mut resume_binding = binding();
    resume_binding.source_agent_id = Some(AgentId::new("source").expect("source"));
    let resume = plan_lifecycle(&child_identity("SubagentResume"), Some(&resume_binding))
        .expect("resume plan");
    assert!(matches!(
        resume,
        DispatchPlan::Blocked(DispatchError::BrokerRequired)
    ));
}

#[test]
fn terminal_cleanup_response_is_strict_and_not_replayable() {
    let response = LaunchCleanupResponse {
        schema: LAUNCH_CLEANUP_SCHEMA.into(),
        launch_id_hash: [4; 32],
        state: PendingLaunchState::Expired,
    };
    response.validate().expect("terminal cleanup");
    let mut replay = response.clone();
    replay.state = PendingLaunchState::Active;
    assert!(replay.validate().is_err());
    let mut malformed = response;
    malformed.schema = "wrong".into();
    assert!(malformed.validate().is_err());
}

#[test]
fn digest_comparison_is_fixed_width_and_constant_time_api() {
    let mut other = [0_u8; 32];
    other[31] = 1;
    assert!(constant_time_digest_eq(&[7; 32], &[7; 32]));
    assert!(!constant_time_digest_eq(&[7; 32], &other));
}

#[test]
fn pending_launch_state_has_one_claim_and_no_direct_active_edge() {
    assert!(PendingLaunchState::Pending.can_claim());
    assert!(!PendingLaunchState::ClaimedUnspawned.can_claim());
    assert!(!PendingLaunchState::Active.can_claim());
    assert!(PendingLaunchState::ClaimedUnspawned.can_activate());
    assert!(!PendingLaunchState::Pending.can_activate());
    assert!(!PendingLaunchState::Active.can_activate());
    assert!(PendingLaunchState::LaunchFailed.is_terminal());
    assert!(PendingLaunchState::Canceled.is_terminal());
    assert!(PendingLaunchState::Expired.is_terminal());
}

#[test]
fn scope_validation_rejects_unicode_and_alias_forms_and_keeps_case_distinct() {
    let scopes = ["crates/core/**".to_owned()];
    for path in [
        "crates/core/src/é.rs",
        "crates/core/./src/lib.rs",
        "crates/core/src/../lib.rs",
        "crates/core/src/lib~1.rs",
    ] {
        assert!(
            path_in_write_scope(path, &scopes).is_err(),
            "unsafe path accepted: {path}"
        );
    }
    assert!(path_in_write_scope("crates/core/src/lib.rs", &scopes).expect("safe path"));
    // Repository names preserve case, so a case variant is a DIFFERENT path
    // rather than a malformed one. crates/core/tests/dispatch_scope_paths.rs owns
    // that contract in full; these keep this suite consistent with it.
    assert!(!path_in_write_scope("Crates/core/src/lib.rs", &scopes).expect("case stays distinct"));
    assert!(
        !path_in_write_scope("crates/core/src/lib.rs", &["Crates/core/**".into()])
            .expect("case stays distinct")
    );
}