lifeloop-cli 0.2.0

Provider-neutral lifecycle abstraction and normalizer for AI harnesses
Documentation
//! Issue #28 product-pilot evidence: a non-CCD Fixity client consumes
//! the real `lifeloop event invoke` subprocess path and observes a
//! delivered payload body.

use std::io::Write;
use std::process::{Command, Stdio};

use lifeloop::{
    AcceptablePlacement, CallbackRequest, DispatchEnvelope, FrameContext, IntegrationMode,
    LifecycleEventKind, PayloadEnvelope, PayloadRef, PlacementClass, RequirementLevel,
    SCHEMA_VERSION,
};

fn lifeloop_bin() -> std::path::PathBuf {
    std::path::PathBuf::from(env!("CARGO_BIN_EXE_lifeloop"))
}

fn request() -> CallbackRequest {
    let entry = lifeloop::lookup_manifest("codex").unwrap();
    CallbackRequest {
        schema_version: SCHEMA_VERSION.to_string(),
        event: LifecycleEventKind::FrameOpening,
        event_id: "evt-fixity-cli-1".into(),
        adapter_id: "codex".into(),
        adapter_version: entry.manifest.adapter_version,
        integration_mode: IntegrationMode::NativeHook,
        invocation_id: "inv-fixity-cli-1".into(),
        harness_session_id: Some("sess-fixity-cli-1".into()),
        harness_run_id: Some("run-fixity-cli-1".into()),
        harness_task_id: None,
        frame_context: Some(FrameContext::top_level("frm-fixity-cli-1")),
        capability_snapshot_ref: None,
        payload_refs: vec![PayloadRef {
            payload_id: "pay-fixity-cli-1".into(),
            payload_kind: "fixity.experience".into(),
            content_digest: Some("sha256:fixity-cli".into()),
            byte_size: Some(124),
        }],
        sequence: Some(1),
        idempotency_key: Some("idem-fixity-cli-1".into()),
        metadata: serde_json::Map::new(),
    }
}

fn payload() -> PayloadEnvelope {
    let body = "Error: TS2345 in API handler. What worked: Number() at call site. Avoid changing the exported function signature.";
    PayloadEnvelope {
        schema_version: SCHEMA_VERSION.to_string(),
        payload_id: "pay-fixity-cli-1".into(),
        client_id: "fixity-pilot".into(),
        payload_kind: "fixity.experience".into(),
        format: "client-defined".into(),
        content_encoding: "utf8".into(),
        body: Some(body.into()),
        body_ref: None,
        byte_size: body.len() as u64,
        content_digest: Some("sha256:fixity-cli".into()),
        acceptable_placements: vec![AcceptablePlacement {
            placement: PlacementClass::DeveloperEquivalentFrame,
            requirement: RequirementLevel::Preferred,
        }],
        idempotency_key: None,
        expires_at_epoch_s: None,
        redaction: None,
        metadata: serde_json::Map::new(),
    }
}

#[test]
fn event_invoke_reaches_fixity_pilot_and_records_payload_observation() {
    let dir = tempfile::tempdir().unwrap();
    let artifact_path = dir.path().join("fixity-cli-artifact.jsonl");
    let dispatch = serde_json::to_vec(&DispatchEnvelope::new(request(), vec![payload()])).unwrap();

    let mut args = vec![
        "event".to_string(),
        "invoke".to_string(),
        "--timeout-ms".to_string(),
        "30000".to_string(),
        "--client-id".to_string(),
        "fixity-pilot".to_string(),
        "--receipt-id".to_string(),
        "rcpt-fixity-cli-1".to_string(),
        "--at-epoch-s".to_string(),
        "1700000028".to_string(),
    ];

    let (client_cmd, client_args) = fixity_client_command(&artifact_path);
    args.push("--client-cmd".into());
    args.push(client_cmd);
    for arg in client_args {
        args.push("--client-arg".into());
        args.push(arg);
    }

    let mut child = Command::new(lifeloop_bin())
        .args(args)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn lifeloop");
    child.stdin.as_mut().unwrap().write_all(&dispatch).unwrap();
    let out = child.wait_with_output().unwrap();
    assert!(
        out.status.success(),
        "stderr={}",
        String::from_utf8_lossy(&out.stderr)
    );

    let receipt: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
    assert_eq!(receipt["receipt_id"], "rcpt-fixity-cli-1");
    assert_eq!(receipt["client_id"], "fixity-pilot");
    assert_eq!(receipt["status"], "delivered");
    assert_eq!(
        receipt["payload_receipts"][0]["payload_id"],
        "pay-fixity-cli-1"
    );
    assert_eq!(
        receipt["payload_receipts"][0]["payload_kind"],
        "fixity.experience"
    );
    assert_eq!(
        receipt["payload_receipts"][0]["content_digest"],
        "sha256:fixity-cli"
    );
    assert_eq!(receipt["payload_receipts"][0]["status"], "delivered");

    let artifact_text = std::fs::read_to_string(&artifact_path).unwrap();
    let artifact: serde_json::Value = serde_json::from_str(artifact_text.trim()).unwrap();
    assert_eq!(artifact["schema"], "lifeloop-fixity-pilot.v0.1");
    assert_eq!(artifact["event"], "frame.opening");
    assert_eq!(
        artifact["payload_observations"][0]["delivery"],
        "inline_body"
    );
    assert!(
        artifact["payload_observations"][0]["body_excerpt"]
            .as_str()
            .unwrap()
            .contains("TS2345")
    );
}

fn fixity_client_command(artifact_path: &std::path::Path) -> (String, Vec<String>) {
    if let Ok(path) = std::env::var("CARGO_BIN_EXE_lifeloop-fixity-pilot") {
        return (
            path,
            vec![
                "--artifact-path".into(),
                artifact_path.to_string_lossy().into_owned(),
            ],
        );
    }

    let candidate = lifeloop_bin()
        .parent()
        .expect("lifeloop bin has parent")
        .join("lifeloop-fixity-pilot");
    if candidate.exists() {
        return (
            candidate.to_string_lossy().into_owned(),
            vec![
                "--artifact-path".into(),
                artifact_path.to_string_lossy().into_owned(),
            ],
        );
    }

    (
        "cargo".into(),
        vec![
            "run".into(),
            "--quiet".into(),
            "-p".into(),
            "fixity-pilot".into(),
            "--bin".into(),
            "lifeloop-fixity-pilot".into(),
            "--".into(),
            "--artifact-path".into(),
            artifact_path.to_string_lossy().into_owned(),
        ],
    )
}