omk 0.5.0

A Rust runtime for Kimi CLI. Turns prompts into proof-backed engineering runs with gates, worktrees, and replay.
Documentation
use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{Map, Value};
use std::path::PathBuf;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GoalTaskDeliveryStatus {
    Planned,
    InProgress,
    Blocked,
    ReadyForReview,
    Delivered,
    Merged,
    Other(String),
}

impl GoalTaskDeliveryStatus {
    pub fn as_str(&self) -> &str {
        match self {
            GoalTaskDeliveryStatus::Planned => "planned",
            GoalTaskDeliveryStatus::InProgress => "in_progress",
            GoalTaskDeliveryStatus::Blocked => "blocked",
            GoalTaskDeliveryStatus::ReadyForReview => "ready_for_review",
            GoalTaskDeliveryStatus::Delivered => "delivered",
            GoalTaskDeliveryStatus::Merged => "merged",
            GoalTaskDeliveryStatus::Other(status) => status.as_str(),
        }
    }
}

impl From<String> for GoalTaskDeliveryStatus {
    fn from(value: String) -> Self {
        match value.as_str() {
            "planned" => GoalTaskDeliveryStatus::Planned,
            "in_progress" => GoalTaskDeliveryStatus::InProgress,
            "blocked" => GoalTaskDeliveryStatus::Blocked,
            "ready_for_review" => GoalTaskDeliveryStatus::ReadyForReview,
            "delivered" => GoalTaskDeliveryStatus::Delivered,
            "merged" => GoalTaskDeliveryStatus::Merged,
            _ => GoalTaskDeliveryStatus::Other(value),
        }
    }
}

impl Serialize for GoalTaskDeliveryStatus {
    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(self.as_str())
    }
}

impl<'de> Deserialize<'de> for GoalTaskDeliveryStatus {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        Ok(String::deserialize(deserializer)?.into())
    }
}

#[derive(Debug, Clone, Default, PartialEq)]
pub struct GoalTaskDeliveryMetadata {
    pub slice_id: Option<String>,
    pub owner: Option<String>,
    pub read_scope: Vec<String>,
    pub write_scope: Vec<String>,
    pub dependencies: Vec<String>,
    pub branch: Option<String>,
    pub worktree_name: Option<String>,
    pub worktree_path: Option<PathBuf>,
    pub gates: Vec<String>,
    pub review_needs: Vec<String>,
    pub pr_url: Option<String>,
    pub commit_sha: Option<String>,
    pub verification_summary: Option<String>,
    pub status: Option<GoalTaskDeliveryStatus>,
    /// Path to the artifact recording why a slice merge was blocked
    /// (used by worktree conflict detection and rollback flows).
    pub conflict_evidence_path: Option<PathBuf>,
    /// Human-readable summary of the blocking reason, persisted alongside
    /// conflict_evidence_path so rollback plans can disambiguate causes.
    pub conflict_blocking_reason: Option<String>,
    /// Identifier of the active lease claiming this slice's write-set.
    /// `None` when no agent currently owns the slice.
    pub slice_lease_id: Option<String>,
    pub extra: Map<String, Value>,
}

impl GoalTaskDeliveryMetadata {
    pub fn from_value(value: &Value) -> Self {
        let Some(object) = value.as_object() else {
            return Self::default();
        };
        let mut extra = object.clone();
        let slice_id = take_string(&mut extra, "slice_id");
        let owner = take_string(&mut extra, "owner");
        let read_scope = take_string_array(&mut extra, "read_scope").unwrap_or_default();
        let write_scope = take_string_array(&mut extra, "write_scope").unwrap_or_default();
        let dependencies = take_string_array(&mut extra, "dependencies").unwrap_or_default();
        let branch = take_string(&mut extra, "branch");
        let worktree_name = take_string(&mut extra, "worktree_name");
        let worktree_path = take_string(&mut extra, "worktree_path").map(PathBuf::from);
        let gates = take_string_array(&mut extra, "gates").unwrap_or_default();
        let review_needs = take_string_array(&mut extra, "review_needs").unwrap_or_default();
        let pr_url = take_string(&mut extra, "pr_url");
        let commit_sha = take_string(&mut extra, "commit_sha");
        let verification_summary = take_string(&mut extra, "verification_summary");
        let status = take_string(&mut extra, "status").map(GoalTaskDeliveryStatus::from);
        let conflict_evidence_path =
            take_string(&mut extra, "conflict_evidence_path").map(PathBuf::from);
        let conflict_blocking_reason = take_string(&mut extra, "conflict_blocking_reason");
        let slice_lease_id = take_string(&mut extra, "slice_lease_id");

        Self {
            slice_id,
            owner,
            read_scope,
            write_scope,
            dependencies,
            branch,
            worktree_name,
            worktree_path,
            gates,
            review_needs,
            pr_url,
            commit_sha,
            verification_summary,
            status,
            conflict_evidence_path,
            conflict_blocking_reason,
            slice_lease_id,
            extra,
        }
    }

    pub fn to_value(&self) -> Value {
        let mut object = self.extra.clone();
        insert_string(&mut object, "slice_id", self.slice_id.as_deref());
        insert_string(&mut object, "owner", self.owner.as_deref());
        insert_string_array(&mut object, "read_scope", &self.read_scope);
        insert_string_array(&mut object, "write_scope", &self.write_scope);
        insert_string_array(&mut object, "dependencies", &self.dependencies);
        insert_string(&mut object, "branch", self.branch.as_deref());
        insert_string(&mut object, "worktree_name", self.worktree_name.as_deref());
        let worktree_path = self
            .worktree_path
            .as_ref()
            .map(|path| path.display().to_string());
        insert_string(&mut object, "worktree_path", worktree_path.as_deref());
        insert_string_array(&mut object, "gates", &self.gates);
        insert_string_array(&mut object, "review_needs", &self.review_needs);
        insert_string(&mut object, "pr_url", self.pr_url.as_deref());
        insert_string(&mut object, "commit_sha", self.commit_sha.as_deref());
        insert_string(
            &mut object,
            "verification_summary",
            self.verification_summary.as_deref(),
        );
        insert_string(
            &mut object,
            "status",
            self.status.as_ref().map(GoalTaskDeliveryStatus::as_str),
        );
        let conflict_evidence_path = self
            .conflict_evidence_path
            .as_ref()
            .map(|path| path.display().to_string());
        insert_string(
            &mut object,
            "conflict_evidence_path",
            conflict_evidence_path.as_deref(),
        );
        insert_string(
            &mut object,
            "conflict_blocking_reason",
            self.conflict_blocking_reason.as_deref(),
        );
        insert_string(
            &mut object,
            "slice_lease_id",
            self.slice_lease_id.as_deref(),
        );
        Value::Object(object)
    }

    pub fn merge_update(&mut self, update: GoalTaskDeliveryMetadataUpdate) {
        replace_if_some(&mut self.slice_id, update.slice_id);
        replace_if_some(&mut self.owner, update.owner);
        if let Some(read_scope) = update.read_scope {
            self.read_scope = read_scope;
        }
        if let Some(write_scope) = update.write_scope {
            self.write_scope = write_scope;
        }
        if let Some(dependencies) = update.dependencies {
            self.dependencies = dependencies;
        }
        replace_if_some(&mut self.branch, update.branch);
        replace_if_some(&mut self.worktree_name, update.worktree_name);
        replace_if_some(&mut self.worktree_path, update.worktree_path);
        if let Some(gates) = update.gates {
            self.gates = gates;
        }
        if let Some(review_needs) = update.review_needs {
            self.review_needs = review_needs;
        }
        replace_if_some(&mut self.pr_url, update.pr_url);
        replace_if_some(&mut self.commit_sha, update.commit_sha);
        replace_if_some(&mut self.verification_summary, update.verification_summary);
        replace_if_some(&mut self.status, update.status);
        replace_if_some(
            &mut self.conflict_evidence_path,
            update.conflict_evidence_path,
        );
        replace_if_some(
            &mut self.conflict_blocking_reason,
            update.conflict_blocking_reason,
        );
        replace_if_some(&mut self.slice_lease_id, update.slice_lease_id);
        self.extra.extend(update.extra);
    }

    pub fn is_empty(&self) -> bool {
        matches!(self.to_value(), Value::Object(object) if object.is_empty())
    }
}

impl Serialize for GoalTaskDeliveryMetadata {
    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        self.to_value().serialize(serializer)
    }
}

impl<'de> Deserialize<'de> for GoalTaskDeliveryMetadata {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let value = Value::deserialize(deserializer)?;
        if !value.is_object() {
            return Err(D::Error::custom("delivery metadata must be a JSON object"));
        }
        Ok(Self::from_value(&value))
    }
}

#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct GoalTaskDeliveryMetadataUpdate {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub slice_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub owner: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub read_scope: Option<Vec<String>>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub write_scope: Option<Vec<String>>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub dependencies: Option<Vec<String>>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub branch: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub worktree_name: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub worktree_path: Option<PathBuf>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub gates: Option<Vec<String>>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub review_needs: Option<Vec<String>>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub pr_url: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub commit_sha: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub verification_summary: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub status: Option<GoalTaskDeliveryStatus>,
    /// Path to the artifact recording why a slice merge was blocked
    /// (used by worktree conflict detection and rollback flows).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub conflict_evidence_path: Option<PathBuf>,
    /// Human-readable summary of the blocking reason, persisted alongside
    /// conflict_evidence_path so rollback plans can disambiguate causes.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub conflict_blocking_reason: Option<String>,
    /// Identifier of the active lease claiming this slice's write-set.
    /// `None` when no agent currently owns the slice.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub slice_lease_id: Option<String>,
    #[serde(default, flatten)]
    pub extra: Map<String, Value>,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct GoalTaskDeliveryRecord {
    pub task_id: String,
    #[serde(flatten)]
    pub metadata: GoalTaskDeliveryMetadata,
}

impl Serialize for GoalTaskDeliveryRecord {
    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut value = self.metadata.to_value();
        if let Some(object) = value.as_object_mut() {
            object.insert("task_id".to_string(), Value::String(self.task_id.clone()));
        }
        value.serialize(serializer)
    }
}

fn take_string(object: &mut Map<String, Value>, key: &str) -> Option<String> {
    let value = object.get(key)?.as_str()?.to_string();
    object.remove(key);
    Some(value)
}

fn take_string_array(object: &mut Map<String, Value>, key: &str) -> Option<Vec<String>> {
    let values = serde_json::from_value(object.get(key)?.clone()).ok()?;
    object.remove(key);
    Some(values)
}

fn insert_string(object: &mut Map<String, Value>, key: &str, value: Option<&str>) {
    if let Some(value) = value {
        object.insert(key.to_string(), Value::String(value.to_string()));
    }
}

fn insert_string_array(object: &mut Map<String, Value>, key: &str, value: &[String]) {
    if !value.is_empty() {
        object.insert(key.to_string(), serde_json::json!(value));
    }
}

fn replace_if_some<T>(target: &mut Option<T>, value: Option<T>) {
    if let Some(value) = value {
        *target = Some(value);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn metadata_roundtrip_with_none_extra_fields() {
        let original = GoalTaskDeliveryMetadata {
            slice_id: Some("slice-1".to_string()),
            owner: Some("agent-a".to_string()),
            read_scope: vec!["src/lib.rs".to_string()],
            write_scope: vec!["src/main.rs".to_string()],
            dependencies: vec!["dep-1".to_string()],
            branch: Some("feature/foo".to_string()),
            worktree_name: Some("wt-1".to_string()),
            worktree_path: Some(PathBuf::from("/tmp/wt1")),
            gates: vec!["test".to_string()],
            review_needs: vec!["code".to_string()],
            pr_url: Some("https://github.com/org/repo/pull/1".to_string()),
            commit_sha: Some("abc123".to_string()),
            verification_summary: Some("ok".to_string()),
            status: Some(GoalTaskDeliveryStatus::InProgress),
            conflict_evidence_path: None,
            conflict_blocking_reason: None,
            slice_lease_id: None,
            extra: Map::new(),
        };

        let json = serde_json::to_value(&original).unwrap();
        let deserialized: GoalTaskDeliveryMetadata = serde_json::from_value(json).unwrap();
        assert_eq!(original, deserialized);
    }
}