omk 0.5.0

A Rust runtime for Kimi CLI. Turns prompts into proof-backed engineering runs with gates, worktrees, and replay.
Documentation
use anyhow::Result;
use std::path::PathBuf;

use crate::cli::team::args::{HealthArgs, StatusArgs};

use crate::runtime::config::{EVENTS_FILE, TEAM_DIR, WORKERS_DIR};
use crate::runtime::sanitize::sanitize_name;
use crate::runtime::state::TeamState;
use crate::runtime::worker::WorkerSpec;

pub(crate) async fn list_teams() -> Result<()> {
    let teams_dir = crate::runtime::config::omk_state_dir().join(TEAM_DIR);

    if !teams_dir.exists() {
        println!("No teams found.");
        return Ok(());
    }

    let mut entries = tokio::fs::read_dir(&teams_dir).await?;
    let mut teams = Vec::new();

    while let Some(entry) = entries.next_entry().await? {
        let path = entry.path();
        if path.is_dir() {
            let name = entry.file_name().to_string_lossy().to_string();

            if let Ok(state) = TeamState::load(&path).await {
                teams.push((name, state));
            }
        }
    }

    if teams.is_empty() {
        println!("No teams found.");
        return Ok(());
    }

    println!("Active teams:\n");
    println!("{:<20} {:<20} Task", "Name", "Phase");
    println!("{}", "".repeat(78));

    for (name, state) in teams {
        println!(
            "{:<20} {:<20} {}",
            name,
            format!("{:?}", state.phase),
            state.task.chars().take(40).collect::<String>()
        );
    }

    println!("\nUse `omk team status <name>` for details.");
    Ok(())
}

pub(crate) async fn status(args: StatusArgs) -> Result<()> {
    let team_name = sanitize_name(&args.name)?;
    let state_dir = crate::runtime::config::omk_state_dir()
        .join(TEAM_DIR)
        .join(&team_name);

    if !state_dir.exists() {
        anyhow::bail!(
            "Team '{}' not found. Expected state at: {}",
            team_name,
            state_dir.display()
        );
    }

    let state = TeamState::load(&state_dir).await?;

    println!("Team:        {}", state.name);
    println!("Task:        {}", state.task);
    println!("Phase:       {:?}", state.phase);
    println!("Created:     {}", state.created_at);
    println!();
    println!("Workers:");

    let workers_dir = state_dir.join(WORKERS_DIR);
    if workers_dir.exists() {
        let mut entries = tokio::fs::read_dir(&workers_dir).await?;
        while let Some(entry) = entries.next_entry().await? {
            let worker_dir = entry.path();
            let spec_path = worker_dir.join("worker-spec.json");
            if !spec_path.exists() {
                continue;
            }

            let spec: WorkerSpec = {
                let json = tokio::fs::read_to_string(&spec_path).await?;
                serde_json::from_str(&json)?
            };

            let hb_status = if spec.heartbeat.exists() {
                match tokio::fs::read_to_string(&spec.heartbeat).await {
                    Ok(json) => match serde_json::from_str::<serde_json::Value>(&json) {
                        Ok(v) => v
                            .get("status")
                            .and_then(|s| s.as_str())
                            .unwrap_or("unknown")
                            .to_string(),
                        Err(_) => "invalid".to_string(),
                    },
                    Err(_) => "unreadable".to_string(),
                }
            } else {
                "missing".to_string()
            };

            let inbox_count = count_jsonl_lines(&spec.inbox).await;
            let outbox_count = count_jsonl_lines(&spec.outbox).await;

            println!(
                "  {:12} role={:10} hb={:8} inbox={:2} outbox={:2}",
                spec.name, spec.role, hb_status, inbox_count, outbox_count
            );
        }
    }

    println!();
    println!("Tasks:       {} total", state.tasks.len());
    for task in &state.tasks {
        println!("  [{:?}] {}", task.status, task.description);
    }

    Ok(())
}

pub(crate) async fn health(args: HealthArgs) -> Result<()> {
    let team_name = sanitize_name(&args.name)?;
    let state_dir = crate::runtime::config::omk_state_dir()
        .join(TEAM_DIR)
        .join(&team_name);

    if !state_dir.exists() {
        anyhow::bail!(
            "Team '{}' not found. Expected state at: {}",
            team_name,
            state_dir.display()
        );
    }

    let event_log = state_dir.join(EVENTS_FILE);
    let event_writer = crate::runtime::events::EventWriter::new(&event_log);
    let run_id = crate::runtime::events::RunId(team_name.clone());

    let watchdog = crate::runtime::watchdog::Watchdog::with_defaults();
    let report = watchdog
        .check_team(&run_id, &state_dir, &event_writer)
        .await?;

    println!("🩺 Health check — {}", report.run_id);
    println!("Checked at:  {}", report.checked_at);
    println!("Workers:     {}", report.workers.len());
    println!();

    for worker in &report.workers {
        let status_icon = match worker.status {
            crate::runtime::watchdog::HealthStatus::Healthy => "",
            crate::runtime::watchdog::HealthStatus::Stalled => "⚠️",
            crate::runtime::watchdog::HealthStatus::Dead => "",
            crate::runtime::watchdog::HealthStatus::Unknown => "",
        };
        println!(
            "  {} {:12} inbox={} outbox={}",
            status_icon, worker.worker_id, worker.inbox_count, worker.outbox_count
        );
        println!("{}", worker.message);
    }

    println!();

    if report.issues_found == 0 {
        println!("🎉 All workers healthy.");
    } else {
        println!(
            "⚠️  {} issue(s) found. Check events.jsonl for details.",
            report.issues_found
        );
    }

    Ok(())
}

pub(crate) async fn count_jsonl_lines(path: &PathBuf) -> usize {
    if !path.exists() {
        return 0;
    }
    match tokio::fs::read_to_string(path).await {
        Ok(content) => content.lines().filter(|l| !l.trim().is_empty()).count(),
        Err(_) => 0,
    }
}