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 crate::runtime::events::{
    Event, EventBuilder, EventKind, EventWriter, RunId, TaskId, WorkerId,
};
use crate::runtime::goal::state::GOAL_CONTROLLER_ACTOR;
use crate::runtime::goal::task_graph::{GoalTask, GoalTaskGraph, GoalTaskStatus};

pub(crate) async fn append_controller_task_events(
    state: &crate::runtime::goal::state::GoalState,
    task_graph: &GoalTaskGraph,
) -> Result<()> {
    let writer = EventWriter::new(state.state_dir.join(crate::runtime::config::EVENTS_FILE));
    let builder = EventBuilder::new(RunId(state.goal_id.clone()));
    let worker_id = WorkerId(GOAL_CONTROLLER_ACTOR.to_string());
    let mut events = Vec::new();

    for task in task_graph
        .tasks
        .iter()
        .filter(|task| task.status == GoalTaskStatus::Done)
    {
        let task_id = TaskId(task.id.clone());
        events.push(
            Event::new(RunId(state.goal_id.clone()), EventKind::TaskStarted)
                .with_actor(GOAL_CONTROLLER_ACTOR)
                .with_payload(serde_json::json!({
                    "task_id": task.id,
                    "worker_id": GOAL_CONTROLLER_ACTOR,
                    "title": task.title,
                }))?,
        );
        events.push(builder.task_completed(
            task_id,
            worker_id.clone(),
            Some(&controller_task_summary(task)),
        )?);
    }

    if events.is_empty() {
        return Ok(());
    }
    writer.append_many(&events).await
}

pub(crate) fn controller_task_summary(task: &GoalTask) -> String {
    let artifacts = task
        .evidence
        .iter()
        .map(|evidence| evidence.path.display().to_string())
        .collect::<Vec<_>>()
        .join(", ");
    format!(
        "{} completed with artifact evidence: {}",
        task.id, artifacts
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::runtime::goal::task_graph::{GoalTask, GoalTaskEvidence, GoalTaskStatus};
    use std::path::PathBuf;

    fn task_with_evidence(id: &str, evidence: Vec<GoalTaskEvidence>) -> GoalTask {
        GoalTask {
            id: id.to_string(),
            title: id.to_string(),
            description: String::new(),
            status: GoalTaskStatus::Done,
            owner_role: None,
            completed_at: None,
            evidence,
            retry_count: 0,
            max_retries: 0,
            lease_expires_at: None,
            dependencies: vec![],
            read_set: vec![],
            write_set: vec![],
            risk: String::new(),
            acceptance: vec![],
        }
    }

    #[test]
    fn controller_task_summary_with_artifacts() {
        let task = task_with_evidence(
            "task-1",
            vec![
                GoalTaskEvidence {
                    kind: "file".to_string(),
                    path: PathBuf::from("src/main.rs"),
                    summary: "changed".to_string(),
                },
                GoalTaskEvidence {
                    kind: "file".to_string(),
                    path: PathBuf::from("src/lib.rs"),
                    summary: "changed".to_string(),
                },
            ],
        );
        let summary = controller_task_summary(&task);
        assert!(summary.contains("task-1"));
        assert!(summary.contains("src/main.rs"));
        assert!(summary.contains("src/lib.rs"));
    }

    #[test]
    fn controller_task_summary_without_artifacts() {
        let task = task_with_evidence("task-1", vec![]);
        let summary = controller_task_summary(&task);
        assert!(summary.contains("task-1"));
        assert!(summary.contains("completed with artifact evidence:"));
    }
}