ritalin 0.4.7

Executive function for AI coding agents. Focus their intelligence, ground their work, stop the avoidable mistakes.
use serde::Serialize;
use std::path::{Path, PathBuf};

use crate::error::AppError;
use crate::ledger::{marker, scope::Scope, state_dir};
use crate::output::{self, Ctx};

#[derive(Serialize)]
struct InitResult {
    state_dir: String,
    outcome: String,
    marker_created: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    shadows: Option<String>,
}

/// `init`/`seed` always target `<cwd>/.ritalin` — never a contract discovered
/// in an ancestor directory. Discovery-based targeting meant `--force` from a
/// subdirectory silently wiped the *ancestor's* ledgers. Returns the target
/// dir plus the ancestor contract it will shadow, if any.
pub fn target_state_dir(cwd: &Path) -> (PathBuf, Option<PathBuf>) {
    let target = cwd.join(".ritalin");
    let discovered = state_dir(cwd);
    let shadows = (discovered != target && discovered.exists()).then_some(discovered);
    (target, shadows)
}

pub fn run(ctx: Ctx, outcome: Option<String>, force: bool) -> Result<(), AppError> {
    let outcome = outcome.unwrap_or_else(|| {
        "TODO: replace with one-line outcome (e.g. \"User can save and reload notification settings\")"
            .to_string()
    });

    let cwd = std::env::current_dir()?;
    let (dir, shadows) = target_state_dir(&cwd);

    // Serialize against concurrent add/seed/gate. The lock creates the state
    // dir, so "already exists" is judged by contract files, not the dir.
    let _guard = crate::ledger::lock_state(&dir)?;
    let has_contract = dir.join("scope.yaml").exists() || dir.join("obligations.jsonl").exists();

    if has_contract && !force {
        return Err(AppError::InvalidInput(
            "contract already exists — use --force to overwrite".into(),
        ));
    }

    // When forcing, clear old ledgers so the contract starts fresh.
    if force && has_contract {
        let _ = std::fs::remove_file(dir.join("obligations.jsonl"));
        let _ = std::fs::remove_file(dir.join("evidence.jsonl"));
    }

    let scope = Scope::new(outcome.clone());
    scope.write(&dir)?;

    // Marker file lives next to .ritalin/, not inside it.
    let marker_msg = format!(
        "ritalin: outcome = {outcome}\n\
         This file is removed by `ritalin gate` once every critical obligation has evidence.\n"
    );
    marker::create(&dir, &marker_msg)?;

    let result = InitResult {
        state_dir: dir.display().to_string(),
        outcome,
        marker_created: true,
        shadows: shadows.map(|p| p.display().to_string()),
    };

    output::print_success_or(ctx, &result, |r| {
        use owo_colors::OwoColorize;
        println!("{} ritalin initialized", "+".green().bold());
        println!("  state:   {}", r.state_dir.dimmed());
        println!("  outcome: {}", r.outcome);
        if let Some(shadows) = &r.shadows {
            println!(
                "  {} nested contract — shadows {} for commands run here and below",
                "WARN".yellow().bold(),
                shadows
            );
        }
        println!();
        println!("Next steps:");
        println!("  1. Research & ground your approach before implementing");
        println!("  2. Add obligations (tests, research, references, freshness):");
        println!(
            "     {}",
            "ritalin add \"Feature works\" --proof \"pnpm test e2e/feature.test.ts\" --kind user_path"
                .dimmed()
        );
        println!(
            "     {}",
            "ritalin add \"Approach grounded\" --proof \"search --mode scholar 'topic' --json | jq '.results | length > 0'\" --kind research_grounded"
                .dimmed()
        );
        println!("  3. Wire the stop hook in .claude/settings.json:");
        println!(
            "     {}",
            r#"{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"ritalin gate --hook-mode"}]}]}}"#
                .dimmed()
        );
        println!("  4. Work, prove, gate. Blocked until every critical obligation has evidence.");
    });

    Ok(())
}