omk 0.5.0

A Rust runtime for Kimi CLI. Turns prompts into proof-backed engineering runs with gates, worktrees, and replay.
Documentation
use assert_cmd::Command;
use predicates::prelude::*;
use serde_json::Value;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command as StdCommand;

fn isolated_env() -> (tempfile::TempDir, Vec<(&'static str, PathBuf)>) {
    omk::test_helpers::isolated_xdg_env()
}

fn omk_cmd(envs: &[(&'static str, PathBuf)]) -> Command {
    let mut cmd = Command::cargo_bin("omk").unwrap();
    for (key, value) in envs {
        cmd.env(key, value);
    }
    cmd
}

fn mock_kimi_path() -> PathBuf {
    assert_cmd::cargo::cargo_bin("mock-kimi")
}

fn xdg_state(envs: &[(&'static str, PathBuf)]) -> PathBuf {
    envs.iter()
        .find_map(|(key, value)| (*key == "XDG_STATE_HOME").then(|| value.clone()))
        .expect("missing XDG_STATE_HOME")
}

fn goal_dirs(envs: &[(&'static str, PathBuf)]) -> Vec<PathBuf> {
    let goals_dir = xdg_state(envs).join("omk").join("goals");
    let mut dirs: Vec<_> = fs::read_dir(goals_dir)
        .expect("missing goals dir")
        .map(|entry| entry.expect("failed to read goal entry").path())
        .filter(|path| path.is_dir())
        .collect();
    dirs.sort();
    dirs
}

fn write_gate_config(project_dir: &Path, gate_name: &str, script: &str) {
    let omk_dir = project_dir.join(".omk");
    fs::create_dir_all(&omk_dir).expect("failed to create .omk dir");
    fs::write(
        omk_dir.join("gates.toml"),
        format!(
            r#"
[[gates]]
name = "{gate_name}"
command = "/bin/sh"
args = ["-c", "{script}"]
required = true
"#
        ),
    )
    .expect("failed to write gates.toml");
}

fn init_git(project_dir: &Path) {
    git(project_dir, &["init"]);
    git(project_dir, &["config", "user.email", "omk@example.com"]);
    git(project_dir, &["config", "user.name", "OMK Test"]);
    git(project_dir, &["add", "."]);
    git(project_dir, &["commit", "-m", "baseline"]);
}

fn git(project_dir: &Path, args: &[&str]) {
    let output = StdCommand::new("git")
        .arg("-C")
        .arg(project_dir)
        .args(args)
        .output()
        .expect("failed to run git");
    assert!(
        output.status.success(),
        "git {:?} failed: stdout={} stderr={}",
        args,
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
}

fn proof_json(envs: &[(&'static str, PathBuf)], project_dir: &Path) -> Value {
    let output = omk_cmd(envs)
        .current_dir(project_dir)
        .args(["goal", "proof", "latest", "--json"])
        .output()
        .expect("omk goal proof failed");
    assert!(output.status.success());
    serde_json::from_slice(&output.stdout).expect("proof output should be JSON")
}

fn read_jsonl(path: &Path) -> Vec<Value> {
    fs::read_to_string(path)
        .expect("missing jsonl file")
        .lines()
        .filter(|line| !line.trim().is_empty())
        .map(|line| serde_json::from_str(line).expect("jsonl line should parse"))
        .collect()
}

#[test]
fn run_until_ready_drives_controller_loop_and_stops_before_manual_acceptance() {
    let (_tmp, envs) = isolated_env();
    let project = tempfile::tempdir().expect("temp project");
    write_gate_config(
        project.path(),
        "acceptance-smoke-demo-performance",
        "echo controller-loop-ok",
    );
    init_git(project.path());

    let mut run = omk_cmd(&envs);
    run.env("MOCK_KIMI", mock_kimi_path())
        .env("MOCK_KIMI_WRITE_FILE", "agent-output.txt")
        .env("OMK_WIRE_WORKER_POLL_INTERVAL_MS", "50")
        .current_dir(project.path())
        .args([
            "goal",
            "run",
            "Implement acceptance smoke demo proof for this CLI",
            "--until-ready",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("Goal run completed"))
        .stdout(predicate::str::contains("Narrative:"))
        .stdout(predicate::str::contains("plan"))
        .stdout(predicate::str::contains("verify"))
        .stdout(predicate::str::contains("execute"))
        .stdout(predicate::str::contains("review"))
        .stdout(predicate::str::contains("manual integration acceptance"))
        .stdout(predicate::str::contains("GitHub mutation: disabled"))
        .stdout(predicate::str::contains("omk goal verify latest").not())
        .stdout(predicate::str::contains("omk goal execute latest").not())
        .stdout(predicate::str::contains("omk goal review latest").not());

    assert!(project.path().join("agent-output.txt").exists());
    let dirs = goal_dirs(&envs);
    assert_eq!(dirs.len(), 1);
    let goal_dir = &dirs[0];
    assert!(goal_dir
        .join("artifacts/policy/manual-integration-blocker.json")
        .exists());
    assert!(!goal_dir
        .join("artifacts/integration/integrator-accept.json")
        .exists());

    let task_graph: Value = serde_json::from_str(
        &fs::read_to_string(goal_dir.join("task-graph.json")).expect("missing task graph"),
    )
    .expect("task graph should be JSON");
    for task_id in [
        "goal-local-verify",
        "goal-agent-execute",
        "goal-review",
        "goal-security-review",
    ] {
        let task = task_graph["tasks"]
            .as_array()
            .unwrap()
            .iter()
            .find(|task| task["id"] == task_id)
            .unwrap_or_else(|| panic!("missing {task_id}"));
        assert_eq!(task["status"], "done", "{task_id} should be done");
    }

    let proof = proof_json(&envs, project.path());
    assert_eq!(proof["status"], "not_ready");
    assert!(proof["changed_files"]
        .as_array()
        .unwrap()
        .iter()
        .any(|file| file.as_str() == Some("agent-output.txt")));
    assert!(proof["known_gaps"].as_array().unwrap().iter().any(|gap| gap
        .as_str()
        .is_some_and(|gap| gap.contains("manual integration acceptance"))));
    assert!(proof["human_decisions_required"]
        .as_array()
        .unwrap()
        .iter()
        .any(|decision| decision
            .as_str()
            .is_some_and(|decision| decision.contains("manual integration acceptance"))));
}

#[test]
fn run_until_ready_failed_gate_is_not_a_human_decision() {
    let (_tmp, envs) = isolated_env();
    let project = tempfile::tempdir().expect("temp project");
    write_gate_config(
        project.path(),
        "acceptance-smoke-demo-performance",
        "echo nope && exit 7",
    );
    init_git(project.path());

    let mut run = omk_cmd(&envs);
    run.current_dir(project.path())
        .args([
            "goal",
            "run",
            "Implement acceptance smoke demo proof for this CLI",
            "--until-ready",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("Blocked: verification blocked"))
        .stdout(predicate::str::contains("Decision needed").not());

    let proof = proof_json(&envs, project.path());
    assert!(proof["known_gaps"].as_array().unwrap().iter().any(|gap| gap
        .as_str()
        .is_some_and(|gap| gap.contains("verification blocked"))));
    assert!(proof["human_decisions_required"]
        .as_array()
        .unwrap()
        .is_empty());
}

#[test]
fn run_until_ready_dispatches_accepted_agent_followup_before_review() {
    let (_tmp, envs) = isolated_env();
    let project = tempfile::tempdir().expect("temp project");
    write_gate_config(
        project.path(),
        "acceptance-smoke-demo-performance",
        "echo controller-followup-ok",
    );
    init_git(project.path());

    let proposal = r#"OMK_TASK_PROPOSAL: {"id":"goal-agent-docs-followup","title":"Document follow-up readiness","description":"Document the remaining readiness follow-up found by the agent wave.","dependencies":["goal-agent-execute"],"read_set":["README.md"],"write_set":["README.md"],"risk":"low","acceptance":["README captures the follow-up readiness gap."],"budget_secs":120}"#;
    let mut run = omk_cmd(&envs);
    run.env("MOCK_KIMI", mock_kimi_path())
        .env("MOCK_KIMI_WRITE_FILE", "agent-output.txt")
        .env("MOCK_KIMI_WIRE_TEXT_WHEN_CONTAINS", "goal-agent-implement")
        .env("MOCK_KIMI_WIRE_TEXT", proposal)
        .env("OMK_WIRE_WORKER_POLL_INTERVAL_MS", "50")
        .current_dir(project.path())
        .args([
            "goal",
            "run",
            "Implement acceptance smoke demo proof and follow-up docs",
            "--until-ready",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("execute").count(2))
        .stdout(predicate::str::contains("review"))
        .stdout(predicate::str::contains("manual integration acceptance"));

    let dirs = goal_dirs(&envs);
    assert_eq!(dirs.len(), 1);
    let goal_dir = &dirs[0];
    let followup_outbox = goal_dir
        .join("artifacts/agent-runs/goal-agent-followups/workers/goal-agent-worker-0/outbox.jsonl");
    let outbox = read_jsonl(&followup_outbox);
    assert!(outbox
        .iter()
        .any(|result| result["task_id"] == "goal-agent-docs-followup"));

    let task_graph: Value = serde_json::from_str(
        &fs::read_to_string(goal_dir.join("task-graph.json")).expect("missing task graph"),
    )
    .expect("task graph should be JSON");
    let followup = task_graph["tasks"]
        .as_array()
        .unwrap()
        .iter()
        .find(|task| task["id"] == "goal-agent-docs-followup")
        .expect("missing accepted follow-up task");
    assert_eq!(followup["status"], "done");
}

#[test]
fn run_until_ready_stops_on_blocked_review_wall_before_manual_integration() {
    let (_tmp, envs) = isolated_env();
    let project = tempfile::tempdir().expect("temp project");
    write_gate_config(
        project.path(),
        "acceptance-smoke-demo",
        "echo controller-review-wall-blocked",
    );
    init_git(project.path());

    let mut run = omk_cmd(&envs);
    run.env("MOCK_KIMI", mock_kimi_path())
        .env("MOCK_KIMI_WRITE_FILE", "agent-output.txt")
        .env("OMK_WIRE_WORKER_POLL_INTERVAL_MS", "50")
        .current_dir(project.path())
        .args([
            "goal",
            "run",
            "Implement acceptance smoke demo proof for this CLI",
            "--until-ready",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("performance review is blocked"))
        .stdout(predicate::str::contains("manual integration acceptance").not());

    let proof = proof_json(&envs, project.path());
    assert_eq!(proof["status"], "not_ready");
    assert!(proof["known_gaps"].as_array().unwrap().iter().any(|gap| gap
        .as_str()
        .is_some_and(|gap| gap.contains("performance review is blocked"))));
    assert!(proof["human_decisions_required"]
        .as_array()
        .unwrap()
        .is_empty());
}