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>,
}
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);
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(),
));
}
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)?;
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(())
}