ritalin 0.2.0

Executive function for AI coding agents. Focus their intelligence, ground their work, stop the avoidable mistakes.
use chrono::Utc;
use serde::Serialize;
use std::process::Command;

use crate::error::AppError;
use crate::ledger::{
    evidence, evidence::Evidence, is_initialized, obligations, state_dir, workspace_hash,
};
use crate::output::{self, Ctx};

const TAIL_LIMIT: usize = 2000;

fn tail(s: &str) -> String {
    if s.len() <= TAIL_LIMIT {
        s.to_string()
    } else {
        // Find a valid UTF-8 char boundary near the desired start position.
        let desired_start = s.len() - TAIL_LIMIT;
        let start = s
            .char_indices()
            .map(|(i, _)| i)
            .find(|&i| i >= desired_start)
            .unwrap_or(desired_start);
        format!("{}", &s[start..])
    }
}

#[derive(Serialize)]
struct ProveResult {
    obligation_id: String,
    command: String,
    exit_code: i32,
    discharged: bool,
    stdout_tail: String,
    stderr_tail: String,
}

pub fn run(ctx: Ctx, id: String, cmd: Option<String>) -> Result<(), AppError> {
    let cwd = std::env::current_dir()?;
    if !is_initialized(&cwd) {
        return Err(AppError::NotInitialized);
    }
    let dir = state_dir(&cwd);

    let ob = obligations::find(&dir, &id)?;
    let command = cmd.unwrap_or_else(|| ob.proof_cmd.clone());

    // Run via shell so users can pass pipes, redirects, env vars, etc.
    let output_res = Command::new("sh").arg("-c").arg(&command).output()?;

    let exit_code = output_res.status.code().unwrap_or(-1);
    let stdout = String::from_utf8_lossy(&output_res.stdout).to_string();
    let stderr = String::from_utf8_lossy(&output_res.stderr).to_string();

    let proof_hash = evidence::proof_hash(&command);
    let project_root = dir.parent().unwrap_or(&cwd);
    let ws_hash = workspace_hash::compute(project_root).unwrap_or_default();

    let ev = Evidence {
        obligation_id: id.clone(),
        command: command.clone(),
        exit_code,
        stdout_tail: tail(&stdout),
        stderr_tail: tail(&stderr),
        proof_hash,
        workspace_hash: ws_hash,
        recorded_at: Utc::now(),
    };
    evidence::append(&dir, &ev)?;

    let discharged = exit_code == 0;
    let result = ProveResult {
        obligation_id: id,
        command,
        exit_code,
        discharged,
        stdout_tail: ev.stdout_tail.clone(),
        stderr_tail: ev.stderr_tail.clone(),
    };

    output::print_success_or(ctx, &result, |r| {
        use owo_colors::OwoColorize;
        let badge = if r.discharged {
            "PASS".green().bold().to_string()
        } else {
            "FAIL".red().bold().to_string()
        };
        println!(
            "{} {} (exit {})",
            badge,
            r.obligation_id.bold(),
            r.exit_code
        );
        println!("  cmd: {}", r.command.dimmed());
        if !r.stderr_tail.is_empty() {
            println!("  stderr: {}", r.stderr_tail.dimmed());
        }
    });

    if !discharged {
        return Err(AppError::VerificationFailed(format!(
            "proof command exited {} for {}",
            exit_code, result.obligation_id
        )));
    }

    Ok(())
}