codex-cli-captain 0.0.10

Codex-Cli-Captain runtime, installer, and MCP server for Codex CLI.
use serde_json::{json, Value};
use std::io;

pub(crate) struct OrchestratorStateUpdateInput<'a> {
    pub(crate) next_step_after_attempt: &'a str,
    pub(crate) can_advance_after_attempt: bool,
    pub(crate) summary: &'a str,
    pub(crate) launch_result: Option<&'a Value>,
    pub(crate) codex_bin: &'a str,
    pub(crate) timestamp: &'a str,
}

pub(crate) struct RunRecordUpdateInput<'a> {
    pub(crate) timestamp: &'a str,
    pub(crate) summary: &'a str,
    pub(crate) attempt_id: &'a str,
    pub(crate) requested_progression_mode: &'a str,
    pub(crate) current_next_step: &'a str,
    pub(crate) codex_bin: &'a str,
    pub(crate) resolved_run: bool,
    pub(crate) follow_up_or_retry: bool,
    pub(crate) reclaimed_worker: bool,
    pub(crate) collapsed_worker_fan_in: bool,
    pub(crate) dispatched_execution: bool,
    pub(crate) effective_task_card: &'a Value,
    pub(crate) launch_result: Option<&'a Value>,
    pub(crate) collapsed_fan_in: Option<&'a Value>,
}

pub(crate) fn apply_orchestrator_state_after_attempt(
    orchestrator_state: &mut Value,
    input: OrchestratorStateUpdateInput<'_>,
) -> io::Result<()> {
    let orchestrator_object = orchestrator_state.as_object_mut().ok_or_else(|| {
        io::Error::new(
            io::ErrorKind::InvalidData,
            "orchestrator-state.json must be an object.",
        )
    })?;
    orchestrator_object.insert(
        "decision".to_string(),
        json!({
            "next_step": input.next_step_after_attempt,
            "can_advance": input.can_advance_after_attempt,
            "summary": input.summary
        }),
    );
    if let Some(launch) = input.launch_result {
        orchestrator_object.insert(
            "execution_request".to_string(),
            json!({
                "entrypoint": "ccc_orchestrate",
                "codex_bin": input.codex_bin,
                "requested_at": input.timestamp,
                "launch_result": launch,
            }),
        );
    }

    Ok(())
}

pub(crate) fn apply_run_record_after_attempt(
    run_record: &mut Value,
    input: RunRecordUpdateInput<'_>,
) -> io::Result<()> {
    let run_object = run_record
        .as_object_mut()
        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "run.json must be an object."))?;
    run_object.insert(
        "updated_at".to_string(),
        Value::String(input.timestamp.to_string()),
    );

    let captain_owned_after_attempt = input.resolved_run
        || input.follow_up_or_retry
        || input.reclaimed_worker
        || input.collapsed_worker_fan_in;
    run_object.insert(
        "active_role".to_string(),
        if captain_owned_after_attempt {
            Value::String("orchestrator".to_string())
        } else if input.dispatched_execution {
            input
                .effective_task_card
                .get("assigned_role")
                .cloned()
                .unwrap_or(Value::String("code specialist".to_string()))
        } else {
            run_object
                .get("active_role")
                .cloned()
                .unwrap_or(Value::Null)
        },
    );
    run_object.insert(
        "active_agent_id".to_string(),
        if captain_owned_after_attempt {
            Value::String("captain".to_string())
        } else if input.dispatched_execution {
            input
                .effective_task_card
                .get("assigned_agent_id")
                .cloned()
                .unwrap_or(Value::String("raider".to_string()))
        } else {
            run_object
                .get("active_agent_id")
                .cloned()
                .unwrap_or(Value::Null)
        },
    );
    run_object.insert(
        "latest_orchestrator_synthesis".to_string(),
        Value::String(input.summary.to_string()),
    );
    run_object.insert(
        "latest_entry_trace".to_string(),
        json!({
            "entrypoint": "ccc_orchestrate",
            "attempt_id": input.attempt_id,
            "requested_progression_mode": input.requested_progression_mode,
            "current_next_step": input.current_next_step,
            "codex_bin": input.codex_bin,
            "completed_at": input.timestamp,
        }),
    );

    if let Some(launch) = input.launch_result {
        append_launch_to_run_record(run_object, input.effective_task_card, launch);
    }
    if input.reclaimed_worker {
        run_object.insert(
            "latest_failure".to_string(),
            json!({
                "stage": "execution",
                "reason": "timeout",
                "summary": input.summary,
                "recorded_at": input.timestamp,
            }),
        );
    }
    if let Some(collapsed) = input.collapsed_fan_in.and_then(Value::as_object) {
        if let Some(thread_ids) = collapsed.get("thread_ids").and_then(Value::as_array) {
            merge_collapsed_thread_ids(run_object, thread_ids);
        }
    }

    Ok(())
}

pub(crate) fn apply_run_state_after_attempt(
    run_state_record: &mut Value,
    timestamp: &str,
    next_step_after_attempt: &str,
    current_phase_name: &str,
) -> io::Result<()> {
    let run_state_object = run_state_record.as_object_mut().ok_or_else(|| {
        io::Error::new(
            io::ErrorKind::InvalidData,
            "run-state.json must be an object.",
        )
    })?;
    run_state_object.insert(
        "updated_at".to_string(),
        Value::String(timestamp.to_string()),
    );
    run_state_object.insert(
        "next_action".to_string(),
        json!({
            "command": next_step_after_attempt
        }),
    );
    run_state_object.insert(
        "current_phase_name".to_string(),
        Value::String(current_phase_name.to_string()),
    );

    Ok(())
}

fn append_launch_to_run_record(
    run_object: &mut serde_json::Map<String, Value>,
    effective_task_card: &Value,
    launch: &Value,
) {
    let child_agents = run_object
        .get("child_agents")
        .and_then(Value::as_array)
        .cloned()
        .unwrap_or_default();
    let specialist_executors = run_object
        .get("specialist_executors")
        .and_then(Value::as_array)
        .cloned()
        .unwrap_or_default();
    run_object.insert(
        "child_agents".to_string(),
        Value::Array(
            child_agents
                .into_iter()
                .chain(std::iter::once(json!({
                    "agent_id": launch.get("child_agent_id").cloned().unwrap_or(Value::Null),
                    "parent_agent_id": "captain",
                    "role": launch.get("assigned_role").cloned().unwrap_or(Value::Null),
                    "status": "running",
                    "task_card_id": effective_task_card.get("task_card_id").cloned().unwrap_or(Value::Null),
                })))
                .collect(),
        ),
    );
    run_object.insert(
        "specialist_executors".to_string(),
        Value::Array(
            specialist_executors
                .into_iter()
                .chain(std::iter::once(json!({
                    "executor_id": format!("specialist-executor:{}", launch.get("child_agent_id").and_then(Value::as_str).unwrap_or("worker")),
                    "status": "running",
                    "task_card_id": effective_task_card.get("task_card_id").cloned().unwrap_or(Value::Null),
                    "delegation_id": launch.get("delegation_id").cloned().unwrap_or(Value::Null),
                    "child_agent_id": launch.get("child_agent_id").cloned().unwrap_or(Value::Null),
                })))
                .collect(),
        ),
    );
}

fn merge_collapsed_thread_ids(
    run_object: &mut serde_json::Map<String, Value>,
    thread_ids: &[Value],
) {
    let mut raw_thread_ids = run_object
        .get("raw_thread_ids")
        .and_then(Value::as_array)
        .cloned()
        .unwrap_or_default();
    for thread_id in thread_ids {
        if !raw_thread_ids.iter().any(|existing| existing == thread_id) {
            raw_thread_ids.push(thread_id.clone());
        }
    }
    run_object.insert(
        "raw_thread_ids".to_string(),
        Value::Array(raw_thread_ids.clone()),
    );
    if run_object
        .get("active_thread_id")
        .unwrap_or(&Value::Null)
        .is_null()
    {
        if let Some(first_thread_id) = raw_thread_ids.first() {
            run_object.insert("active_thread_id".to_string(), first_thread_id.clone());
        }
    }
}