omk 0.5.0

A Rust runtime for Kimi CLI. Turns prompts into proof-backed engineering runs with gates, worktrees, and replay.
Documentation
use tempfile::TempDir;

use crate::runtime::config::{EVENTS_FILE, WORKERS_DIR};
use crate::runtime::events::{EventKind, EventReader, EventWriter};
use crate::runtime::scheduler::runner::TeamRunner;
use crate::runtime::scheduler::task::{Task, TaskState};
use crate::runtime::worker::{ResultStatus, WorkerResult, WorkerSpec};

pub async fn make_runner(tmp: &TempDir) -> TeamRunner {
    let state_dir = tmp.path().join("state");
    let event_log = state_dir.join(EVENTS_FILE);
    tokio::fs::create_dir_all(&state_dir).await.unwrap();
    let event_writer = EventWriter::new(&event_log);
    TeamRunner::init(
        "run-test",
        "test task",
        tmp.path(),
        &state_dir,
        event_writer,
    )
    .await
    .unwrap()
}

pub async fn make_runner_with_tasks(tmp: &TempDir, tasks: Vec<Task>) -> TeamRunner {
    let state_dir = tmp.path().join("state");
    let event_log = state_dir.join(EVENTS_FILE);
    tokio::fs::create_dir_all(&state_dir).await.unwrap();
    TeamRunner::init_with_tasks(
        "run-test",
        tmp.path(),
        &state_dir,
        EventWriter::new(&event_log),
        tasks,
    )
    .await
    .unwrap()
}

pub async fn make_spec(tmp: &TempDir, name: &str) -> WorkerSpec {
    let dir = tmp.path().join("state").join(WORKERS_DIR).join(name);
    tokio::fs::create_dir_all(&dir).await.unwrap();
    WorkerSpec {
        name: name.to_string(),
        role: "coder".to_string(),
        inbox: dir.join("inbox.jsonl"),
        outbox: dir.join("outbox.jsonl"),
        heartbeat: dir.join("heartbeat.json"),
        project_dir: None,
        external_tools: None,
        approval_policy: crate::runtime::wire_worker::ApprovalPolicy::default(),
        approval_timeout_secs: crate::runtime::worker::default_approval_timeout_secs(),
    }
}

pub async fn read_inbox(spec: &WorkerSpec) -> String {
    tokio::fs::read_to_string(&spec.inbox).await.unwrap()
}

pub async fn write_outbox(spec: &WorkerSpec, content: impl AsRef<[u8]>) {
    tokio::fs::write(&spec.outbox, content).await.unwrap();
}

pub fn assert_task_state(
    runner: &TeamRunner,
    task_id: &str,
    state: TaskState,
    owner: Option<&str>,
) {
    let task = runner
        .claim_store
        .get(&task_id.to_string())
        .expect("task must exist");
    assert_eq!(task.state, state, "task {task_id} state mismatch");
    assert_eq!(
        task.owner.as_deref(),
        owner,
        "task {task_id} owner mismatch"
    );
}

pub fn assert_task_completed(runner: &TeamRunner, task_id: &str) {
    let task = runner
        .claim_store
        .get(&task_id.to_string())
        .expect("task must exist");
    assert_eq!(
        task.state,
        TaskState::Completed,
        "task {task_id} must be completed"
    );
    assert!(
        task.completed_at.is_some(),
        "task {task_id} must have completed_at"
    );
}

pub fn success_result(task_id: &str) -> WorkerResult {
    WorkerResult {
        task_id: task_id.to_string(),
        status: ResultStatus::Success,
        summary: "done".to_string(),
        artifacts: vec![],
        elapsed_secs: 5,
    }
}

pub fn simple_failed_json(task_id: &str, error: &str) -> serde_json::Value {
    serde_json::json!({
        "id": task_id,
        "status": "failed",
        "error": error,
    })
}

pub fn stale_cleanup_marker(tmp: &TempDir, worker_name: &str) -> std::path::PathBuf {
    tmp.path()
        .join("state")
        .join(WORKERS_DIR)
        .join(worker_name)
        .join("stale-worker-cleanup.json")
}

pub async fn assert_scheduler_event_present(tmp: &TempDir, kind: EventKind, worker_id: &str) {
    let events = EventReader::read_all(&tmp.path().join("state").join(EVENTS_FILE))
        .await
        .unwrap();
    assert!(
        events.iter().any(|event| {
            event.kind == kind
                && event.actor.as_deref() == Some("scheduler")
                && event
                    .payload
                    .as_ref()
                    .and_then(|payload| payload.get("worker_id"))
                    .and_then(|value| value.as_str())
                    == Some(worker_id)
        }),
        "expected {kind:?} event for worker {worker_id}"
    );
}