//! Decapod library crate.
//!
//! Exposes the core control-plane runtime (`core`), embedded constitution/document
//! access (`constitution`), and plugin subsystems (`plugins`).
//!
//! Runtime operational contracts for agents are defined in repository entrypoint
//! docs and constitution documents, not in Rust source comments.

pub(crate) mod cli;
pub mod constitution;
pub mod core;
pub mod plugins;
pub(crate) mod subsystems;

use cli::*;

use core::{
    db, docs, docs_cli, error, flight_recorder, migration, obligation, plan_governance, proof,
    repomap, scaffold, state_commit,
    store::{Store, StoreKind},
    todo, trace, validate, workspace,
};
use plugins::{
    aptitude, archive, container, context, cron, decide, doctor, eval, federation, feedback,
    health, internalize, knowledge, lcm, map_ops, policy, primitives, reflex, verify, watcher,
    workflow,
};

use clap::{CommandFactory, Parser};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
use std::fs;
use std::io;
use std::io::IsTerminal;
use std::io::Read;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::sync::OnceLock;
use std::sync::mpsc;
use std::thread;
use std::time::{SystemTime, UNIX_EPOCH};

// CLI struct definitions have been moved to src/cli.rs

// (remaining CLI struct definitions removed — now in src/cli.rs)

fn find_decapod_project_root(start_dir: &Path) -> Result<PathBuf, error::DecapodError> {
    let mut current_dir = PathBuf::from(start_dir);
    loop {
        if current_dir.join(".decapod").exists() {
            return Ok(current_dir);
        }
        if !current_dir.pop() {
            return Err(error::DecapodError::NotFound(
                "'.decapod' directory not found in current or parent directories. Run `decapod init` first.".to_string(),
            ));
        }
    }
}

// Process-local session password - eliminates unsafe env::set_var
static SESSION_PASSWORD: OnceLock<String> = OnceLock::new();

fn clean_project(dir: Option<PathBuf>) -> Result<(), error::DecapodError> {
    let raw_dir = match dir {
        Some(d) => d,
        None => std::env::current_dir()?,
    };
    let target_dir = std::fs::canonicalize(&raw_dir).map_err(error::DecapodError::IoError)?;

    let decapod_root = target_dir.join(".decapod");
    if decapod_root.exists() {
        println!("Removing directory: {}", decapod_root.display());
        fs::remove_dir_all(&decapod_root).map_err(error::DecapodError::IoError)?;
    }

    for file in ["AGENTS.md", "CLAUDE.md", "GEMINI.md", "CODEX.md"] {
        let path = target_dir.join(file);
        if path.exists() {
            println!("Removing file: {}", path.display());
            fs::remove_file(&path).map_err(error::DecapodError::IoError)?;
        }
    }
    println!("Decapod files cleaned from {}", target_dir.display());
    Ok(())
}

fn decapod_config_path(target_dir: &Path) -> PathBuf {
    target_dir.join(".decapod").join("config.toml")
}

fn load_project_config_if_present(
    target_dir: &Path,
) -> Result<Option<DecapodProjectConfig>, error::DecapodError> {
    let config_path = decapod_config_path(target_dir);
    if !config_path.exists() {
        return Ok(None);
    }
    let raw = fs::read_to_string(&config_path).map_err(error::DecapodError::IoError)?;
    let cfg: DecapodProjectConfig = toml::from_str(&raw).map_err(|e| {
        error::DecapodError::ValidationError(format!("AUTOREMEDIABLE_VALIDATION_ERROR code=INVALID_CONFIG_SCHEMA severity=transient auto_remediable=true audience=agent agent_action=\"fix the .decapod/config.toml schema to be valid TOML\" user_note=\"Configuration file schema is invalid; the agent should correct the file or report the issue.\"\nInvalid .decapod/config.toml schema: {}", e))
    })?;
    Ok(Some(cfg))
}

fn write_project_config(
    target_dir: &Path,
    config: &DecapodProjectConfig,
    dry_run: bool,
) -> Result<(), error::DecapodError> {
    if dry_run {
        return Ok(());
    }
    let config_path = decapod_config_path(target_dir);
    if let Some(parent) = config_path.parent() {
        fs::create_dir_all(parent).map_err(error::DecapodError::IoError)?;
    }
    let serialized = toml::to_string_pretty(config).map_err(|e| {
        error::DecapodError::ValidationError(format!("AUTOREMEDIABLE_VALIDATION_ERROR code=CONFIG_SERIALIZE_FAILED severity=transient auto_remediable=true audience=agent agent_action=\"ensure the .decapod/config.toml data can be serialized (e.g., fix data types)\" user_note=\"Failed to serialize configuration; the agent should adjust the config content or report the issue.\"\nFailed to serialize config.toml: {}", e))
    })?;
    fs::write(config_path, serialized).map_err(error::DecapodError::IoError)?;
    Ok(())
}

fn seed_init_generated_state(target_dir: &Path, dry_run: bool) -> Result<(), error::DecapodError> {
    if dry_run {
        return Ok(());
    }

    let _ = docs_cli::sync_override_checksum(target_dir, false)?;
    Ok(())
}

fn is_not_git_repository_error(err: &error::DecapodError) -> bool {
    matches!(
        err,
        error::DecapodError::ValidationError(message)
            if message.contains("Not in a git repository")
    )
}

fn infer_repo_context(target_dir: &Path) -> RepoContext {
    let mut ctx = RepoContext {
        product_name: target_dir
            .file_name()
            .and_then(|s| s.to_str())
            .map(|s| s.to_string()),
        ..RepoContext::default()
    };

    if target_dir.join("Cargo.toml").exists() {
        ctx.primary_languages.push("rust".to_string());
        ctx.detected_surfaces.push("cargo".to_string());
        if let Ok(raw) = fs::read_to_string(target_dir.join("Cargo.toml"))
            && let Ok(v) = toml::from_str::<toml::Value>(&raw)
            && let Some(name) = v
                .get("package")
                .and_then(|p| p.get("name"))
                .and_then(|n| n.as_str())
        {
            ctx.product_name = Some(name.to_string());
        }
    }
    if target_dir.join("package.json").exists() {
        ctx.primary_languages
            .push("typescript/javascript".to_string());
        ctx.detected_surfaces.push("npm".to_string());
    }
    if target_dir.join("pyproject.toml").exists() || target_dir.join("requirements.txt").exists() {
        ctx.primary_languages.push("python".to_string());
        ctx.detected_surfaces.push("python".to_string());
    }
    if target_dir.join("go.mod").exists() {
        ctx.primary_languages.push("go".to_string());
        ctx.detected_surfaces.push("go".to_string());
    }
    infer_languages_from_source_files(target_dir, &mut ctx);

    if target_dir.join("frontend").exists() || target_dir.join("web").exists() {
        ctx.detected_surfaces.push("frontend".to_string());
    }
    if target_dir.join("api").exists()
        || target_dir.join("server").exists()
        || target_dir.join("backend").exists()
    {
        ctx.detected_surfaces.push("backend".to_string());
    }

    if ctx.detected_surfaces.iter().any(|s| s == "frontend") {
        ctx.product_type = Some("application".to_string());
    } else if !ctx.detected_surfaces.is_empty() || !ctx.primary_languages.is_empty() {
        ctx.product_type = Some("service_or_library".to_string());
    }

    let intent_path = target_dir.join(core::project_specs::LOCAL_PROJECT_SPECS_INTENT);
    if intent_path.exists()
        && let Ok(intent) = fs::read_to_string(intent_path)
        && let Some(summary) = core::project_specs::first_markdown_content_line(&intent)
    {
        ctx.product_summary = Some(summary);
    }
    let architecture_path = target_dir.join(core::project_specs::LOCAL_PROJECT_SPECS_ARCHITECTURE);
    if architecture_path.exists()
        && let Ok(arch) = fs::read_to_string(architecture_path)
        && let Some(direction) = core::project_specs::first_markdown_content_line(&arch)
    {
        ctx.architecture_direction = Some(direction);
    }

    if ctx.product_summary.is_none() {
        let readme_path = target_dir.join("README.md");
        if readme_path.exists()
            && let Ok(readme) = fs::read_to_string(readme_path)
            && let Some(summary) = core::project_specs::first_markdown_content_line(&readme)
        {
            ctx.product_summary = Some(summary);
        }
    }

    if ctx.product_summary.is_none() {
        ctx.product_summary = Some(match ctx.product_name.as_deref() {
            Some(name) => format!("Deliver {} against explicit user intent with proof-backed completion.", name),
            None => "Deliver the repository outcome against explicit user intent with proof-backed completion.".to_string(),
        });
    }
    if ctx.architecture_direction.is_none() {
        let has_frontend = ctx.detected_surfaces.iter().any(|s| s == "frontend");
        let has_backend = ctx.detected_surfaces.iter().any(|s| s == "backend");
        let inferred = match (has_frontend, has_backend) {
            (true, true) => {
                "Layered frontend/backend system with explicit contracts, isolated mutation boundaries, and proof-gated promotion."
            }
            (true, false) => {
                "Frontend-first architecture with explicit API boundaries and deterministic validation gates."
            }
            (false, true) => {
                "Service-oriented backend with clear interface boundaries, durable state ownership, and proof-gated releases."
            }
            (false, false) => {
                "Composable repository architecture with explicit boundaries and proof-backed delivery invariants."
            }
        };
        ctx.architecture_direction = Some(inferred.to_string());
    }
    if ctx.done_criteria.is_none() {
        ctx.done_criteria = Some(
            "Decapod validate passes, required tests pass, and promotion-relevant artifacts are present."
                .to_string(),
        );
    }

    ctx.primary_languages.sort();
    ctx.primary_languages.dedup();
    ctx.detected_surfaces.sort();
    ctx.detected_surfaces.dedup();
    ctx
}

fn infer_languages_from_source_files(target_dir: &Path, ctx: &mut RepoContext) {
    fn visit(dir: &Path, remaining: &mut usize, ctx: &mut RepoContext) {
        if *remaining == 0 {
            return;
        }
        let Ok(entries) = fs::read_dir(dir) else {
            return;
        };
        for entry in entries.flatten() {
            if *remaining == 0 {
                return;
            }
            let path = entry.path();
            let name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
            if path.is_dir() {
                if matches!(
                    name,
                    ".git" | ".decapod" | "target" | "node_modules" | "dist" | "build"
                ) {
                    continue;
                }
                visit(&path, remaining, ctx);
                continue;
            }

            *remaining -= 1;
            let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("");
            match ext {
                "py" => {
                    ctx.primary_languages.push("python".to_string());
                    ctx.detected_surfaces.push("python".to_string());
                }
                "ts" | "tsx" => {
                    ctx.primary_languages.push("typescript".to_string());
                    ctx.detected_surfaces.push("typescript".to_string());
                }
                "js" | "jsx" | "mjs" | "cjs" => {
                    ctx.primary_languages.push("javascript".to_string());
                    ctx.detected_surfaces.push("javascript".to_string());
                }
                "go" => {
                    ctx.primary_languages.push("go".to_string());
                    ctx.detected_surfaces.push("go".to_string());
                }
                "rs" => {
                    ctx.primary_languages.push("rust".to_string());
                    ctx.detected_surfaces.push("rust".to_string());
                }
                "sh" | "bash" | "zsh" => {
                    ctx.primary_languages.push("shell".to_string());
                    ctx.detected_surfaces.push("shell".to_string());
                }
                _ => {}
            }
        }
    }

    let mut remaining = 512;
    visit(target_dir, &mut remaining, ctx);
}

fn read_seed_list_env(var: &str) -> Vec<String> {
    std::env::var(var)
        .ok()
        .map(|v| {
            v.split(',')
                .map(|s| s.trim().to_string())
                .filter(|s| !s.is_empty())
                .collect::<Vec<_>>()
        })
        .unwrap_or_default()
}

fn dedupe_sorted(list: &mut Vec<String>) {
    list.sort();
    list.dedup();
}

fn apply_substrate_adoption(ctx: &mut RepoContext, target_dir: &Path) {
    // Adoption: if OVERRIDE.md exists, it has the highest priority for docs.
    // Existing INTENT.md or README.md are lower priority than config.toml.

    // Check OVERRIDE.md (explicit user-defined override)
    if let Some(override_intent) = core::assets::get_override_doc(target_dir, "specs/INTENT")
        && let Some(summary) = core::project_specs::first_markdown_content_line(&override_intent)
    {
        ctx.product_summary = Some(summary);
    }

    // Fallback to existing generated spec ONLY if not already set by config.toml or OVERRIDE.md
    if ctx.product_summary.is_none() {
        let intent_path = target_dir.join(core::project_specs::LOCAL_PROJECT_SPECS_INTENT);
        if intent_path.exists()
            && let Ok(intent) = fs::read_to_string(intent_path)
            && let Some(summary) = core::project_specs::first_markdown_content_line(&intent)
        {
            ctx.product_summary = Some(summary);
        }
    }

    // README fallback if still none
    if ctx.product_summary.is_none() {
        let readme_path = target_dir.join("README.md");
        if readme_path.exists()
            && let Ok(readme) = fs::read_to_string(readme_path)
            && let Some(summary) = core::project_specs::first_markdown_content_line(&readme)
        {
            ctx.product_summary = Some(summary);
        }
    }

    // Architecture adoption (OVERRIDE.md wins, then config.toml, then ARCHITECTURE.md)
    if let Some(override_arch) = core::assets::get_override_doc(target_dir, "specs/ARCHITECTURE")
        && let Some(direction) = core::project_specs::first_markdown_content_line(&override_arch)
    {
        ctx.architecture_direction = Some(direction);
    }

    if ctx.architecture_direction.is_none() {
        let architecture_path =
            target_dir.join(core::project_specs::LOCAL_PROJECT_SPECS_ARCHITECTURE);
        if architecture_path.exists()
            && let Ok(arch) = fs::read_to_string(architecture_path)
            && let Some(direction) = core::project_specs::first_markdown_content_line(&arch)
        {
            ctx.architecture_direction = Some(direction);
        }
    }
}

fn apply_repo_context_env_overrides(ctx: &mut RepoContext) {
    if let Ok(v) = std::env::var("DECAPOD_INIT_PRODUCT_NAME") {
        let trimmed = v.trim();
        if !trimmed.is_empty() {
            ctx.product_name = Some(trimmed.to_string());
        }
    }
    if let Ok(v) = std::env::var("DECAPOD_INIT_PRODUCT_SUMMARY") {
        let trimmed = v.trim();
        if !trimmed.is_empty() {
            ctx.product_summary = Some(trimmed.to_string());
        }
    }
    if let Ok(v) = std::env::var("DECAPOD_INIT_ARCHITECTURE_DIRECTION") {
        let trimmed = v.trim();
        if !trimmed.is_empty() {
            ctx.architecture_direction = Some(trimmed.to_string());
        }
    }
    if let Ok(v) = std::env::var("DECAPOD_INIT_PRODUCT_TYPE") {
        let trimmed = v.trim();
        if !trimmed.is_empty() {
            ctx.product_type = Some(trimmed.to_string());
        }
    }
    if let Ok(v) = std::env::var("DECAPOD_INIT_DONE_CRITERIA") {
        let trimmed = v.trim();
        if !trimmed.is_empty() {
            ctx.done_criteria = Some(trimmed.to_string());
        }
    }
    if std::env::var("DECAPOD_INIT_PRIMARY_LANGUAGES").is_ok() {
        ctx.primary_languages = read_seed_list_env("DECAPOD_INIT_PRIMARY_LANGUAGES");
    }
    if std::env::var("DECAPOD_INIT_SURFACES").is_ok() {
        ctx.detected_surfaces = read_seed_list_env("DECAPOD_INIT_SURFACES");
    }
    dedupe_sorted(&mut ctx.primary_languages);
    dedupe_sorted(&mut ctx.detected_surfaces);
}

fn apply_repo_context_cli_overrides(ctx: &mut RepoContext, init_with: &InitWithCli) {
    if let Some(v) = init_with.product_name.as_ref() {
        let trimmed = v.trim();
        if !trimmed.is_empty() {
            ctx.product_name = Some(trimmed.to_string());
        }
    }
    if let Some(v) = init_with.product_summary.as_ref() {
        let trimmed = v.trim();
        if !trimmed.is_empty() {
            ctx.product_summary = Some(trimmed.to_string());
        }
    }
    if let Some(v) = init_with.architecture_direction.as_ref() {
        let trimmed = v.trim();
        if !trimmed.is_empty() {
            ctx.architecture_direction = Some(trimmed.to_string());
        }
    }
    if let Some(v) = init_with.product_type.as_ref() {
        let trimmed = v.trim();
        if !trimmed.is_empty() {
            ctx.product_type = Some(trimmed.to_string());
        }
    }
    if let Some(v) = init_with.done_criteria.as_ref() {
        let trimmed = v.trim();
        if !trimmed.is_empty() {
            ctx.done_criteria = Some(trimmed.to_string());
        }
    }
    if !init_with.primary_languages.is_empty() {
        ctx.primary_languages = init_with
            .primary_languages
            .iter()
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty())
            .collect();
    }
    if !init_with.detected_surfaces.is_empty() {
        ctx.detected_surfaces = init_with
            .detected_surfaces
            .iter()
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty())
            .collect();
    }
    dedupe_sorted(&mut ctx.primary_languages);
    dedupe_sorted(&mut ctx.detected_surfaces);
}

fn prompt_line(prompt: &str) -> Result<String, error::DecapodError> {
    print!("{}", prompt);
    io::stdout().flush().map_err(error::DecapodError::IoError)?;
    let mut buf = String::new();
    io::stdin()
        .read_line(&mut buf)
        .map_err(error::DecapodError::IoError)?;
    Ok(strip_ansi_escape_sequences(buf.trim()).trim().to_string())
}

fn strip_ansi_escape_sequences(input: &str) -> String {
    let mut out = String::with_capacity(input.len());
    let mut chars = input.chars().peekable();
    while let Some(ch) = chars.next() {
        if ch != '\x1b' {
            out.push(ch);
            continue;
        }
        if chars.peek() == Some(&'[') {
            chars.next();
            for next in chars.by_ref() {
                if ('@'..='~').contains(&next) {
                    break;
                }
            }
        }
    }
    out
}

const LANGUAGES: &[&str] = &[
    "Rust",
    "TypeScript",
    "JavaScript",
    "Python",
    "Go",
    "Java",
    "Kotlin",
    "Swift",
    "C",
    "C++",
    "C#",
    "Zig",
    "Ruby",
    "PHP",
    "Elixir",
    "Erlang",
    "Scala",
    "Clojure",
    "Dart",
    "Haskell",
    "OCaml",
    "F#",
    "Lua",
    "R",
    "Julia",
    "SQL",
    "HCL",
    "Shell",
    "PowerShell",
    "Other",
];

const ARCH_DIRECTIONS: &[(&str, &str)] = &[
    ("webapp", "Web application (TypeScript, React/Vue/Svelte)"),
    ("microservice", "Microservice (Go, Rust, or Java)"),
    ("library", "Library/SDK (language-agnostic)"),
    ("cli", "Command-line tool (Rust, Go, Python)"),
    ("lambda", "Lambda/Serverless (Python, TypeScript, Go)"),
    ("mobile-android", "Android (Kotlin, Java)"),
    ("mobile-ios", "iOS (Swift)"),
    ("multiarch", "Multi-platform (Rust, C/C++)"),
    ("infra", "Infrastructure/Terraform (HCL, Python)"),
    ("data", "Data pipeline (Python, SQL)"),
];

const DIAGRAM_NOTATION_OPTIONS: &[&str] = &["ascii", "mermaid"];
const DIAGRAM_NOTATION_DESCRIPTIONS: &[&str] = &[
    "ASCII/text blocks readable in terminals and plain Markdown",
    "Mermaid diagrams rendered by GitHub Markdown from readable text source",
];

fn normalize_language(input: &str) -> String {
    match input.trim().to_lowercase().as_str() {
        "ts" | "typescript" => "TypeScript".to_string(),
        "js" | "javascript" => "JavaScript".to_string(),
        "py" | "python" => "Python".to_string(),
        "rs" | "rust" => "Rust".to_string(),
        "golang" | "go" => "Go".to_string(),
        "kt" | "kotlin" => "Kotlin".to_string(),
        "swift" => "Swift".to_string(),
        "c" => "C".to_string(),
        "cpp" | "c++" | "cplusplus" => "C++".to_string(),
        "csharp" | "c#" => "C#".to_string(),
        "zig" => "Zig".to_string(),
        "rb" | "ruby" => "Ruby".to_string(),
        "php" => "PHP".to_string(),
        "ex" | "elixir" => "Elixir".to_string(),
        "erl" | "erlang" => "Erlang".to_string(),
        "scala" => "Scala".to_string(),
        "clj" | "clojure" => "Clojure".to_string(),
        "dart" => "Dart".to_string(),
        "hs" | "haskell" => "Haskell".to_string(),
        "ml" | "ocaml" => "OCaml".to_string(),
        "fs" | "fsharp" | "f#" => "F#".to_string(),
        "lua" => "Lua".to_string(),
        "r" => "R".to_string(),
        "jl" | "julia" => "Julia".to_string(),
        "sql" => "SQL".to_string(),
        "terraform" | "tf" | "hcl" => "HCL".to_string(),
        "bash" | "sh" | "shell" => "Shell".to_string(),
        "pwsh" | "powershell" => "PowerShell".to_string(),
        "other" => "Other".to_string(),
        _ => input.trim().to_string(),
    }
}

fn language_choice_seed(current: &[String], recommendation: &[String]) -> Vec<String> {
    if !current.is_empty() {
        return current.iter().map(|s| normalize_language(s)).collect();
    }
    recommendation
        .iter()
        .map(|s| normalize_language(s))
        .collect()
}

fn apply_architecture_language_recommendation(ctx: &mut RepoContext) {
    if !ctx.primary_languages.is_empty() {
        return;
    }
    if let Some(arch) = ctx.architecture_direction.as_deref() {
        ctx.primary_languages = infer_language_from_architecture(arch);
    }
}

struct TerminalModeGuard {
    saved_mode: String,
    tty: fs::File,
}

impl Drop for TerminalModeGuard {
    fn drop(&mut self) {
        if let Ok(tty) = self.tty.try_clone() {
            let _ = std::process::Command::new("stty")
                .arg(&self.saved_mode)
                .stdin(Stdio::from(tty))
                .status();
        }
        print!("\x1b[?25h");
        println!();
    }
}

fn enter_raw_terminal_mode() -> Option<(TerminalModeGuard, fs::File)> {
    let tty = fs::OpenOptions::new()
        .read(true)
        .write(true)
        .open("/dev/tty")
        .ok()?;
    let output = std::process::Command::new("stty")
        .arg("-g")
        .stdin(Stdio::from(tty.try_clone().ok()?))
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }
    let saved_mode = String::from_utf8(output.stdout).ok()?.trim().to_string();
    let status = std::process::Command::new("stty")
        .args(["raw", "-echo"])
        .stdin(Stdio::from(tty.try_clone().ok()?))
        .status()
        .ok()?;
    if !status.success() {
        return None;
    }
    let input = tty.try_clone().ok()?;
    Some((TerminalModeGuard { saved_mode, tty }, input))
}

fn terminal_selector_available(_default: &[String]) -> bool {
    io::stdin().is_terminal()
}

fn find_selector_match(options: &[&str], typed: &str) -> Option<usize> {
    let typed = typed.trim();
    if typed.is_empty() {
        return None;
    }
    if let Ok(index) = typed.parse::<usize>() {
        return options.get(index.saturating_sub(1)).map(|_| index - 1);
    }
    let typed = typed.to_lowercase();
    options
        .iter()
        .position(|option| option.eq_ignore_ascii_case(&typed))
        .or_else(|| {
            options
                .iter()
                .position(|option| option.to_lowercase().starts_with(&typed))
        })
}

fn selector_shown(options: &[&str], selected: usize, typed: &str) -> String {
    if typed.is_empty() {
        return options[selected].to_string();
    }
    find_selector_match(options, typed)
        .and_then(|index| options.get(index))
        .map(|option| (*option).to_string())
        .unwrap_or_else(|| typed.to_string())
}

fn selector_default_index(options: &[&str], default: &[String]) -> Option<usize> {
    default
        .first()
        .and_then(|d| options.iter().position(|o| d.eq_ignore_ascii_case(o)))
}

fn selector_render_lines(
    options: &[&str],
    descriptions: Option<&[&str]>,
    selected: usize,
    default_idx: Option<usize>,
    typed: &str,
    prompt: &str,
) -> Vec<String> {
    let shown = selector_shown(options, selected, typed);
    let input = if typed.is_empty() {
        shown.clone()
    } else {
        typed.to_string()
    };
    let mut lines = vec![format!("{prompt}{input}")];
    for (i, option) in options.iter().enumerate() {
        let cursor = if i == selected { ">" } else { " " };
        let marker = if default_idx == Some(i) { "✓" } else { " " };
        let suffix = descriptions
            .and_then(|items| items.get(i))
            .map(|description| format!(" -> {description}"))
            .unwrap_or_default();
        lines.push(format!(
            "    {cursor} {marker} {:>2}. {option}{suffix}",
            i + 1
        ));
    }
    lines
}

fn update_selector_from_byte(options: &[&str], selected: &mut usize, typed: &mut String, byte: u8) {
    match byte {
        8 | 127 => {
            typed.pop();
            if let Some(index) = find_selector_match(options, typed) {
                *selected = index;
            }
        }
        byte if byte.is_ascii_graphic() || byte == b' ' => {
            typed.push(byte as char);
            if let Some(index) = find_selector_match(options, typed) {
                *selected = index;
            }
        }
        _ => {}
    }
}

#[cfg(test)]
fn selector_result_for_input(options: &[&str], default: &[String], input: &[u8]) -> String {
    let mut selected = default
        .first()
        .and_then(|d| {
            options
                .iter()
                .position(|option| d.eq_ignore_ascii_case(option))
        })
        .unwrap_or(0);
    let mut typed = String::new();
    let mut index = 0;
    while index < input.len() {
        match input[index] {
            b'\r' | b'\n' => return selector_shown(options, selected, &typed),
            27 if input.get(index + 1) == Some(&b'[') => {
                match input.get(index + 2).copied() {
                    Some(b'A') => {
                        typed.clear();
                        selected = selected.checked_sub(1).unwrap_or_else(|| options.len() - 1);
                    }
                    Some(b'B') => {
                        typed.clear();
                        selected = (selected + 1) % options.len();
                    }
                    _ => {}
                }
                index += 3;
                continue;
            }
            byte => update_selector_from_byte(options, &mut selected, &mut typed, byte),
        }
        index += 1;
    }
    selector_shown(options, selected, &typed)
}

#[cfg(test)]
fn selector_render_for_input(
    options: &[&str],
    descriptions: Option<&[&str]>,
    default: &[String],
    input: &[u8],
) -> Vec<String> {
    let mut selected = selector_default_index(options, default).unwrap_or(0);
    let mut typed = String::new();
    let mut index = 0;
    while index < input.len() {
        match input[index] {
            b'\r' | b'\n' => break,
            27 if input.get(index + 1) == Some(&b'[') => {
                match input.get(index + 2).copied() {
                    Some(b'A') => {
                        typed.clear();
                        selected = selected.checked_sub(1).unwrap_or_else(|| options.len() - 1);
                    }
                    Some(b'B') => {
                        typed.clear();
                        selected = (selected + 1) % options.len();
                    }
                    _ => {}
                }
                index += 3;
                continue;
            }
            byte => update_selector_from_byte(options, &mut selected, &mut typed, byte),
        }
        index += 1;
    }
    selector_render_lines(
        options,
        descriptions,
        selected,
        selector_default_index(options, default),
        &typed,
        "    choice: ",
    )
}

fn prompt_select_fallback(
    options: &[&str],
    default: &[String],
    prompt: &str,
) -> Result<Option<String>, error::DecapodError> {
    if options.is_empty() {
        return Ok(None);
    }
    let default_idx = default
        .first()
        .and_then(|d| options.iter().position(|o| d.eq_ignore_ascii_case(o)));
    for (i, opt) in options.iter().enumerate() {
        let marker = if default_idx == Some(i) { "✓" } else { " " };
        println!("    {} {:>2}. {}", marker, i + 1, opt);
    }
    let default_val = default_idx.map(|i| i + 1).unwrap_or(1);
    loop {
        println!();
        let line = prompt_line(&format!(
            "{prompt}[1-{}, default={default_val}]: ",
            options.len()
        ))?;
        let trimmed = line.trim();
        if trimmed.is_empty() {
            return default_idx
                .map(|i| Ok(Some(options[i].to_string())))
                .unwrap_or(Ok(None));
        }
        if let Ok(n) = trimmed.parse::<usize>()
            && (1..=options.len()).contains(&n)
        {
            return Ok(Some(options[n - 1].to_string()));
        }
        if let Some(pos) = options.iter().position(|o| o.eq_ignore_ascii_case(trimmed)) {
            return Ok(Some(options[pos].to_string()));
        }
        println!(
            "    Invalid choice. Enter a number (1-{}) or name.",
            options.len()
        );
    }
}

fn prompt_terminal_selector(
    options: &[&str],
    descriptions: Option<&[&str]>,
    default: &[String],
    prompt: &str,
) -> Result<Option<String>, error::DecapodError> {
    if options.is_empty() {
        return Ok(None);
    }
    if !io::stdin().is_terminal() {
        return prompt_select_fallback(options, default, prompt);
    }
    let mut selected = selector_default_index(options, default).unwrap_or(0);
    let default_idx = selector_default_index(options, default);
    let Some((_guard, mut input)) = enter_raw_terminal_mode() else {
        return prompt_select_fallback(options, default, prompt);
    };
    let mut typed = String::new();
    let mut rendered_lines = 0;
    print!("\x1b[?25l");
    loop {
        if rendered_lines > 0 {
            print!("\x1b[{rendered_lines}F");
        }
        let lines =
            selector_render_lines(options, descriptions, selected, default_idx, &typed, prompt);
        for line in &lines {
            print!("\r\x1b[K{line}\n");
        }
        rendered_lines = lines.len();
        io::stdout().flush().map_err(error::DecapodError::IoError)?;

        let mut byte = [0_u8; 1];
        input
            .read_exact(&mut byte)
            .map_err(error::DecapodError::IoError)?;
        match byte[0] {
            b'\r' | b'\n' => {
                let shown = selector_shown(options, selected, &typed);
                print!("\x1b[{rendered_lines}F");
                print!("\r\x1b[K{prompt}{shown}\n");
                for _ in 1..rendered_lines {
                    print!("\r\x1b[K\n");
                }
                print!(
                    "\x1b[{lines_up}F",
                    lines_up = rendered_lines.saturating_sub(1)
                );
                io::stdout().flush().map_err(error::DecapodError::IoError)?;
                return Ok(Some(shown));
            }
            3 => {
                return Err(error::DecapodError::ValidationError(
                    "init prompt interrupted".to_string(),
                ));
            }
            8 | 127 => {
                update_selector_from_byte(options, &mut selected, &mut typed, byte[0]);
            }
            27 => {
                let mut seq = [0_u8; 2];
                if input.read_exact(&mut seq).is_ok() && seq[0] == b'[' {
                    match seq[1] {
                        b'A' => {
                            typed.clear();
                            selected = selected.checked_sub(1).unwrap_or_else(|| options.len() - 1);
                        }
                        b'B' => {
                            typed.clear();
                            selected = (selected + 1) % options.len();
                        }
                        _ => {}
                    }
                }
            }
            byte => update_selector_from_byte(options, &mut selected, &mut typed, byte),
        }
    }
}

fn prompt_language_choice(
    current: &[String],
    recommendation: &[String],
) -> Result<Vec<String>, error::DecapodError> {
    use crate::core::ansi::AnsiExt;
    let inferred = if current.is_empty() {
        "None".to_string()
    } else {
        current.join(", ")
    };
    let default = language_choice_seed(current, recommendation);
    let default_label = if default.is_empty() {
        "None".to_string()
    } else {
        default.join(", ")
    };
    let recommendation_label = if recommendation.is_empty() {
        "None".to_string()
    } else {
        recommendation
            .iter()
            .map(|s| normalize_language(s))
            .collect::<Vec<_>>()
            .join(", ")
    };
    let supports_arrows = terminal_selector_available(&default);

    println!();
    println!("{}", "  Primary language(s)".bright_white().bold());
    println!("    inferred from files: {}", inferred);
    println!("    recommended for architecture: {}", recommendation_label);
    println!("    current selection/default: {}", default_label);
    if supports_arrows {
        println!("    options: up/down, type name or number, comma-separated for multiple");
    } else {
        println!("    options: type name or number, comma-separated for multiple");
    }
    println!("    press Enter to use the current selection/default");

    let choice = match prompt_terminal_selector(LANGUAGES, None, &default, "    choice: ")? {
        Some(choice) => choice,
        None => prompt_line("    choice: ")?,
    };

    if choice.is_empty() {
        return Ok(default);
    }

    Ok(parse_language_choice(&choice))
}

fn parse_language_choice(choice: &str) -> Vec<String> {
    choice
        .split(',')
        .map(|s| {
            let trimmed = s.trim();
            trimmed
                .parse::<usize>()
                .ok()
                .and_then(|n| LANGUAGES.get(n.saturating_sub(1)))
                .map(|lang| (*lang).to_string())
                .unwrap_or_else(|| normalize_language(trimmed))
        })
        .filter(|s| !s.is_empty())
        .collect()
}

fn infer_language_from_architecture(arch: &str) -> Vec<String> {
    let arch = arch.to_lowercase();
    match arch.as_str() {
        "webapp" => vec!["TypeScript".to_string()],
        "microservice" => vec!["Go".to_string()],
        "library" => vec!["Rust".to_string()],
        "cli" => vec!["Rust".to_string()],
        "lambda" => vec!["Python".to_string()],
        "mobile-android" => vec!["Kotlin".to_string()],
        "mobile-ios" => vec!["Swift".to_string()],
        "multiarch" => vec!["Rust".to_string()],
        "infra" => vec!["HCL".to_string()],
        "data" => vec!["Python".to_string()],
        _ if arch.contains("web") || arch.contains("frontend") => vec!["TypeScript".to_string()],
        _ if arch.contains("microservice") || arch.contains("backend") || arch.contains("api") => {
            vec!["Go".to_string()]
        }
        _ if arch.contains("cli") || arch.contains("command-line") => vec!["Rust".to_string()],
        _ if arch.contains("serverless") || arch.contains("lambda") => vec!["Python".to_string()],
        _ if arch.contains("android") => vec!["Kotlin".to_string()],
        _ if arch.contains("ios") => vec!["Swift".to_string()],
        _ if arch.contains("embedded") || arch.contains("systems") => vec!["Zig".to_string()],
        _ if arch.contains("infra") || arch.contains("terraform") => vec!["HCL".to_string()],
        _ if arch.contains("data") || arch.contains("ml") => vec!["Python".to_string()],
        _ => vec![],
    }
}

fn prompt_architecture_choice(
    current: Option<&str>,
) -> Result<Option<String>, error::DecapodError> {
    use crate::core::ansi::AnsiExt;
    let inferred = current.unwrap_or("None");
    let current_matches_common = current.is_some_and(|c| {
        ARCH_DIRECTIONS
            .iter()
            .any(|(arch, _)| c.eq_ignore_ascii_case(arch))
    });
    let default = if current_matches_common {
        current.map(|s| vec![s.to_string()]).unwrap_or_default()
    } else {
        Vec::new()
    };
    let supports_arrows = terminal_selector_available(&default);

    println!();
    println!("{}", "  Architecture".bright_white().bold());
    println!("    inferred: {}", inferred);
    println!("    current selection/default: {}", inferred);
    println!("    common approaches:");
    if supports_arrows {
        println!("    options: up/down, type name or number, or type your architecture");
    } else {
        println!("    options: type name or number, or type your architecture");
    }

    let arch_options = ARCH_DIRECTIONS
        .iter()
        .map(|(arch, _)| *arch)
        .collect::<Vec<_>>();
    let arch_descriptions = ARCH_DIRECTIONS
        .iter()
        .map(|(_, description)| *description)
        .collect::<Vec<_>>();
    let choice = match prompt_terminal_selector(
        &arch_options,
        Some(&arch_descriptions),
        &default,
        "    choice: ",
    )? {
        Some(choice) => choice,
        None => prompt_line("    choice: ")?,
    };

    if choice.is_empty() {
        return Ok(current.map(|s| s.to_string()));
    }

    if let Ok(index) = choice.parse::<usize>()
        && let Some((arch, _)) = ARCH_DIRECTIONS.get(index.saturating_sub(1))
    {
        return Ok(Some((*arch).to_string()));
    }

    Ok(Some(choice.trim().to_string()))
}

fn print_init_block(title: &str, subtitle: &str) {
    use crate::core::ansi::AnsiExt;
    println!();
    println!("{}", format!("◢ {}", title).bright_cyan().bold());
    println!("{}", format!("  {}", subtitle).bright_black());
}

fn prompt_text_field(
    label: &str,
    helper: &str,
    default_value: &str,
) -> Result<String, error::DecapodError> {
    use crate::core::ansi::AnsiExt;
    println!();
    println!("{}", format!("  {}", label).bright_white().bold());
    println!("{}", format!("    {}", helper).bright_black());
    println!(
        "{}",
        format!("    inferred: {}", default_value).bright_black()
    );
    let line = prompt_line(&format!("{}", "    input: ".bright_cyan().bold()))?;
    if line.trim().is_empty() {
        Ok(default_value.to_string())
    } else {
        Ok(line)
    }
}

fn prompt_line_default(prompt: &str, default_value: &str) -> Result<String, error::DecapodError> {
    prompt_text_field(
        prompt,
        "Press Enter to keep inferred context.",
        default_value,
    )
}

fn prompt_yes_no(prompt: &str, default_yes: bool) -> Result<bool, error::DecapodError> {
    use crate::core::ansi::AnsiExt;
    let suffix = if default_yes { "[Y/n]" } else { "[y/N]" };
    println!();
    println!("{}", format!("  {}", prompt).bright_white().bold());
    let line = prompt_line(&format!(
        "{} {} ",
        "    choice:".bright_cyan().bold(),
        suffix.bright_black()
    ))?;
    if line.is_empty() {
        return Ok(default_yes);
    }
    let normalized = line.to_ascii_lowercase();
    Ok(matches!(normalized.as_str(), "y" | "yes"))
}

fn resolve_existing_init_dir(raw: &Path) -> Result<PathBuf, error::DecapodError> {
    std::fs::canonicalize(raw).map_err(error::DecapodError::IoError)
}

fn resolve_or_create_project_dir(
    current_dir: &Path,
    raw: &Path,
    dry_run: bool,
) -> Result<PathBuf, error::DecapodError> {
    let candidate = if raw.is_absolute() {
        raw.to_path_buf()
    } else {
        current_dir.join(raw)
    };
    if candidate.exists() && !candidate.is_dir() {
        return Err(error::DecapodError::ValidationError(format!(
            "project directory target '{}' exists but is not a directory",
            candidate.display()
        )));
    }
    if !dry_run {
        std::fs::create_dir_all(&candidate).map_err(error::DecapodError::IoError)?;
    }
    if candidate.exists() {
        std::fs::canonicalize(&candidate).map_err(error::DecapodError::IoError)
    } else {
        Ok(candidate)
    }
}

fn prompt_init_target_dir(current_dir: &Path) -> Result<PathBuf, error::DecapodError> {
    if prompt_yes_no("Initialize the existing current directory?", true)? {
        return resolve_existing_init_dir(current_dir);
    }
    let project_name = prompt_text_field(
        "Project directory name",
        "Decapod will create this directory and initialize inside it.",
        "my-project",
    )?;
    let project_name = project_name.trim();
    if project_name.is_empty() {
        return Err(error::DecapodError::ValidationError(
            "Project directory name cannot be empty".to_string(),
        ));
    }
    resolve_or_create_project_dir(current_dir, Path::new(project_name), false)
}

fn diagram_style_choice(style: InitDiagramStyle) -> &'static str {
    match style {
        InitDiagramStyle::Ascii => "ascii",
        InitDiagramStyle::Mermaid => "mermaid",
    }
}

fn diagram_style_label(style: InitDiagramStyle) -> &'static str {
    match style {
        InitDiagramStyle::Ascii => "ascii/text",
        InitDiagramStyle::Mermaid => "mermaid",
    }
}

fn parse_diagram_style_choice(
    raw: &str,
    default_style: InitDiagramStyle,
) -> Result<InitDiagramStyle, error::DecapodError> {
    let choice = raw.trim();
    if choice.is_empty() {
        return Ok(default_style);
    }

    match choice.to_ascii_lowercase().as_str() {
        "1" | "ascii" | "ascii/text" | "ascii-text" | "text" | "plain" | "plain-text"
        | "plaintext" => Ok(InitDiagramStyle::Ascii),
        "2" | "mermaid" | "mmd" => Ok(InitDiagramStyle::Mermaid),
        _ => Err(error::DecapodError::ValidationError(
            "Invalid diagram notation; expected ascii/text or mermaid".to_string(),
        )),
    }
}

fn prompt_diagram_style(
    default_style: InitDiagramStyle,
) -> Result<InitDiagramStyle, error::DecapodError> {
    print_init_block(
        "Diagram Notation",
        "Choose the README-visible diagram notation Decapod should generate.",
    );
    println!(
        "    current selection/default: {}",
        diagram_style_label(default_style).bright_white()
    );
    println!("    options: up/down, type name or number");
    let default = vec![diagram_style_choice(default_style).to_string()];
    let choice = prompt_terminal_selector(
        DIAGRAM_NOTATION_OPTIONS,
        Some(DIAGRAM_NOTATION_DESCRIPTIONS),
        &default,
        "    choice: ",
    )?
    .unwrap_or_else(|| String::from(diagram_style_choice(default_style)));

    parse_diagram_style_choice(&choice, default_style)
}

fn init_with_from_config(
    config: &DecapodProjectConfig,
    target_dir: PathBuf,
    force: bool,
    dry_run: bool,
) -> InitWithCli {
    let has = |name: &str| config.init.entrypoints.iter().any(|e| e == name);
    let all_entrypoints =
        has("AGENTS.md") && has("CLAUDE.md") && has("GEMINI.md") && has("CODEX.md");
    InitWithCli {
        dir: Some(target_dir),
        project_dir: None,
        force,
        dry_run,
        all: all_entrypoints,
        claude: has("CLAUDE.md"),
        gemini: has("GEMINI.md"),
        agents: has("AGENTS.md"),
        specs: config.init.specs,
        diagram_style: config.init.diagram_style,
        product_name: config.repo.product_name.clone(),
        product_summary: config.repo.product_summary.clone(),
        architecture_direction: config.repo.architecture_direction.clone(),
        product_type: config.repo.product_type.clone(),
        done_criteria: config.repo.done_criteria.clone(),
        primary_languages: config.repo.primary_languages.clone(),
        detected_surfaces: config.repo.detected_surfaces.clone(),
    }
}

fn config_from_init_with(init: &InitWithCli, repo: RepoContext) -> DecapodProjectConfig {
    let mut entrypoints = Vec::new();
    let no_entrypoint_flags = !init.claude && !init.gemini && !init.agents;
    if init.all || init.agents || no_entrypoint_flags {
        entrypoints.push("AGENTS.md".to_string());
    }
    if init.all || init.claude || (!init.gemini && !init.agents) {
        entrypoints.push("CLAUDE.md".to_string());
    }
    if init.all || init.gemini || (!init.claude && !init.agents) {
        entrypoints.push("GEMINI.md".to_string());
    }
    if init.all || no_entrypoint_flags {
        entrypoints.push("CODEX.md".to_string());
    }
    DecapodProjectConfig {
        schema_version: "1.0.0".to_string(),
        init: InitConfigSection {
            specs: init.specs,
            diagram_style: init.diagram_style,
            entrypoints,
        },
        repo,
    }
}

fn interactive_init_with(
    config: &DecapodProjectConfig,
    target_dir: PathBuf,
    force: bool,
    dry_run: bool,
) -> Result<InitWithCli, error::DecapodError> {
    print_init_block(
        "Decapod Setup",
        "Existing .decapod/config.toml detected. Confirm your setup profile.",
    );
    let mut next = init_with_from_config(config, target_dir, force, dry_run);
    if config.init.entrypoints.is_empty() {
        let all_entrypoints = prompt_yes_no(
            "Include all default agent entrypoints (AGENTS/CLAUDE/GEMINI/CODEX)?",
            true,
        )?;
        if all_entrypoints {
            next.all = true;
            next.agents = true;
            next.claude = true;
            next.gemini = true;
        }
    }
    if config.init.specs {
        next.specs = true;
    }
    next.diagram_style = prompt_diagram_style(next.diagram_style)?;
    Ok(next)
}

fn enrich_repo_context_interactive(repo: &mut RepoContext) -> Result<(), error::DecapodError> {
    print_init_block(
        "Repository Context",
        "Review inferred intent before generating .decapod/generated/specs/.",
    );

    let current_summary = repo.product_summary.clone().unwrap_or_else(|| {
        "Deliver the repository outcome against explicit user intent with proof-backed completion."
            .to_string()
    });
    repo.product_summary = Some(prompt_line_default("Intent outcome", &current_summary)?);

    repo.architecture_direction =
        prompt_architecture_choice(repo.architecture_direction.as_deref())?;

    let recommended_languages = repo
        .architecture_direction
        .as_deref()
        .map(infer_language_from_architecture)
        .unwrap_or_default();
    repo.primary_languages =
        prompt_language_choice(&repo.primary_languages, &recommended_languages)?;

    let refine_now = prompt_yes_no(
        "Refine done criteria now? (You can evolve .decapod/config.toml and .decapod/generated/specs/*.md later.)",
        false,
    )?;
    if refine_now {
        let current_done = repo.done_criteria.clone().unwrap_or_else(|| {
            "Decapod validate passes, required tests pass, and promotion-relevant artifacts are present."
                .to_string()
        });
        repo.done_criteria = Some(prompt_line_default("Done criteria", &current_done)?);
    }
    Ok(())
}

fn run_init_apply(
    init_with: &InitWithCli,
    current_dir: &Path,
    repo_ctx: &RepoContext,
) -> Result<PathBuf, error::DecapodError> {
    let target_dir = match &init_with.dir {
        Some(d) => d.clone(),
        None => current_dir.to_path_buf(),
    };
    let target_dir = if target_dir.exists() {
        std::fs::canonicalize(&target_dir).map_err(error::DecapodError::IoError)?
    } else {
        target_dir
    };

    let setup_decapod_root = target_dir.join(".decapod");
    if setup_decapod_root.exists() && !init_with.force {
        use crate::core::ansi::AnsiExt;
        println!(
            "{} {}",
            "init:".bright_yellow(),
            "already initialized (.decapod exists); rerun with --force, or use `decapod init with --force`"
                .bright_red()
        );
        return Ok(target_dir);
    }

    use sha2::{Digest, Sha256};
    let mut existing_agent_files = vec![];
    for file in ["AGENTS.md", "CLAUDE.md", "GEMINI.md", "CODEX.md"] {
        if target_dir.join(file).exists() {
            existing_agent_files.push(file);
        }
    }

    let mut created_backups = false;
    let mut backup_count = 0usize;
    if !init_with.dry_run {
        for file in &existing_agent_files {
            let path = target_dir.join(file);
            let template_content = core::assets::get_template(file).unwrap_or_default();
            let mut hasher = Sha256::new();
            hasher.update(template_content.as_bytes());
            let template_hash = format!("{:x}", hasher.finalize());
            let existing_content = fs::read_to_string(&path).unwrap_or_default();
            let mut hasher = Sha256::new();
            hasher.update(existing_content.as_bytes());
            let existing_hash = format!("{:x}", hasher.finalize());
            if template_hash != existing_hash {
                created_backups = true;
                backup_count += 1;
                let backup_path = target_dir.join(format!("{}.bak", file));
                fs::rename(&path, &backup_path).map_err(error::DecapodError::IoError)?;
            }
        }
    }

    if !init_with.dry_run {
        scaffold::blend_legacy_entrypoints(&target_dir)?;
    }

    let mut agent_files_to_generate = if init_with.claude || init_with.gemini || init_with.agents {
        let mut files = vec![];
        if init_with.claude {
            files.push("CLAUDE.md".to_string());
        }
        if init_with.gemini {
            files.push("GEMINI.md".to_string());
        }
        if init_with.agents {
            files.push("AGENTS.md".to_string());
        }
        files
    } else {
        existing_agent_files
            .into_iter()
            .map(|s| s.to_string())
            .collect()
    };

    if !agent_files_to_generate.is_empty()
        && !agent_files_to_generate.iter().any(|f| f == "AGENTS.md")
    {
        agent_files_to_generate.push("AGENTS.md".to_string());
    }

    let scaffold_summary = scaffold::scaffold_project_entrypoints(&scaffold::ScaffoldOptions {
        target_dir: target_dir.clone(),
        force: init_with.force,
        dry_run: init_with.dry_run,
        agent_files: agent_files_to_generate,
        created_backups,
        all: init_with.all,
        generate_specs: init_with.specs,
        diagram_style: match init_with.diagram_style {
            InitDiagramStyle::Ascii => scaffold::DiagramStyle::Ascii,
            InitDiagramStyle::Mermaid => scaffold::DiagramStyle::Mermaid,
        },
        specs_seed: Some(scaffold::SpecsSeed {
            product_name: repo_ctx.product_name.clone(),
            product_summary: repo_ctx.product_summary.clone(),
            architecture_direction: repo_ctx.architecture_direction.clone(),
            product_type: repo_ctx.product_type.clone(),
            primary_languages: repo_ctx.primary_languages.clone(),
            detected_surfaces: repo_ctx.detected_surfaces.clone(),
            done_criteria: repo_ctx.done_criteria.clone(),
        }),
    })?;

    let target_display = setup_decapod_root
        .parent()
        .unwrap_or(current_dir)
        .display()
        .to_string();
    use crate::core::ansi::AnsiExt;
    print_init_block(
        "Decapod Init Summary",
        "Scaffold completed with the following changes.",
    );
    println!("  Target: {}", target_display.bright_white());
    println!(
        "  Mode: {}",
        if init_with.dry_run {
            "Dry Run".bright_yellow()
        } else {
            "Apply".bright_green()
        }
    );
    println!(
        "  Entrypoints: created={}, unchanged={}, preserved={}",
        scaffold_summary
            .entrypoints_created
            .to_string()
            .bright_green(),
        scaffold_summary
            .entrypoints_unchanged
            .to_string()
            .bright_yellow(),
        scaffold_summary
            .entrypoints_preserved
            .to_string()
            .bright_white()
    );
    println!(
        "  Config: created={}, unchanged={}, preserved={}",
        scaffold_summary.config_created.to_string().bright_green(),
        scaffold_summary
            .config_unchanged
            .to_string()
            .bright_yellow(),
        scaffold_summary.config_preserved.to_string().bright_white()
    );
    println!(
        "  Specs: created={}, unchanged={}, preserved={}",
        scaffold_summary.specs_created.to_string().bright_green(),
        scaffold_summary.specs_unchanged.to_string().bright_yellow(),
        scaffold_summary.specs_preserved.to_string().bright_white()
    );
    println!("  Backups: {}", backup_count.to_string().bright_magenta());
    println!(
        "  Diagram Notation: {}",
        diagram_style_label(init_with.diagram_style).bright_white()
    );
    println!(
        "{} {}",
        "✓".bright_green().bold(),
        "Ready".bright_green().bold()
    );

    Ok(target_dir)
}

pub fn run() -> Result<(), error::DecapodError> {
    let cli = Cli::parse();
    let argv: Vec<String> = std::env::args().skip(1).collect();
    let current_dir = std::env::current_dir()?;
    let decapod_root_option = find_decapod_project_root(&current_dir);
    let store_root: PathBuf;

    match cli.command {
        Command::Version => {
            // Version command - simple output for scripts/parsing
            println!("v{}", migration::DECAPOD_VERSION);
            return Ok(());
        }
        Command::Init(init_group) => {
            let base_init_invocation = init_group.command.is_none();
            let init_with = match init_group.command {
                Some(InitCommand::Clean { dir }) => {
                    clean_project(dir)?;
                    return Ok(());
                }
                Some(InitCommand::With(with)) => with,
                None => {
                    if init_group.dir.is_some() && init_group.project_dir.is_some() {
                        return Err(error::DecapodError::ValidationError(
                            "Use either --dir for an existing directory or --project-dir to create/select a project directory, not both.".to_string(),
                        ));
                    }
                    let target = if let Some(project_dir) = init_group.project_dir.as_ref() {
                        resolve_or_create_project_dir(
                            &current_dir,
                            project_dir,
                            init_group.dry_run,
                        )?
                    } else if let Some(dir) = init_group.dir.as_ref() {
                        resolve_existing_init_dir(dir)?
                    } else if io::stdin().is_terminal() {
                        prompt_init_target_dir(&current_dir)?
                    } else {
                        resolve_existing_init_dir(&current_dir)?
                    };
                    let maybe_cfg = load_project_config_if_present(&target)?;
                    if let Some(cfg) = maybe_cfg {
                        let mut with = if io::stdin().is_terminal() {
                            interactive_init_with(
                                &cfg,
                                target.clone(),
                                init_group.force,
                                init_group.dry_run,
                            )?
                        } else {
                            init_with_from_config(
                                &cfg,
                                target.clone(),
                                init_group.force,
                                init_group.dry_run,
                            )
                        };
                        // Keep base command flags as explicit runtime overrides.
                        if init_group.all {
                            with.all = true;
                            with.agents = true;
                            with.claude = true;
                            with.gemini = true;
                        }
                        if init_group.agents {
                            with.agents = true;
                        }
                        if init_group.claude {
                            with.claude = true;
                        }
                        if init_group.gemini {
                            with.gemini = true;
                        }
                        if init_group.product_name.is_some() {
                            with.product_name = init_group.product_name.clone();
                        }
                        if init_group.product_summary.is_some() {
                            with.product_summary = init_group.product_summary.clone();
                        }
                        if init_group.architecture_direction.is_some() {
                            with.architecture_direction = init_group.architecture_direction.clone();
                        }
                        if init_group.product_type.is_some() {
                            with.product_type = init_group.product_type.clone();
                        }
                        if init_group.done_criteria.is_some() {
                            with.done_criteria = init_group.done_criteria.clone();
                        }
                        if !init_group.primary_languages.is_empty() {
                            with.primary_languages = init_group.primary_languages.clone();
                        }
                        if !init_group.detected_surfaces.is_empty() {
                            with.detected_surfaces = init_group.detected_surfaces.clone();
                        }
                        with
                    } else {
                        let diagram_style = if io::stdin().is_terminal() {
                            prompt_diagram_style(init_group.diagram_style)?
                        } else {
                            init_group.diagram_style
                        };
                        InitWithCli {
                            dir: Some(target),
                            project_dir: None,
                            force: init_group.force,
                            dry_run: init_group.dry_run,
                            all: init_group.all,
                            claude: init_group.claude,
                            gemini: init_group.gemini,
                            agents: init_group.agents,
                            specs: true,
                            diagram_style,
                            product_name: init_group.product_name.clone(),
                            product_summary: init_group.product_summary.clone(),
                            architecture_direction: init_group.architecture_direction.clone(),
                            product_type: init_group.product_type.clone(),
                            done_criteria: init_group.done_criteria.clone(),
                            primary_languages: init_group.primary_languages.clone(),
                            detected_surfaces: init_group.detected_surfaces.clone(),
                        }
                    }
                }
            };

            if init_with.dir.is_some() && init_with.project_dir.is_some() {
                return Err(error::DecapodError::ValidationError(
                    "Use either --dir for an existing directory or --project-dir to create/select a project directory, not both.".to_string(),
                ));
            }
            let init_target = if let Some(project_dir) = init_with.project_dir.as_ref() {
                resolve_or_create_project_dir(&current_dir, project_dir, init_with.dry_run)?
            } else if let Some(dir) = init_with.dir.as_ref() {
                resolve_existing_init_dir(dir)?
            } else {
                resolve_existing_init_dir(&current_dir)?
            };
            let mut init_with = init_with;
            init_with.dir = Some(init_target.clone());
            init_with.project_dir = None;
            let mut repo_ctx = infer_repo_context(&init_target);
            apply_repo_context_env_overrides(&mut repo_ctx);
            apply_repo_context_cli_overrides(&mut repo_ctx, &init_with);
            apply_substrate_adoption(&mut repo_ctx, &init_target);
            apply_architecture_language_recommendation(&mut repo_ctx);
            if base_init_invocation && io::stdin().is_terminal() {
                enrich_repo_context_interactive(&mut repo_ctx)?;
            }
            let target_dir = run_init_apply(&init_with, &current_dir, &repo_ctx)?;
            let config = config_from_init_with(&init_with, repo_ctx);
            write_project_config(&target_dir, &config, init_with.dry_run)?;
            seed_init_generated_state(&target_dir, init_with.dry_run)?;
        }
        Command::Session(session_cli) => {
            run_session_command(session_cli)?;
        }
        Command::Release(release_cli) => {
            let project_root = decapod_root_option?;
            run_release_command(release_cli, &project_root)?;
        }
        Command::Setup(setup_cli) => match setup_cli.command {
            SetupCommand::Hook {
                commit_msg,
                pre_commit,
                uninstall,
            } => {
                run_hook_install(commit_msg, pre_commit, uninstall)?;
            }
        },
        _ => {
            let project_root = decapod_root_option?;
            let is_validate_cmd = matches!(&cli.command, Command::Validate(_));
            if requires_session_token(&cli.command) {
                ensure_session_valid()?;
            }
            enforce_worktree_requirement(&cli.command, &project_root)?;

            // For other commands, ensure .decapod exists
            let decapod_root_path = project_root.join(".decapod");
            store_root = decapod_root_path.join("data");
            std::fs::create_dir_all(&store_root).map_err(error::DecapodError::IoError)?;
            if should_route_via_group_broker(&cli.command, &argv) {
                match core::group_broker::maybe_route_mutation(&store_root, &argv) {
                    Err(e) => {
                        if !core::group_broker::is_internal_invocation() {
                            return Err(e);
                        }
                    }
                    Ok(routed) if routed && !core::group_broker::is_internal_invocation() => {
                        // Routed mutation completed via broker path.
                        return Ok(());
                    }
                    Ok(routed) => {
                        if !routed
                            && !core::group_broker::is_internal_invocation()
                            && enforce_route_strict_mode()
                        {
                            return Err(error::DecapodError::ValidationError(
                                "BROKER_ROUTE_REQUIRED: routed mutator cannot bypass broker in strict mode"
                                    .to_string(),
                            ));
                        }
                    }
                }
            }

            // Check for version/schema changes and run protected migrations if needed.
            // Backups are auto-created in .decapod/data only when schema upgrades are pending.
            let migration_result =
                migration::check_and_migrate_with_backup(&decapod_root_path, |data_root| {
                    subsystems::initialize_all_dbs(data_root)
                });
            match migration_result {
                Ok(()) => {}
                Err(e) if is_validate_cmd => {
                    let normalized = normalize_validate_error(e);
                    return Err(attach_validate_diagnostic_if_enabled(
                        normalized,
                        &project_root,
                        0,
                        validate_timeout_secs(),
                    ));
                }
                Err(e) => return Err(e),
            }

            // Best-effort hygiene: routinely scrub stale git worktree metadata/config.
            // This must not block primary command execution.
            if let Err(e) = workspace::prune_stale_worktree_config(&project_root)
                && !is_not_git_repository_error(&e)
            {
                eprintln!("warn: worktree maintenance skipped: {e}");
            }

            let project_store = Store {
                kind: StoreKind::Repo,
                root: store_root.clone(),
            };

            if should_auto_clock_in(&cli.command)
                && let Err(e) =
                    retry_transient_sqlite(|| todo::clock_in_agent_presence(&project_store), 4)
            {
                if is_transient_sqlite_contention_error(&e) {
                    eprintln!(
                        "warn: presence clock-in skipped due transient sqlite contention: {e}"
                    );
                } else {
                    return Err(e);
                }
            }

            match cli.command {
                Command::Activate => {
                    println!("decapod.activate: ok");
                }
                Command::Validate(validate_cli) => {
                    run_validate_command(validate_cli, &project_root, &project_store)?;
                }
                Command::Version => show_version_info()?,
                Command::Docs(docs_cli) => {
                    let result = docs_cli::run_docs_cli(docs_cli)?;
                    if result.ingested_core_constitution {
                        mark_core_constitution_ingested(&project_root, "docs.ingest")?;
                    }
                }
                Command::Todo(todo_cli) => todo::run_todo_cli(&project_store, todo_cli)?,
                Command::Obligation(obligation_cli) => {
                    obligation::run_obligation_cli(&project_store, obligation_cli)?
                }
                Command::Govern(govern_cli) => {
                    run_govern_command(govern_cli, &project_store, &store_root)?;
                }
                Command::Data(data_cli) => {
                    run_data_command(data_cli, &project_store, &project_root, &store_root)?;
                }
                Command::Auto(auto_cli) => run_auto_command(auto_cli, &project_store)?,
                Command::Qa(qa_cli) => run_qa_command(qa_cli, &project_store, &project_root)?,
                Command::Decide(decide_cli) => decide::run_decide_cli(&project_store, decide_cli)?,
                Command::Workspace(workspace_cli) => {
                    run_workspace_command(workspace_cli, &project_root)?;
                }
                Command::Rpc(rpc_cli) => {
                    run_rpc_command(rpc_cli, &project_root)?;
                }
                Command::Handshake(handshake_cli) => {
                    run_handshake_command(handshake_cli, &project_root)?;
                }
                Command::Release(release_cli) => {
                    run_release_command(release_cli, &project_root)?;
                }
                Command::Capabilities(cap_cli) => {
                    run_capabilities_command(cap_cli)?;
                }
                Command::Internalize(internalize_cli) => {
                    internalize::run_internalize_cli(&project_store, &store_root, internalize_cli)?;
                }
                Command::Preflight(preflight_cli) => {
                    run_preflight_command(preflight_cli, &project_root)?;
                }
                Command::Impact(impact_cli) => {
                    run_impact_command(impact_cli, &project_root)?;
                }
                Command::Infer(infer_cli) => {
                    run_infer_command(infer_cli, &project_root)?;
                }
                Command::Trace(trace_cli) => {
                    run_trace_command(trace_cli, &project_root)?;
                }
                Command::Eval(eval_cli) => {
                    eval::run_eval_cli(&project_store, eval_cli)?;
                }
                Command::FlightRecorder(fr_cli) => {
                    flight_recorder::run_flight_recorder_cli(&project_store, fr_cli)?;
                }
                Command::StateCommit(sc_cli) => {
                    run_state_commit_command(sc_cli, &project_root)?;
                }
                Command::Doctor(doctor_cli) => {
                    doctor::run_doctor_cli(&project_store, &project_root, doctor_cli)?;
                }
                Command::Lcm(lcm_cli) => {
                    lcm::run_lcm_cli(&project_store, lcm_cli)?;
                }
                Command::Map(map_cli) => {
                    map_ops::run_map_cli(&project_store, map_cli)?;
                }
                Command::Demo(demo_cli) => {
                    run_demo_command(demo_cli, &project_root)?;
                }
                _ => unreachable!(),
            }
        }
    }
    Ok(())
}

fn should_route_via_group_broker(command: &Command, argv: &[String]) -> bool {
    if core::group_broker::is_internal_invocation() {
        return false;
    }
    match command {
        Command::Todo(_) => todo_argv_is_mutating(argv),
        Command::Decide(decide_cli) => decide_command_is_mutating(decide_cli),
        Command::Data(data_cli) => match &data_cli.command {
            DataCommand::Federation(_) => federation_argv_is_mutating(argv),
            DataCommand::Knowledge(_) => knowledge_argv_is_mutating(argv),
            _ => false,
        },
        _ => false,
    }
}

fn enforce_route_strict_mode() -> bool {
    std::env::var("DECAPOD_GROUP_BROKER_ENFORCE_ROUTE")
        .ok()
        .map(|v| v == "1")
        .unwrap_or(false)
}

fn todo_argv_is_mutating(argv: &[String]) -> bool {
    let Some(sub) = argv.get(1).map(|s| s.as_str()) else {
        return false;
    };
    !matches!(
        sub,
        "list"
            | "get"
            | "show"
            | "categories"
            | "ownerships"
            | "claim-status"
            | "presence"
            | "list-owners"
            | "expertise"
    )
}

fn decide_command_is_mutating(decide_cli: &decide::DecideCli) -> bool {
    matches!(
        decide_cli.command,
        decide::DecideCommand::Start { .. }
            | decide::DecideCommand::Record { .. }
            | decide::DecideCommand::Complete { .. }
            | decide::DecideCommand::Init
    )
}

fn knowledge_argv_is_mutating(argv: &[String]) -> bool {
    matches!(argv.get(2).map(|s| s.as_str()), Some("add" | "promote"))
}

fn federation_argv_is_mutating(argv: &[String]) -> bool {
    matches!(
        argv.get(2).map(|s| s.as_str()),
        Some(
            "add"
                | "edit"
                | "supersede"
                | "deprecate"
                | "dispute"
                | "link"
                | "unlink"
                | "sources-add"
                | "init"
                | "rebuild"
        )
    )
}

fn should_auto_clock_in(command: &Command) -> bool {
    match command {
        Command::Todo(todo_cli) => !todo::is_heartbeat_command(todo_cli),
        Command::Version
        | Command::Activate
        | Command::Init(_)
        | Command::Setup(_)
        | Command::Session(_)
        | Command::Release(_)
        | Command::StateCommit(_)
        | Command::Doctor(_) => false,
        _ => true,
    }
}

fn command_requires_worktree(command: &Command) -> bool {
    match command {
        Command::Init(_)
        | Command::Activate
        | Command::Setup(_)
        | Command::Session(_)
        | Command::Version
        | Command::Validate(_)
        | Command::Workspace(_)
        | Command::Capabilities(_)
        | Command::Trace(_)
        | Command::FlightRecorder(_)
        | Command::Docs(_)
        | Command::Handshake(_)
        | Command::Release(_)
        | Command::Todo(_)
        | Command::Eval(_)
        | Command::StateCommit(_)
        | Command::Doctor(_) => false,
        Command::Data(data_cli) => !matches!(data_cli.command, DataCommand::Schema(_)),
        Command::Rpc(_) => false,
        _ => true,
    }
}

fn is_canonical_decapod_worktree_path(path: &Path) -> bool {
    let mut saw_decapod = false;
    for comp in path.components() {
        let seg = comp.as_os_str().to_string_lossy();
        if seg == ".decapod" {
            saw_decapod = true;
            continue;
        }
        if saw_decapod && seg == "workspaces" {
            return true;
        }
    }
    false
}

fn command_requires_todo_scoped_worktree(command: &Command) -> bool {
    !matches!(
        command,
        Command::Validate(_)
            | Command::Activate
            | Command::Docs(_)
            | Command::Release(_)
            | Command::Trace(_)
            | Command::Capabilities(_)
            | Command::Doctor(_)
            | Command::StateCommit(_)
            | Command::Qa(_)
    )
}

fn command_requires_canonical_worktree_path(command: &Command) -> bool {
    !matches!(
        command,
        Command::Validate(_)
            | Command::Activate
            | Command::Docs(_)
            | Command::Release(_)
            | Command::Trace(_)
            | Command::Capabilities(_)
            | Command::Doctor(_)
            | Command::StateCommit(_)
            | Command::Qa(_)
    )
}

fn branch_contains_todo_ticket_id(branch: &str) -> bool {
    let branch = branch.to_ascii_lowercase();
    if branch.contains("r_") {
        return true;
    }
    if let Ok(hash_re) = fancy_regex::Regex::new(r"todo-[a-z0-9]{6}(\b|-|$)")
        && hash_re.is_match(&branch).unwrap_or(false)
    {
        return true;
    }
    let chars: Vec<char> = branch.chars().collect();
    if chars.len() < 21 {
        return false;
    }
    for i in 0..=(chars.len() - 21) {
        let type_ok = chars[i..i + 4].iter().all(|c| c.is_ascii_lowercase());
        let sep_ok = chars[i + 4] == '_';
        let body_ok = chars[i + 5..i + 21]
            .iter()
            .all(|c| c.is_ascii_alphanumeric());
        if type_ok && sep_ok && body_ok {
            return true;
        }
    }
    false
}

fn enforce_worktree_requirement(
    command: &Command,
    project_root: &Path,
) -> Result<(), error::DecapodError> {
    if std::env::var("DECAPOD_VALIDATE_SKIP_GIT_GATES").is_ok() {
        return Ok(());
    }
    if !command_requires_worktree(command) {
        return Ok(());
    }

    let status = crate::core::workspace::get_workspace_status(project_root)?;
    if status.git.in_worktree {
        let worktree_path = status
            .git
            .worktree_path
            .clone()
            .unwrap_or_else(|| project_root.to_path_buf());
        if command_requires_canonical_worktree_path(command)
            && !is_canonical_decapod_worktree_path(&worktree_path)
        {
            return Err(error::DecapodError::ValidationError(format!(
                "SCOPE_VIOLATION: non-canonical worktree path '{}'. Decapod-managed work must run from '.decapod/workspaces/*'. Run `decapod workspace ensure --branch agent/<id>/<topic>` and execute from the returned path.",
                worktree_path.display()
            )));
        }

        if command_requires_todo_scoped_worktree(command)
            && !branch_contains_todo_ticket_id(&status.git.current_branch)
        {
            return Err(error::DecapodError::ValidationError(format!(
                "SCOPE_VIOLATION: branch '{}' is not todo-scoped. Run `decapod todo add \"<task>\"`, `decapod todo claim --id <task-id>`, then `decapod workspace ensure`.",
                status.git.current_branch
            )));
        }
        return Ok(());
    }

    Err(error::DecapodError::ValidationError(format!(
        "Command requires isolated git worktree under '.decapod/workspaces'; current checkout is not a worktree (branch='{}'). Run `decapod workspace ensure --branch agent/<id>/<topic>` and execute from the reported worktree path.",
        status.git.current_branch
    )))
}

fn rpc_op_requires_worktree(op: &str) -> bool {
    !matches!(
        op,
        "agent.init"
            | "workspace.status"
            | "workspace.ensure"
            | "assurance.evaluate"
            | "mentor.obligations"
            | "context.resolve"
            | "context.scope"
            | "context.capsule.query"
            | "context.bindings"
            | "constitution.get"
            | "schema.get"
            | "store.upsert"
            | "store.query"
            | "validate.run"
            | "standards.resolve"
    )
}

fn enforce_worktree_requirement_for_rpc(
    op: &str,
    project_root: &Path,
) -> Result<(), error::DecapodError> {
    if std::env::var("DECAPOD_VALIDATE_SKIP_GIT_GATES").is_ok() {
        return Ok(());
    }
    if !rpc_op_requires_worktree(op) {
        return Ok(());
    }

    let status = crate::core::workspace::get_workspace_status(project_root)?;
    if status.git.in_worktree {
        let worktree_path = status
            .git
            .worktree_path
            .clone()
            .unwrap_or_else(|| project_root.to_path_buf());
        if !matches!(
            op,
            "validate.run"
                | "context.resolve"
                | "context.scope"
                | "context.capsule.query"
                | "context.bindings"
                | "schema.get"
        ) && !is_canonical_decapod_worktree_path(&worktree_path)
        {
            return Err(error::DecapodError::ValidationError(format!(
                "SCOPE_VIOLATION: RPC op '{}' must execute from a Decapod-managed worktree under '.decapod/workspaces/*' (current '{}'). Run `decapod workspace ensure` and retry.",
                op,
                worktree_path.display()
            )));
        }
        return Ok(());
    }

    Err(error::DecapodError::ValidationError(format!(
        "RPC op '{}' requires isolated git worktree under '.decapod/workspaces'; current checkout is not a worktree (branch='{}'). Run `decapod workspace ensure --branch agent/<id>/<topic>` and execute from the reported worktree path.",
        op, status.git.current_branch
    )))
}

fn rpc_op_bypasses_session(op: &str) -> bool {
    matches!(
        op,
        "agent.init"
            | "context.resolve"
            | "context.scope"
            | "context.capsule.query"
            | "context.bindings"
            | "constitution.get"
            | "schema.get"
            | "store.upsert"
            | "store.query"
            | "validate.run"
            | "workspace.status"
            | "workspace.ensure"
            | "standards.resolve"
    )
}

fn requires_session_token(command: &Command) -> bool {
    match command {
        // Bootstrap/session lifecycle + version + capabilities are sessionless.
        Command::Init(_)
        | Command::Session(_)
        | Command::Version
        | Command::Activate
        | Command::Docs(_)
        | Command::Capabilities(_)
        | Command::Release(_)
        | Command::Trace(_)
        | Command::FlightRecorder(_)
        | Command::StateCommit(_)
        | Command::Doctor(_) => false,
        Command::Data(DataCli {
            command: DataCommand::Schema(_),
        }) => false,
        Command::Rpc(rpc_cli) => {
            if let Some(ref op) = rpc_cli.op {
                !rpc_op_bypasses_session(op)
            } else {
                // If op is not provided via flag, we'll check it after parsing JSON in run_rpc_command
                false
            }
        }
        _ => true,
    }
}

#[derive(Debug, Serialize, Deserialize)]
struct AgentSessionRecord {
    agent_id: String,
    token: String,
    password_hash: String,
    issued_at_epoch_secs: u64,
    expires_at_epoch_secs: u64,
}

#[derive(Debug, Serialize, Deserialize)]
struct ConstitutionalAwarenessRecord {
    agent_id: String,
    session_token: Option<String>,
    initialized_at_epoch_secs: u64,
    validated_at_epoch_secs: Option<u64>,
    core_constitution_ingested_at_epoch_secs: Option<u64>,
    context_resolved_at_epoch_secs: Option<u64>,
    source_ops: Vec<String>,
}

fn now_epoch_secs() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0)
}

fn session_ttl_secs() -> u64 {
    std::env::var("DECAPOD_SESSION_TTL_SECS")
        .ok()
        .and_then(|v| v.parse::<u64>().ok())
        .filter(|v| *v > 0)
        .unwrap_or(3600)
}

fn current_agent_id() -> String {
    std::env::var("DECAPOD_AGENT_ID")
        .ok()
        .map(|v| v.trim().to_string())
        .filter(|v| !v.is_empty())
        .unwrap_or_else(|| "unknown".to_string())
}

fn sanitize_agent_component(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for ch in s.chars() {
        if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
            out.push(ch.to_ascii_lowercase());
        } else {
            out.push('-');
        }
    }
    out.trim_matches('-').to_string()
}

fn sessions_dir(project_root: &Path) -> PathBuf {
    project_root
        .join(".decapod")
        .join("generated")
        .join("sessions")
}

fn session_file_for_agent(project_root: &Path, agent_id: &str) -> PathBuf {
    sessions_dir(project_root).join(format!("{}.json", sanitize_agent_component(agent_id)))
}

fn awareness_dir(project_root: &Path) -> PathBuf {
    project_root
        .join(".decapod")
        .join("generated")
        .join("awareness")
}

fn awareness_file_for_agent(project_root: &Path, agent_id: &str) -> PathBuf {
    awareness_dir(project_root).join(format!("{}.json", sanitize_agent_component(agent_id)))
}

fn hash_password(password: &str, token: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(token.as_bytes());
    hasher.update(b":");
    hasher.update(password.as_bytes());
    let digest = hasher.finalize();
    let mut out = String::with_capacity(digest.len() * 2);
    for b in digest {
        out.push_str(&format!("{:02x}", b));
    }
    out
}

fn generate_ephemeral_password() -> Result<String, error::DecapodError> {
    let mut buf = vec![0u8; 24];
    let mut urandom = fs::File::open("/dev/urandom").map_err(error::DecapodError::IoError)?;
    urandom
        .read_exact(&mut buf)
        .map_err(error::DecapodError::IoError)?;
    let mut out = String::with_capacity(buf.len() * 2);
    for b in buf {
        out.push_str(&format!("{:02x}", b));
    }
    Ok(out)
}

fn read_agent_session(
    project_root: &Path,
    agent_id: &str,
) -> Result<Option<AgentSessionRecord>, error::DecapodError> {
    let path = session_file_for_agent(project_root, agent_id);
    if !path.exists() {
        return Ok(None);
    }
    let raw = fs::read_to_string(&path).map_err(error::DecapodError::IoError)?;
    let rec: AgentSessionRecord = serde_json::from_str(&raw)
        .map_err(|e| error::DecapodError::SessionError(format!("invalid session file: {}", e)))?;
    Ok(Some(rec))
}

fn atomic_write_file(path: &Path, body: &str) -> Result<(), error::DecapodError> {
    let parent = path.parent().ok_or_else(|| {
        error::DecapodError::IoError(std::io::Error::other(
            "target path is missing parent directory",
        ))
    })?;
    fs::create_dir_all(parent).map_err(error::DecapodError::IoError)?;

    let file_name = path
        .file_name()
        .and_then(|v| v.to_str())
        .unwrap_or("file")
        .to_string();
    let nonce = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_nanos())
        .unwrap_or(0);
    let tmp = parent.join(format!(
        ".{}.tmp-{}-{}",
        file_name,
        std::process::id(),
        nonce
    ));
    fs::write(&tmp, body).map_err(error::DecapodError::IoError)?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut perms = fs::metadata(&tmp)
            .map_err(error::DecapodError::IoError)?
            .permissions();
        perms.set_mode(0o600);
        fs::set_permissions(&tmp, perms).map_err(error::DecapodError::IoError)?;
    }
    fs::rename(&tmp, path).map_err(error::DecapodError::IoError)?;
    Ok(())
}

fn write_agent_session(
    project_root: &Path,
    rec: &AgentSessionRecord,
) -> Result<(), error::DecapodError> {
    let dir = sessions_dir(project_root);
    fs::create_dir_all(&dir).map_err(error::DecapodError::IoError)?;
    let path = session_file_for_agent(project_root, &rec.agent_id);
    let body = serde_json::to_string_pretty(rec)
        .map_err(|e| error::DecapodError::SessionError(format!("session encode error: {}", e)))?;
    atomic_write_file(&path, &body)?;
    Ok(())
}

fn clear_agent_awareness(project_root: &Path, agent_id: &str) -> Result<(), error::DecapodError> {
    let path = awareness_file_for_agent(project_root, agent_id);
    if path.exists() {
        fs::remove_file(path).map_err(error::DecapodError::IoError)?;
    }
    Ok(())
}

fn read_awareness_record(
    project_root: &Path,
    agent_id: &str,
) -> Result<Option<ConstitutionalAwarenessRecord>, error::DecapodError> {
    let path = awareness_file_for_agent(project_root, agent_id);
    if !path.exists() {
        return Ok(None);
    }
    let raw = fs::read_to_string(path).map_err(error::DecapodError::IoError)?;
    let rec: ConstitutionalAwarenessRecord = serde_json::from_str(&raw).map_err(|e| {
        error::DecapodError::ValidationError(format!(
            "invalid constitutional awareness record: {}",
            e
        ))
    })?;
    Ok(Some(rec))
}

fn write_awareness_record(
    project_root: &Path,
    rec: &ConstitutionalAwarenessRecord,
) -> Result<(), error::DecapodError> {
    let dir = awareness_dir(project_root);
    fs::create_dir_all(&dir).map_err(error::DecapodError::IoError)?;
    let path = awareness_file_for_agent(project_root, &rec.agent_id);
    let body = serde_json::to_string_pretty(rec).map_err(|e| {
        error::DecapodError::ValidationError(format!("awareness encode error: {}", e))
    })?;
    atomic_write_file(&path, &body)?;
    Ok(())
}

fn mark_constitution_initialized(project_root: &Path) -> Result<(), error::DecapodError> {
    let agent_id = current_agent_id();
    let session_token = read_agent_session(project_root, &agent_id)?.map(|s| s.token);
    let now = now_epoch_secs();
    let existing = read_awareness_record(project_root, &agent_id)?;
    let mut source_ops = existing
        .as_ref()
        .map(|r| r.source_ops.clone())
        .unwrap_or_default();
    if !source_ops.iter().any(|op| op == "agent.init") {
        source_ops.push("agent.init".to_string());
    }
    let rec = ConstitutionalAwarenessRecord {
        agent_id,
        session_token,
        initialized_at_epoch_secs: now,
        validated_at_epoch_secs: existing.as_ref().and_then(|r| r.validated_at_epoch_secs),
        core_constitution_ingested_at_epoch_secs: existing
            .as_ref()
            .and_then(|r| r.core_constitution_ingested_at_epoch_secs),
        context_resolved_at_epoch_secs: existing.and_then(|r| r.context_resolved_at_epoch_secs),
        source_ops,
    };
    write_awareness_record(project_root, &rec)
}

fn mark_constitution_context_resolved(project_root: &Path) -> Result<(), error::DecapodError> {
    let agent_id = current_agent_id();
    let mut rec =
        read_awareness_record(project_root, &agent_id)?.unwrap_or(ConstitutionalAwarenessRecord {
            agent_id: agent_id.clone(),
            session_token: read_agent_session(project_root, &agent_id)?.map(|s| s.token),
            initialized_at_epoch_secs: now_epoch_secs(),
            validated_at_epoch_secs: None,
            core_constitution_ingested_at_epoch_secs: None,
            context_resolved_at_epoch_secs: None,
            source_ops: Vec::new(),
        });
    rec.context_resolved_at_epoch_secs = Some(now_epoch_secs());
    if !rec.source_ops.iter().any(|op| op == "context.resolve") {
        rec.source_ops.push("context.resolve".to_string());
    }
    write_awareness_record(project_root, &rec)
}

fn mark_validation_completed(project_root: &Path) -> Result<(), error::DecapodError> {
    let agent_id = current_agent_id();
    let mut rec =
        read_awareness_record(project_root, &agent_id)?.unwrap_or(ConstitutionalAwarenessRecord {
            agent_id: agent_id.clone(),
            session_token: read_agent_session(project_root, &agent_id)?.map(|s| s.token),
            initialized_at_epoch_secs: now_epoch_secs(),
            validated_at_epoch_secs: None,
            core_constitution_ingested_at_epoch_secs: None,
            context_resolved_at_epoch_secs: None,
            source_ops: Vec::new(),
        });
    rec.validated_at_epoch_secs = Some(now_epoch_secs());
    if !rec.source_ops.iter().any(|op| op == "validate") {
        rec.source_ops.push("validate".to_string());
    }
    write_awareness_record(project_root, &rec)
}

fn mark_core_constitution_ingested(
    project_root: &Path,
    source_op: &str,
) -> Result<(), error::DecapodError> {
    let agent_id = current_agent_id();
    let mut rec =
        read_awareness_record(project_root, &agent_id)?.unwrap_or(ConstitutionalAwarenessRecord {
            agent_id: agent_id.clone(),
            session_token: read_agent_session(project_root, &agent_id)?.map(|s| s.token),
            initialized_at_epoch_secs: now_epoch_secs(),
            validated_at_epoch_secs: None,
            core_constitution_ingested_at_epoch_secs: None,
            context_resolved_at_epoch_secs: None,
            source_ops: Vec::new(),
        });
    rec.core_constitution_ingested_at_epoch_secs = Some(now_epoch_secs());
    if !rec.source_ops.iter().any(|op| op == source_op) {
        rec.source_ops.push(source_op.to_string());
    }
    write_awareness_record(project_root, &rec)
}

fn cleanup_expired_sessions(
    project_root: &Path,
    store_root: &Path,
) -> Result<Vec<String>, error::DecapodError> {
    let dir = sessions_dir(project_root);
    if !dir.exists() {
        return Ok(Vec::new());
    }
    let now = now_epoch_secs();
    let mut expired_agents = Vec::new();
    for entry in fs::read_dir(&dir).map_err(error::DecapodError::IoError)? {
        let entry = entry.map_err(error::DecapodError::IoError)?;
        let path = entry.path();
        if path.extension().and_then(|s| s.to_str()) != Some("json") {
            continue;
        }
        let raw = match fs::read_to_string(&path) {
            Ok(v) => v,
            Err(_) => {
                let _ = fs::remove_file(&path);
                continue;
            }
        };
        let rec: AgentSessionRecord = match serde_json::from_str(&raw) {
            Ok(v) => v,
            Err(_) => {
                let _ = fs::remove_file(&path);
                continue;
            }
        };
        if rec.expires_at_epoch_secs <= now {
            let _ = fs::remove_file(&path);
            expired_agents.push(rec.agent_id);
        }
    }

    if !expired_agents.is_empty() {
        todo::cleanup_stale_agent_assignments(store_root, &expired_agents, "session.expired")?;
        for agent_id in &expired_agents {
            let _ = clear_agent_awareness(project_root, agent_id);
        }
    }

    Ok(expired_agents)
}

fn ensure_session_valid() -> Result<(), error::DecapodError> {
    let current_dir = std::env::current_dir()?;
    let project_root = find_decapod_project_root(&current_dir)?;
    let store_root = project_root.join(".decapod").join("data");
    fs::create_dir_all(&store_root).map_err(error::DecapodError::IoError)?;
    let _ = cleanup_expired_sessions(&project_root, &store_root)?;

    let agent_id = current_agent_id();
    let session = read_agent_session(&project_root, &agent_id)?;
    let Some(session) = session else {
        // Auto-acquire session if none exists (entrypoint funnel behavior)
        return auto_acquire_session(&project_root, &agent_id);
    };

    if session.expires_at_epoch_secs <= now_epoch_secs() {
        let _ = fs::remove_file(session_file_for_agent(&project_root, &agent_id));
        let _ = todo::cleanup_stale_agent_assignments(
            &store_root,
            std::slice::from_ref(&agent_id),
            "session.expired",
        );
        // Auto-acquire session if expired (entrypoint funnel behavior)
        return auto_acquire_session(&project_root, &agent_id);
    }

    if agent_id == "unknown" {
        // Force session instantiation for unknown agents (required for validate)
        return auto_acquire_session(&project_root, &agent_id);
    }

    // Read from OnceLock first (process-local), fall back to env if not set
    let supplied_password = SESSION_PASSWORD
        .get()
        .cloned()
        .or_else(|| std::env::var("DECAPOD_SESSION_PASSWORD").ok())
        .inspect(|p| {
            // Cache it in OnceLock if found in env
            let _ = SESSION_PASSWORD.get_or_init(|| p.clone());
        });

    let supplied_password = match supplied_password {
        Some(p) => p,
        None => {
            // No password in env - auto-acquire new session (entrypoint funnel)
            return auto_acquire_session(&project_root, &agent_id);
        }
    };
    let supplied_hash = hash_password(&supplied_password, &session.token);
    if supplied_hash != session.password_hash {
        // Password invalid - auto-acquire new session (entrypoint funnel)
        return auto_acquire_session(&project_root, &agent_id);
    }
    Ok(())
}

fn auto_acquire_session(project_root: &Path, agent_id: &str) -> Result<(), error::DecapodError> {
    let issued = now_epoch_secs();
    let expires = issued.saturating_add(session_ttl_secs());
    let token = crate::core::ulid::new_ulid();
    let password = generate_ephemeral_password()?;
    let rec = AgentSessionRecord {
        agent_id: agent_id.to_string(),
        token: token.clone(),
        password_hash: hash_password(&password, &token),
        issued_at_epoch_secs: issued,
        expires_at_epoch_secs: expires,
    };
    write_agent_session(project_root, &rec)?;

    // Set the password for subsequent operations in this process using process-local OnceLock
    // Eliminates unsafe env::set_var and multi-threading UB
    SESSION_PASSWORD.get_or_init(|| password);

    eprintln!("session: auto-acquired for agent '{}'.", agent_id);

    Ok(())
}

use crate::core::ansi::AnsiExt;
use crate::core::migration::DECAPOD_VERSION;

fn check_and_update_version() -> Result<bool, error::DecapodError> {
    let current_version = DECAPOD_VERSION;

    // Skip check if curl not available (treat all errors as skip)
    match std::process::Command::new("curl").arg("--version").output() {
        Ok(o) if !o.status.success() => return Ok(false),
        Err(_) => return Ok(false),
        _ => {}
    }

    let latest_version = match fetch_latest_crates_version() {
        Ok(v) => v,
        Err(_) => return Ok(false),
    };

    if version_gt(&latest_version, current_version) {
        eprintln!(
            "{} Decapod v{} → v{}, updating...",
            "⚠".bright_yellow(),
            current_version,
            latest_version
        );

        let _ = backup_decapod_state(); // Best effort

        // Skip if cargo not available
        match std::process::Command::new("cargo")
            .arg("--version")
            .output()
        {
            Ok(o) if !o.status.success() => {
                eprintln!(
                    "{} cargo not available, skipping update",
                    "⚠".bright_yellow()
                );
                return Ok(false);
            }
            Err(_) => return Ok(false),
            _ => {}
        }

        if install_decapod().is_ok() {
            eprintln!("{} Updated to v{}.", "✓".bright_green(), latest_version);

            // Prompt to migrate/refresh config for new fields
            let project_root = std::env::current_dir()
                .ok()
                .and_then(|d| find_decapod_project_root(&d).ok());

            if let Some(root) = project_root {
                let config_path = root.join(".decapod").join("config.toml");
                if config_path.exists() {
                    eprintln!(
                        "{} Check for new config fields in .decapod/config.toml",
                        "→".bright_cyan()
                    );
                }
            }

            return Ok(true);
        }
    }

    Ok(false)
}

fn fetch_latest_crates_version() -> Result<String, error::DecapodError> {
    let output = std::process::Command::new("curl")
        .args(["-s", "https://crates.io/api/v1/crates/decapod"])
        .output()
        .map_err(|e| {
            error::DecapodError::ValidationError(format!("Failed to check version: {}", e))
        })?;

    if !output.status.success() {
        return Err(error::DecapodError::ValidationError(
            "Failed to fetch latest version".to_string(),
        ));
    }

    let json: serde_json::Value = serde_json::from_slice(&output.stdout)
        .map_err(|e| error::DecapodError::ValidationError(format!("Invalid response: {}", e)))?;

    json.get("version")
        .and_then(|v| v.get("num"))
        .and_then(|n| n.as_str())
        .map(|s| s.to_string())
        .ok_or_else(|| error::DecapodError::ValidationError("Could not parse version".to_string()))
}

fn version_gt(new: &str, current: &str) -> bool {
    let new_parts: Vec<u32> = new.split('.').filter_map(|p| p.parse().ok()).collect();
    let cur_parts: Vec<u32> = current.split('.').filter_map(|p| p.parse().ok()).collect();

    for i in 0..new_parts.len().max(cur_parts.len()) {
        let new_p = new_parts.get(i).unwrap_or(&0);
        let cur_p = cur_parts.get(i).unwrap_or(&0);
        if new_p > cur_p {
            return true;
        }
        if new_p < cur_p {
            return false;
        }
    }
    false
}

fn backup_decapod_state() -> Result<(), error::DecapodError> {
    let current_dir = std::env::current_dir()?;
    let project_root = find_decapod_project_root(&current_dir)?;
    let decapod_dir = project_root.join(".decapod");

    if !decapod_dir.exists() {
        return Ok(());
    }

    let backup_dir = decapod_dir.join("backups");
    fs::create_dir_all(&backup_dir).map_err(error::DecapodError::IoError)?;

    let timestamp = std::time::SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0);
    let backup_name = format!("backup_{}_{}", DECAPOD_VERSION, timestamp);
    let backup_path = backup_dir.join(&backup_name);

    let mut backup_file = fs::File::create(&backup_path).map_err(error::DecapodError::IoError)?;

    let override_path = decapod_dir.join("OVERRIDE.md");
    let overrides = if override_path.exists() {
        fs::read_to_string(&override_path).unwrap_or_default()
    } else {
        String::new()
    };

    let now = std::time::SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0);
    let content = format!(
        "# Backup at {} v{}\n# OVERRIDE.md\n{}\n",
        now, DECAPOD_VERSION, overrides
    );

    backup_file.write_all(content.as_bytes())?;

    Ok(())
}

fn install_decapod() -> Result<(), error::DecapodError> {
    let output = std::process::Command::new("cargo")
        .args(["install", "decapod"])
        .output()
        .map_err(|e| error::DecapodError::ValidationError(format!("Failed to install: {}", e)))?;

    if !output.status.success() {
        let err = String::from_utf8_lossy(&output.stderr);
        return Err(error::DecapodError::ValidationError(format!(
            "Install failed: {}",
            err
        )));
    }

    Ok(())
}

fn run_session_command(session_cli: SessionCli) -> Result<(), error::DecapodError> {
    let current_dir = std::env::current_dir()?;
    let project_root = find_decapod_project_root(&current_dir)?;
    let store_root = project_root.join(".decapod").join("data");
    fs::create_dir_all(&store_root).map_err(error::DecapodError::IoError)?;
    let _ = cleanup_expired_sessions(&project_root, &store_root)?;

    // Check and update version on session acquire
    if matches!(session_cli.command, SessionCommand::Acquire)
        && let Ok(true) = check_and_update_version()
    {
        eprintln!(
            "{} Restart Session: decapod session acquire",
            "→".bright_cyan()
        );
    }

    match session_cli.command {
        SessionCommand::Acquire => {
            let agent_id = current_agent_id();
            if let Some(existing) = read_agent_session(&project_root, &agent_id)?
                && existing.expires_at_epoch_secs > now_epoch_secs()
            {
                println!(
                    "Session already active for agent '{}'. Use 'decapod session status' for details.",
                    agent_id
                );
                return Ok(());
            }

            let issued = now_epoch_secs();
            let expires = issued.saturating_add(session_ttl_secs());
            let token = crate::core::ulid::new_ulid();
            let password = generate_ephemeral_password()?;
            let rec = AgentSessionRecord {
                agent_id: agent_id.clone(),
                token: token.clone(),
                password_hash: hash_password(&password, &token),
                issued_at_epoch_secs: issued,
                expires_at_epoch_secs: expires,
            };
            write_agent_session(&project_root, &rec)?;
            clear_agent_awareness(&project_root, &agent_id)?;

            println!("Session acquired successfully.");
            println!("Agent: {}", agent_id);
            println!("Token: {}", token);
            println!("Password: {}", password);
            println!("ExpiresAtEpoch: {}", expires);
            println!(
                "Export before running other commands: DECAPOD_AGENT_ID='{}' and DECAPOD_SESSION_PASSWORD='<password>'",
                rec.agent_id
            );
            println!("\nYou may now use other decapod commands.");
            Ok(())
        }
        SessionCommand::Status => {
            let agent_id = current_agent_id();
            if let Some(session) = read_agent_session(&project_root, &agent_id)? {
                println!("Session active");
                println!("Agent: {}", session.agent_id);
                println!("Token: {}", session.token);
                println!("IssuedAtEpoch: {}", session.issued_at_epoch_secs);
                println!("ExpiresAtEpoch: {}", session.expires_at_epoch_secs);
            } else {
                println!("No active session");
                println!("Run 'decapod session acquire' to start a session");
            }
            Ok(())
        }
        SessionCommand::Release => {
            let agent_id = current_agent_id();
            let session_path = session_file_for_agent(&project_root, &agent_id);
            if session_path.exists() {
                std::fs::remove_file(&session_path).map_err(error::DecapodError::IoError)?;
                clear_agent_awareness(&project_root, &agent_id)?;
                let _ = todo::cleanup_stale_agent_assignments(
                    &store_root,
                    std::slice::from_ref(&agent_id),
                    "session.release",
                );
                println!("Session released");
            } else {
                println!("No active session to release");
            }
            Ok(())
        }
        SessionCommand::Init {
            scope,
            mut proofs,
            force,
        } => {
            if proofs.is_empty() {
                proofs.push("decapod validate".to_string());
            }
            run_session_init(&project_root, &scope, &proofs, force)
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct HandshakeArtifact {
    schema_version: String,
    request_id: String,
    agent_id: String,
    repo_version: String,
    scope: String,
    proofs: Vec<String>,
    declared_docs: Vec<String>,
    doc_hashes: serde_json::Value,
    artifact_hash: String,
}

fn hash_bytes_hex(input: &[u8]) -> String {
    let mut hasher = Sha256::new();
    hasher.update(input);
    format!("{:x}", hasher.finalize())
}

fn required_handshake_docs() -> Vec<&'static str> {
    vec![
        "CLAUDE.md",
        "AGENTS.md",
        "core/DECAPOD",
        "interfaces/CONTROL_PLANE",
    ]
}

fn build_handshake_artifact(
    project_root: &Path,
    scope: &str,
    proofs: &[String],
) -> Result<HandshakeArtifact, error::DecapodError> {
    let mut doc_hashes = serde_json::Map::new();
    let required_docs = required_handshake_docs();
    for rel in &required_docs {
        let hash = if rel.ends_with(".md") && !rel.contains('/') {
            // Root files like AGENTS.md
            let abs = project_root.join(rel);
            if !abs.exists() {
                return Err(error::DecapodError::ValidationError(format!(
                    "Handshake requires `{}` to exist.",
                    rel
                )));
            }
            let bytes = fs::read(&abs).map_err(error::DecapodError::IoError)?;
            hash_bytes_hex(&bytes)
        } else {
            // Constitution docs from embedded assets
            match crate::core::assets::get_embedded_doc(rel) {
                Some(c) => hash_bytes_hex(c.as_bytes()),
                None => {
                    return Err(error::DecapodError::ValidationError(format!(
                        "Handshake requires constitution doc `{}` (embedded) to be accessible.",
                        rel
                    )));
                }
            }
        };

        doc_hashes.insert((*rel).to_string(), serde_json::json!(hash));
    }

    let request_id = crate::core::ulid::new_ulid();
    let mut unsigned = serde_json::json!({
        "schema_version": "1.0.0",
        "request_id": request_id,
        "agent_id": current_agent_id(),
        "repo_version": migration::DECAPOD_VERSION,
        "scope": scope,
        "proofs": proofs,
        "declared_docs": required_docs,
        "doc_hashes": doc_hashes,
    });
    let canonical = serde_json::to_vec(&unsigned).map_err(|e| {
        error::DecapodError::ValidationError(format!("Failed to encode handshake artifact: {e}"))
    })?;
    let artifact_hash = hash_bytes_hex(&canonical);
    unsigned["artifact_hash"] = serde_json::json!(artifact_hash);

    serde_json::from_value(unsigned).map_err(|e| {
        error::DecapodError::ValidationError(format!("Failed to finalize handshake artifact: {e}"))
    })
}

fn write_handshake_artifact(
    project_root: &Path,
    artifact: &HandshakeArtifact,
) -> Result<PathBuf, error::DecapodError> {
    let dir = project_root
        .join(".decapod")
        .join("records")
        .join("handshakes");
    fs::create_dir_all(&dir).map_err(error::DecapodError::IoError)?;
    let file = format!(
        "{}-{}.json",
        crate::core::time::now_epoch_z(),
        artifact.agent_id.replace('/', "_")
    );
    let path = dir.join(file);
    let pretty = serde_json::to_vec_pretty(artifact).map_err(|e| {
        error::DecapodError::ValidationError(format!("Failed to serialize handshake record: {e}"))
    })?;
    fs::write(&path, pretty).map_err(error::DecapodError::IoError)?;
    Ok(path)
}

fn run_handshake_command(
    cli: HandshakeCli,
    project_root: &Path,
) -> Result<(), error::DecapodError> {
    if cli.proofs.is_empty() {
        return Err(error::DecapodError::ValidationError(
            "Handshake requires at least one `--proof` declaration.".to_string(),
        ));
    }
    let artifact = build_handshake_artifact(project_root, &cli.scope, &cli.proofs)?;
    let path = write_handshake_artifact(project_root, &artifact)?;
    println!(
        "{}",
        serde_json::json!({
            "cmd": "handshake",
            "status": "ok",
            "path": path,
            "artifact_hash": artifact.artifact_hash,
            "repo_version": artifact.repo_version,
            "scope": artifact.scope,
            "proofs": artifact.proofs,
        })
    );
    Ok(())
}

fn run_session_init(
    project_root: &Path,
    scope: &str,
    proofs: &[String],
    force: bool,
) -> Result<(), error::DecapodError> {
    let mut created = Vec::new();
    let mut skipped = Vec::new();

    let tasks_dir = project_root.join("tasks");
    fs::create_dir_all(&tasks_dir).map_err(error::DecapodError::IoError)?;

    let todo_path = tasks_dir.join("todo.md");
    let todo_stub = "\
# Work Session Plan

- Task: <replace-with-task-id-and-title>
- Scope: <replace-with-scope>
- Constraints: keep daemonless, repo-native, proof-gated

## Required Constitution Links
- core/DECAPOD
- interfaces/CONTROL_PLANE
- specs/SECURITY

## Proof Plan
- decapod validate
";
    write_stub(&todo_path, todo_stub, force, &mut created, &mut skipped)?;

    let intent_path = project_root.join("INTENT.md");
    let intent_stub = "\
# INTENT

## Problem
<what outcome is required>

## Constraints
- daemonless
- repo-native canonical state
- deterministic reducers and proof gates

## Acceptance Proofs
- decapod validate
";
    write_stub(&intent_path, intent_stub, force, &mut created, &mut skipped)?;

    let handshake_path = project_root.join("HANDSHAKE.md");
    let handshake_stub = "\
# HANDSHAKE

- Agent: <agent-id>
- Scope: <scope>
- Proofs: <proof-list>
- Record: `.decapod/records/handshakes/<latest>.json`
";
    write_stub(
        &handshake_path,
        handshake_stub,
        force,
        &mut created,
        &mut skipped,
    )?;

    let artifact = build_handshake_artifact(project_root, scope, proofs)?;
    let artifact_path = write_handshake_artifact(project_root, &artifact)?;

    println!(
        "{}",
        serde_json::json!({
            "cmd": "session.init",
            "status": "ok",
            "created": created,
            "skipped": skipped,
            "handshake_record": artifact_path,
            "template_refs": [
                "Embedded: templates now in Rust via template_agents(), template_named_agent(), template_readme()"
            ]
        })
    );
    Ok(())
}

fn write_stub(
    path: &Path,
    content: &str,
    force: bool,
    created: &mut Vec<String>,
    skipped: &mut Vec<String>,
) -> Result<(), error::DecapodError> {
    if path.exists() && !force {
        skipped.push(path.display().to_string());
        return Ok(());
    }
    fs::write(path, content).map_err(error::DecapodError::IoError)?;
    created.push(path.display().to_string());
    Ok(())
}

fn run_release_command(cli: ReleaseCli, project_root: &Path) -> Result<(), error::DecapodError> {
    match cli.command {
        ReleaseCommand::Check => run_release_check(project_root),
        ReleaseCommand::Inventory => run_release_inventory(project_root),
        ReleaseCommand::LineageSync => run_release_lineage_sync(project_root),
    }
}

fn run_release_check(project_root: &Path) -> Result<(), error::DecapodError> {
    let mut failures = Vec::new();
    let mut lineage_records: Vec<(String, PolicyLineage)> = Vec::new();
    let mut changelog_raw: Option<String> = None;
    let changelog = project_root.join("CHANGELOG.md");
    let cargo_lock = project_root.join("Cargo.lock");
    let cargo_toml = project_root.join("Cargo.toml");
    let rpc_golden_req = project_root.join("tests/golden/rpc/v1/agent_init.request.json");
    let rpc_golden_res = project_root.join("tests/golden/rpc/v1/agent_init.response.json");
    let artifact_manifest =
        project_root.join(".decapod/generated/artifacts/provenance/artifact_manifest.json");
    let proof_manifest =
        project_root.join(".decapod/generated/artifacts/provenance/proof_manifest.json");
    let intent_convergence_manifest = project_root
        .join(".decapod/generated/artifacts/provenance/intent_convergence_checklist.json");

    if !changelog.exists() {
        failures.push("CHANGELOG.md missing".to_string());
    } else {
        let raw = fs::read_to_string(&changelog).map_err(error::DecapodError::IoError)?;
        changelog_raw = Some(raw.clone());
        if !raw.contains("## [Unreleased]") {
            failures.push("CHANGELOG.md missing `## [Unreleased]` section".to_string());
        }
    }
    if crate::core::assets::get_embedded_doc("docs/MIGRATIONS").is_none() {
        failures.push("docs/MIGRATIONS missing from embedded assets".to_string());
    }
    if !cargo_lock.exists() {
        failures.push("Cargo.lock missing (locked builds required)".to_string());
    }
    if !cargo_toml.exists() {
        failures.push("Cargo.toml missing".to_string());
    }
    if !rpc_golden_req.exists() || !rpc_golden_res.exists() {
        failures.push("RPC golden vectors missing under tests/golden/rpc/v1".to_string());
    }
    if !artifact_manifest.exists() {
        failures.push(
            "artifact provenance manifest missing: .decapod/generated/artifacts/provenance/artifact_manifest.json"
                .to_string(),
        );
    }
    if !proof_manifest.exists() {
        failures.push(
            "proof provenance manifest missing: .decapod/generated/artifacts/provenance/proof_manifest.json"
                .to_string(),
        );
    }
    if !intent_convergence_manifest.exists() {
        failures.push(
            "intent convergence manifest missing: .decapod/generated/artifacts/provenance/intent_convergence_checklist.json"
                .to_string(),
        );
    }
    if artifact_manifest.exists() && proof_manifest.exists() && intent_convergence_manifest.exists()
    {
        match stamp_release_policy_lineage(
            project_root,
            [
                &artifact_manifest,
                &proof_manifest,
                &intent_convergence_manifest,
            ],
        ) {
            Ok(lineage) => lineage_records.push(("lineage stamp baseline".to_string(), lineage)),
            Err(e) => failures.push(format!("provenance lineage stamping failed: {}", e)),
        }
    }
    if artifact_manifest.exists() {
        match validate_artifact_manifest(project_root, &artifact_manifest) {
            Ok(lineage) => lineage_records.push(("artifact manifest".to_string(), lineage)),
            Err(e) => failures.push(format!("artifact manifest invalid: {}", e)),
        }
    }
    if proof_manifest.exists() {
        match validate_proof_manifest(project_root, &proof_manifest) {
            Ok(lineage) => lineage_records.push(("proof manifest".to_string(), lineage)),
            Err(e) => failures.push(format!("proof manifest invalid: {}", e)),
        }
    }
    if intent_convergence_manifest.exists() {
        match validate_intent_convergence_manifest(project_root, &intent_convergence_manifest) {
            Ok(lineage) => {
                lineage_records.push(("intent convergence manifest".to_string(), lineage))
            }
            Err(e) => failures.push(format!("intent convergence manifest invalid: {}", e)),
        }
    }

    if let Some((baseline_name, baseline)) = lineage_records.first() {
        for (name, lineage) in lineage_records.iter().skip(1) {
            if lineage != baseline {
                failures.push(format!(
                    "policy lineage mismatch: '{}' differs from '{}' ({:?} != {:?})",
                    name, baseline_name, lineage, baseline
                ));
            }
        }
    }

    let changed_paths = git_changed_paths(project_root);
    if has_schema_or_interface_changes(&changed_paths) {
        if let Some(changelog_text) = changelog_raw {
            if !changelog_mentions_schema_or_interface(&changelog_text) {
                failures.push(
                    "schema/interface files changed but CHANGELOG.md [Unreleased] has no schema/interface entry"
                        .to_string(),
                );
            }
        } else {
            failures.push(
                "schema/interface files changed but CHANGELOG.md could not be read".to_string(),
            );
        }
    }

    if !failures.is_empty() {
        return Err(error::DecapodError::ValidationError(format!(
            "release.check failed:\n- {}",
            failures.join("\n- ")
        )));
    }

    println!(
        "{}",
        serde_json::json!({
            "cmd": "release.check",
            "status": "ok",
            "checks": [
                "changelog.unreleased",
                "migrations.doc",
                "cargo.lock.present",
                "rpc.golden_vectors.present",
                "provenance.manifests.verified",
                "intent_convergence.manifest.verified",
                "schema_interface.changelog.policy"
            ]
        })
    );
    Ok(())
}

fn run_release_inventory(project_root: &Path) -> Result<(), error::DecapodError> {
    let inventory = build_release_inventory(project_root)?;
    let out_dir = project_root
        .join(".decapod")
        .join("generated")
        .join("artifacts")
        .join("inventory");
    fs::create_dir_all(&out_dir).map_err(error::DecapodError::IoError)?;
    let out_path = out_dir.join("repo_inventory.json");
    let payload = serde_json::to_vec_pretty(&inventory).map_err(|e| {
        error::DecapodError::ValidationError(format!(
            "failed to serialize release inventory artifact: {e}"
        ))
    })?;
    fs::write(&out_path, payload).map_err(error::DecapodError::IoError)?;

    println!(
        "{}",
        serde_json::json!({
            "cmd": "release.inventory",
            "status": "ok",
            "artifact": ".decapod/generated/artifacts/inventory/repo_inventory.json",
            "summary": inventory["totals"]
        })
    );
    Ok(())
}

fn run_release_lineage_sync(project_root: &Path) -> Result<(), error::DecapodError> {
    let artifact_manifest =
        project_root.join(".decapod/generated/artifacts/provenance/artifact_manifest.json");
    let proof_manifest =
        project_root.join(".decapod/generated/artifacts/provenance/proof_manifest.json");
    let intent_convergence_manifest = project_root
        .join(".decapod/generated/artifacts/provenance/intent_convergence_checklist.json");

    let mut missing = Vec::new();
    if !artifact_manifest.exists() {
        missing.push(".decapod/generated/artifacts/provenance/artifact_manifest.json");
    }
    if !proof_manifest.exists() {
        missing.push(".decapod/generated/artifacts/provenance/proof_manifest.json");
    }
    if !intent_convergence_manifest.exists() {
        missing.push(".decapod/generated/artifacts/provenance/intent_convergence_checklist.json");
    }
    if !missing.is_empty() {
        return Err(error::DecapodError::ValidationError(format!(
            "release.lineage_sync missing required provenance manifests: {}",
            missing.join(", ")
        )));
    }

    let lineage = stamp_release_policy_lineage(
        project_root,
        [
            &artifact_manifest,
            &proof_manifest,
            &intent_convergence_manifest,
        ],
    )?;
    println!(
        "{}",
        serde_json::json!({
            "cmd": "release.lineage_sync",
            "status": "ok",
            "policy_lineage": {
                "policy_hash": lineage.policy_hash,
                "policy_revision": lineage.policy_revision,
                "risk_tier": lineage.risk_tier,
                "capsule_path": lineage.capsule_path,
                "capsule_hash": lineage.capsule_hash
            },
            "manifests": [
                ".decapod/generated/artifacts/provenance/artifact_manifest.json",
                ".decapod/generated/artifacts/provenance/proof_manifest.json",
                ".decapod/generated/artifacts/provenance/intent_convergence_checklist.json"
            ]
        })
    );
    Ok(())
}

fn sha256_file(path: &Path) -> Result<String, error::DecapodError> {
    let bytes = fs::read(path).map_err(error::DecapodError::IoError)?;
    let mut hasher = Sha256::new();
    hasher.update(bytes);
    Ok(format!("{:x}", hasher.finalize()))
}

fn sha256_text(input: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(input.as_bytes());
    format!("{:x}", hasher.finalize())
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct PolicyLineage {
    policy_hash: String,
    policy_revision: String,
    risk_tier: String,
    capsule_path: String,
    capsule_hash: String,
}

fn resolve_release_risk_tier() -> Result<String, error::DecapodError> {
    let tier = std::env::var("DECAPOD_RELEASE_RISK_TIER").unwrap_or_else(|_| "medium".to_string());
    let normalized = tier.trim().to_ascii_lowercase();
    if !matches!(normalized.as_str(), "low" | "medium" | "high" | "critical") {
        return Err(error::DecapodError::ValidationError(format!(
            "invalid DECAPOD_RELEASE_RISK_TIER '{}': expected low|medium|high|critical",
            tier
        )));
    }
    Ok(normalized)
}

fn resolve_release_capsule(project_root: &Path) -> Result<(String, String), error::DecapodError> {
    let fallback = core::context_capsule::query_embedded_capsule(
        project_root,
        "release provenance",
        "interfaces",
        Some("R_releasecheck"),
        None,
        8,
    )?;
    let fallback_path = core::context_capsule::context_capsule_path(project_root, &fallback);
    let capsule = if fallback_path.exists() {
        let raw = fs::read_to_string(&fallback_path).map_err(error::DecapodError::IoError)?;
        // If the on-disk capsule is empty (e.g. after a shallow checkout or
        // force-push that left a zero-length tracked file), fall through to
        // the freshly-generated capsule instead of failing with a parse error.
        if raw.trim().is_empty() {
            fallback
        } else {
            let parsed: core::context_capsule::DeterministicContextCapsule =
                serde_json::from_str(&raw).map_err(|e| {
                    error::DecapodError::ValidationError(format!(
                        "invalid release capsule JSON at '{}': {}",
                        fallback_path.display(),
                        e
                    ))
                })?;
            parsed.with_recomputed_hash().map_err(|e| {
                error::DecapodError::ValidationError(format!(
                    "failed to recompute release capsule hash at '{}': {}",
                    fallback_path.display(),
                    e
                ))
            })?
        }
    } else {
        fallback
    };
    let path = core::context_capsule::write_context_capsule(project_root, &capsule)?;
    let rel_path = path
        .strip_prefix(project_root)
        .map_err(|_| {
            error::DecapodError::ValidationError(format!(
                "release capsule path '{}' is outside project root",
                path.display()
            ))
        })?
        .to_string_lossy()
        .replace('\\', "/");
    Ok((rel_path, capsule.capsule_hash))
}

fn stamp_release_policy_lineage<const N: usize>(
    project_root: &Path,
    manifest_paths: [&Path; N],
) -> Result<PolicyLineage, error::DecapodError> {
    let policy_revision = "policy.release@v1".to_string();
    let risk_tier = resolve_release_risk_tier()?;
    let (capsule_path, capsule_hash) = resolve_release_capsule(project_root)?;
    let policy_hash = sha256_text(&format!(
        "{}|{}|{}",
        policy_revision, risk_tier, capsule_hash
    ));
    let lineage_json = serde_json::json!({
        "policy_hash": policy_hash,
        "policy_revision": policy_revision,
        "risk_tier": risk_tier,
        "capsule_path": capsule_path,
        "capsule_hash": capsule_hash
    });

    for manifest_path in manifest_paths {
        let raw = fs::read_to_string(manifest_path).map_err(error::DecapodError::IoError)?;
        let mut v: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
            error::DecapodError::ValidationError(format!(
                "failed to parse JSON manifest '{}': {}",
                manifest_path.display(),
                e
            ))
        })?;
        let obj = v.as_object_mut().ok_or_else(|| {
            error::DecapodError::ValidationError(format!(
                "manifest '{}' must be a JSON object",
                manifest_path.display()
            ))
        })?;
        obj.insert("policy_lineage".to_string(), lineage_json.clone());
        let updated = serde_json::to_vec_pretty(&v).map_err(|e| {
            error::DecapodError::ValidationError(format!(
                "failed to serialize stamped manifest '{}': {}",
                manifest_path.display(),
                e
            ))
        })?;
        fs::write(manifest_path, updated).map_err(error::DecapodError::IoError)?;
    }

    Ok(PolicyLineage {
        policy_hash: lineage_json["policy_hash"]
            .as_str()
            .unwrap_or("")
            .to_string(),
        policy_revision: lineage_json["policy_revision"]
            .as_str()
            .unwrap_or("")
            .to_string(),
        risk_tier: lineage_json["risk_tier"].as_str().unwrap_or("").to_string(),
        capsule_path: lineage_json["capsule_path"]
            .as_str()
            .unwrap_or("")
            .to_string(),
        capsule_hash: lineage_json["capsule_hash"]
            .as_str()
            .unwrap_or("")
            .to_string(),
    })
}

fn validate_policy_lineage(
    project_root: &Path,
    v: &serde_json::Value,
    manifest_label: &str,
) -> Result<PolicyLineage, error::DecapodError> {
    let lineage = v
        .get("policy_lineage")
        .and_then(|x| x.as_object())
        .ok_or_else(|| {
            error::DecapodError::ValidationError(format!(
                "{manifest_label} missing policy_lineage object"
            ))
        })?;

    let required = [
        "policy_hash",
        "policy_revision",
        "risk_tier",
        "capsule_path",
        "capsule_hash",
    ];
    for key in required {
        let value = lineage.get(key).and_then(|x| x.as_str()).unwrap_or("");
        if value.is_empty() || value.contains("TO_BE_FILLED") {
            return Err(error::DecapodError::ValidationError(format!(
                "{manifest_label} policy_lineage.{key} must be non-empty and non-placeholder"
            )));
        }
    }

    let policy_hash = lineage
        .get("policy_hash")
        .and_then(|x| x.as_str())
        .unwrap_or("");
    if policy_hash.len() != 64 || !policy_hash.chars().all(|c| c.is_ascii_hexdigit()) {
        return Err(error::DecapodError::ValidationError(format!(
            "{manifest_label} policy_lineage.policy_hash must be a 64-char hex digest"
        )));
    }

    let capsule_hash = lineage
        .get("capsule_hash")
        .and_then(|x| x.as_str())
        .unwrap_or("");
    if capsule_hash.len() != 64 || !capsule_hash.chars().all(|c| c.is_ascii_hexdigit()) {
        return Err(error::DecapodError::ValidationError(format!(
            "{manifest_label} policy_lineage.capsule_hash must be a 64-char hex digest"
        )));
    }

    let risk_tier = lineage
        .get("risk_tier")
        .and_then(|x| x.as_str())
        .unwrap_or("");
    if !matches!(risk_tier, "low" | "medium" | "high" | "critical") {
        return Err(error::DecapodError::ValidationError(format!(
            "{manifest_label} policy_lineage.risk_tier invalid: expected low|medium|high|critical"
        )));
    }

    let capsule_path = lineage
        .get("capsule_path")
        .and_then(|x| x.as_str())
        .unwrap_or("");
    let abs = project_root.join(capsule_path);
    if !abs.exists() {
        return Err(error::DecapodError::ValidationError(format!(
            "{manifest_label} policy_lineage.capsule_path '{}' does not exist",
            capsule_path
        )));
    }

    let raw_capsule = fs::read_to_string(&abs).map_err(error::DecapodError::IoError)?;
    // If the on-disk capsule is empty (e.g. shallow checkout or force-push
    // left a zero-length tracked file), skip integrity checks — the capsule
    // will be regenerated on the next resolve pass.
    if !raw_capsule.trim().is_empty() {
        let parsed: core::context_capsule::DeterministicContextCapsule =
            serde_json::from_str(&raw_capsule).map_err(|e| {
                error::DecapodError::ValidationError(format!(
                    "{manifest_label} policy_lineage capsule at '{}' is not valid deterministic capsule JSON: {}",
                    capsule_path, e
                ))
            })?;
        let normalized = parsed.with_recomputed_hash().map_err(|e| {
            error::DecapodError::ValidationError(format!(
                "{manifest_label} policy_lineage capsule hash computation failed for '{}': {}",
                capsule_path, e
            ))
        })?;

        if parsed.capsule_hash != normalized.capsule_hash {
            return Err(error::DecapodError::ValidationError(format!(
                "{manifest_label} policy_lineage capsule file '{}' has internal hash mismatch",
                capsule_path
            )));
        }
        if capsule_hash != normalized.capsule_hash {
            return Err(error::DecapodError::ValidationError(format!(
                "{manifest_label} policy_lineage capsule_hash mismatch for '{}'",
                capsule_path
            )));
        }
    }

    Ok(PolicyLineage {
        policy_hash: policy_hash.to_string(),
        policy_revision: lineage
            .get("policy_revision")
            .and_then(|x| x.as_str())
            .unwrap_or("")
            .to_string(),
        risk_tier: risk_tier.to_string(),
        capsule_path: capsule_path.to_string(),
        capsule_hash: capsule_hash.to_string(),
    })
}

fn validate_artifact_manifest(
    project_root: &Path,
    manifest_path: &Path,
) -> Result<PolicyLineage, error::DecapodError> {
    let raw = fs::read_to_string(manifest_path).map_err(error::DecapodError::IoError)?;
    let v: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
        error::DecapodError::ValidationError(format!("artifact manifest is not valid JSON: {e}"))
    })?;
    if v.get("schema_version").and_then(|x| x.as_str()) != Some("1.0.0") {
        return Err(error::DecapodError::ValidationError(
            "artifact manifest schema_version must be 1.0.0".to_string(),
        ));
    }
    if v.get("kind").and_then(|x| x.as_str()) != Some("artifact_manifest") {
        return Err(error::DecapodError::ValidationError(
            "artifact manifest kind must be artifact_manifest".to_string(),
        ));
    }
    let lineage = validate_policy_lineage(project_root, &v, "artifact manifest")?;

    let artifacts = v
        .get("artifacts")
        .and_then(|x| x.as_array())
        .ok_or_else(|| {
            error::DecapodError::ValidationError(
                "artifact manifest artifacts[] required".to_string(),
            )
        })?;
    if artifacts.is_empty() {
        return Err(error::DecapodError::ValidationError(
            "artifact manifest artifacts[] must not be empty".to_string(),
        ));
    }

    for entry in artifacts {
        let path = entry.get("path").and_then(|x| x.as_str()).ok_or_else(|| {
            error::DecapodError::ValidationError("artifact entry missing path".to_string())
        })?;
        let sha = entry
            .get("sha256")
            .and_then(|x| x.as_str())
            .ok_or_else(|| {
                error::DecapodError::ValidationError("artifact entry missing sha256".to_string())
            })?;
        if sha.is_empty() || sha.contains("TO_BE_FILLED") {
            return Err(error::DecapodError::ValidationError(format!(
                "artifact entry '{}' has placeholder sha256",
                path
            )));
        }
        let abs = project_root.join(path);
        if !abs.exists() {
            return Err(error::DecapodError::ValidationError(format!(
                "artifact entry '{}' does not exist",
                path
            )));
        }
        let actual = sha256_file(&abs)?;
        if actual != sha {
            return Err(error::DecapodError::ValidationError(format!(
                "artifact entry '{}' sha256 mismatch",
                path
            )));
        }
    }
    Ok(lineage)
}

fn validate_proof_manifest(
    project_root: &Path,
    manifest_path: &Path,
) -> Result<PolicyLineage, error::DecapodError> {
    let raw = fs::read_to_string(manifest_path).map_err(error::DecapodError::IoError)?;
    let v: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
        error::DecapodError::ValidationError(format!("proof manifest is not valid JSON: {e}"))
    })?;
    if v.get("schema_version").and_then(|x| x.as_str()) != Some("1.0.0") {
        return Err(error::DecapodError::ValidationError(
            "proof manifest schema_version must be 1.0.0".to_string(),
        ));
    }
    if v.get("kind").and_then(|x| x.as_str()) != Some("proof_manifest") {
        return Err(error::DecapodError::ValidationError(
            "proof manifest kind must be proof_manifest".to_string(),
        ));
    }
    let lineage = validate_policy_lineage(project_root, &v, "proof manifest")?;
    let proofs = v.get("proofs").and_then(|x| x.as_array()).ok_or_else(|| {
        error::DecapodError::ValidationError("proof manifest proofs[] required".to_string())
    })?;
    if proofs.is_empty() {
        return Err(error::DecapodError::ValidationError(
            "proof manifest proofs[] must not be empty".to_string(),
        ));
    }
    for p in proofs {
        let command = p.get("command").and_then(|x| x.as_str()).unwrap_or("");
        let result = p.get("result").and_then(|x| x.as_str()).unwrap_or("");
        if command.is_empty() || command.contains("TO_BE_FILLED") {
            return Err(error::DecapodError::ValidationError(
                "proof manifest command must be non-empty and non-placeholder".to_string(),
            ));
        }
        if result.is_empty() || result.contains("TO_BE_FILLED") {
            return Err(error::DecapodError::ValidationError(
                "proof manifest result must be non-empty and non-placeholder".to_string(),
            ));
        }
    }
    let env = v
        .get("environment")
        .and_then(|x| x.as_object())
        .ok_or_else(|| {
            error::DecapodError::ValidationError("proof manifest environment required".to_string())
        })?;
    for key in ["os", "rust"] {
        let value = env.get(key).and_then(|x| x.as_str()).unwrap_or("");
        if value.is_empty() || value.contains("TO_BE_FILLED") {
            return Err(error::DecapodError::ValidationError(format!(
                "proof manifest environment.{} must be non-empty and non-placeholder",
                key
            )));
        }
    }
    Ok(lineage)
}

fn validate_intent_convergence_manifest(
    project_root: &Path,
    manifest_path: &Path,
) -> Result<PolicyLineage, error::DecapodError> {
    let raw = fs::read_to_string(manifest_path).map_err(error::DecapodError::IoError)?;
    let v: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
        error::DecapodError::ValidationError(format!(
            "intent convergence manifest is not valid JSON: {e}"
        ))
    })?;
    if v.get("schema_version").and_then(|x| x.as_str()) != Some("1.0.0") {
        return Err(error::DecapodError::ValidationError(
            "intent convergence manifest schema_version must be 1.0.0".to_string(),
        ));
    }
    if v.get("kind").and_then(|x| x.as_str()) != Some("intent_convergence_checklist") {
        return Err(error::DecapodError::ValidationError(
            "intent convergence manifest kind must be intent_convergence_checklist".to_string(),
        ));
    }
    let lineage = validate_policy_lineage(project_root, &v, "intent convergence manifest")?;

    for key in ["pr", "intent", "scope", "checklist"] {
        if v.get(key).is_none() {
            return Err(error::DecapodError::ValidationError(format!(
                "intent convergence manifest missing '{}' field",
                key
            )));
        }
    }

    let checklist = v
        .get("checklist")
        .and_then(|x| x.as_array())
        .ok_or_else(|| {
            error::DecapodError::ValidationError(
                "intent convergence manifest checklist[] required".to_string(),
            )
        })?;
    if checklist.is_empty() {
        return Err(error::DecapodError::ValidationError(
            "intent convergence manifest checklist[] must not be empty".to_string(),
        ));
    }

    for item in checklist {
        let name = item.get("name").and_then(|x| x.as_str()).unwrap_or("");
        let status = item.get("status").and_then(|x| x.as_str()).unwrap_or("");
        let evidence = item.get("evidence").and_then(|x| x.as_str()).unwrap_or("");
        if name.is_empty() || status.is_empty() || evidence.is_empty() {
            return Err(error::DecapodError::ValidationError(
                "intent convergence checklist entries require name/status/evidence".to_string(),
            ));
        }
        if matches!(status, "pending" | "unknown") {
            return Err(error::DecapodError::ValidationError(format!(
                "intent convergence checklist item '{}' must be resolved (status={})",
                name, status
            )));
        }
    }
    Ok(lineage)
}

fn build_release_inventory(project_root: &Path) -> Result<serde_json::Value, error::DecapodError> {
    let mut paths = Vec::new();
    let mut roots = vec!["src", "tests"];
    if project_root.join("constitution").exists() {
        roots.push("constitution");
    }
    for root in roots {
        let p = project_root.join(root);
        if p.is_dir() {
            collect_files_recursive(&p, &mut paths)?;
        }
    }
    if project_root.join("assets/constitution.json").exists() {
        paths.push(PathBuf::from("assets/constitution.json"));
    }
    paths.sort();

    let mut top_files = Vec::new();
    let mut totals_by_root: BTreeMap<&'static str, u64> = BTreeMap::new();
    let mut rust_files = 0u64;
    let mut test_files = 0u64;

    for path in paths {
        let rel = match path.strip_prefix(project_root) {
            Ok(p) => p.to_path_buf(),
            Err(_) => continue,
        };
        let rel_s = rel.to_string_lossy().replace('\\', "/");
        let raw = fs::read_to_string(&path).unwrap_or_default();
        let loc = raw.lines().count() as u64;
        if rel_s.starts_with("src/") {
            *totals_by_root.entry("src_loc").or_insert(0) += loc;
        } else if rel_s.starts_with("tests/") {
            *totals_by_root.entry("tests_loc").or_insert(0) += loc;
        } else if rel_s.starts_with("constitution/") || rel_s == "assets/constitution.json" {
            *totals_by_root.entry("constitution_loc").or_insert(0) += loc;
        }
        if rel_s.ends_with(".rs") {
            rust_files += 1;
        }
        if rel_s.starts_with("tests/") {
            test_files += 1;
        }
        top_files.push((rel_s, loc));
    }

    top_files.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
    let top_files: Vec<serde_json::Value> = top_files
        .into_iter()
        .take(25)
        .map(|(path, loc)| serde_json::json!({ "path": path, "loc": loc }))
        .collect();

    let src_loc = *totals_by_root.get("src_loc").unwrap_or(&0);
    let tests_loc = *totals_by_root.get("tests_loc").unwrap_or(&0);
    let constitution_loc = *totals_by_root.get("constitution_loc").unwrap_or(&0);

    Ok(serde_json::json!({
        "schema_version": "1.0.0",
        "kind": "repo_inventory",
        "scope": ["src", "tests", "assets/constitution.json"],
        "totals": {
            "src_loc": src_loc,
            "tests_loc": tests_loc,
            "constitution_loc": constitution_loc,
            "total_loc": src_loc + tests_loc + constitution_loc,
            "rust_files": rust_files,
            "test_files": test_files
        },
        "top_files_by_loc": top_files
    }))
}

fn collect_files_recursive(root: &Path, out: &mut Vec<PathBuf>) -> Result<(), error::DecapodError> {
    if !root.exists() {
        return Ok(());
    }
    for entry in fs::read_dir(root).map_err(error::DecapodError::IoError)? {
        let entry = entry.map_err(error::DecapodError::IoError)?;
        let path = entry.path();
        if path.is_dir() {
            collect_files_recursive(&path, out)?;
        } else if path.is_file() {
            out.push(path);
        }
    }
    Ok(())
}

fn git_changed_paths(project_root: &Path) -> Vec<String> {
    let output = std::process::Command::new("git")
        .current_dir(project_root)
        .args(["status", "--porcelain"])
        .output();
    let Ok(output) = output else {
        return Vec::new();
    };
    if !output.status.success() {
        return Vec::new();
    }
    let raw = String::from_utf8_lossy(&output.stdout);
    let mut paths = Vec::new();
    for line in raw.lines() {
        if line.len() < 4 {
            continue;
        }
        let candidate = line[3..].trim();
        if let Some((_, to)) = candidate.split_once(" -> ") {
            paths.push(to.trim().to_string());
        } else {
            paths.push(candidate.to_string());
        }
    }
    paths
}

fn has_schema_or_interface_changes(paths: &[String]) -> bool {
    paths.iter().any(|path| {
        path == "assets/constitution.json"
            || path == "src/core/schemas.rs"
            || path == "src/core/rpc.rs"
            || path.starts_with("tests/golden/rpc/")
    })
}

fn changelog_mentions_schema_or_interface(changelog_raw: &str) -> bool {
    let lower = changelog_raw.to_ascii_lowercase();
    let Some(start) = lower.find("## [unreleased]") else {
        return false;
    };
    let section = &lower[start..];
    let next_heading = section[14..]
        .find("\n## ")
        .map(|idx| idx + 14)
        .unwrap_or(section.len());
    let unreleased = &section[..next_heading];
    unreleased.contains("schema") || unreleased.contains("interface")
}

#[derive(Debug, Clone, Serialize)]
struct ValidationHealAction {
    action: String,
    outcome: String,
    detail: String,
}

fn should_scaffold_validation_surfaces(project_root: &Path) -> bool {
    let required = [
        "AGENTS.md",
        ".decapod/README.md",
        ".decapod/generated/Dockerfile",
        ".decapod/generated/specs/README.md",
        ".decapod/generated/specs/INTENT.md",
        ".decapod/generated/specs/ARCHITECTURE.md",
        ".decapod/generated/specs/INTERFACES.md",
        ".decapod/generated/specs/VALIDATION.md",
        ".decapod/generated/specs/.manifest.json",
        ".decapod/generated/policy/context_capsule_policy.json",
    ];
    required.iter().any(|rel| !project_root.join(rel).exists())
}

fn heal_agents_contract(
    project_root: &Path,
) -> Result<Option<ValidationHealAction>, error::DecapodError> {
    let path = project_root.join("AGENTS.md");
    if !path.exists() {
        let content = core::assets::get_template("AGENTS.md").ok_or_else(|| {
            error::DecapodError::ValidationError("Missing AGENTS.md template".to_string())
        })?;
        atomic_write_file(&path, &content)?;
        return Ok(Some(ValidationHealAction {
            action: "heal_agents_contract".to_string(),
            outcome: "recreated".to_string(),
            detail: "Restored missing AGENTS.md from the canonical Decapod template.".to_string(),
        }));
    }

    let mut content = fs::read_to_string(&path).map_err(error::DecapodError::IoError)?;
    let mut anchors = Vec::new();
    for marker in [
        "Stop if",
        "via decapod CLI",
        "Interface abstraction boundary",
        "Strict Dependency: You are strictly bound to the Decapod governance kernel",
    ] {
        if !content.contains(marker) {
            anchors.push(marker);
        }
    }
    if anchors.is_empty() {
        return Ok(None);
    }

    content.push_str("\n\n<!-- decapod-validator-anchors\n");
    for anchor in &anchors {
        content.push_str(anchor);
        content.push('\n');
    }
    content.push_str("-->\n");
    atomic_write_file(&path, &content)?;
    Ok(Some(ValidationHealAction {
        action: "heal_agents_contract".to_string(),
        outcome: "updated".to_string(),
        detail: format!(
            "Added {} missing validator anchor(s) to AGENTS.md.",
            anchors.len()
        ),
    }))
}

fn heal_validation_scaffold(
    project_root: &Path,
) -> Result<Option<ValidationHealAction>, error::DecapodError> {
    if !should_scaffold_validation_surfaces(project_root) {
        return Ok(None);
    }

    let repo_ctx = infer_repo_context(project_root);
    let summary = scaffold::scaffold_project_entrypoints(&scaffold::ScaffoldOptions {
        target_dir: project_root.to_path_buf(),
        force: false,
        dry_run: false,
        agent_files: Vec::new(),
        created_backups: false,
        all: false,
        generate_specs: true,
        diagram_style: scaffold::DiagramStyle::Ascii,
        specs_seed: Some(scaffold::SpecsSeed {
            product_name: repo_ctx.product_name,
            product_summary: repo_ctx.product_summary,
            architecture_direction: repo_ctx.architecture_direction,
            product_type: repo_ctx.product_type,
            primary_languages: repo_ctx.primary_languages,
            detected_surfaces: repo_ctx.detected_surfaces,
            done_criteria: repo_ctx.done_criteria,
        }),
    })?;

    Ok(Some(ValidationHealAction {
        action: "heal_validation_scaffold".to_string(),
        outcome: "updated".to_string(),
        detail: format!(
            "Scaffolded missing validation surfaces (entrypoints_created={}, config_created={}, specs_created={}).",
            summary.entrypoints_created, summary.config_created, summary.specs_created
        ),
    }))
}

fn heal_override_checksum(
    project_root: &Path,
) -> Result<Option<ValidationHealAction>, error::DecapodError> {
    match docs_cli::sync_override_checksum(project_root, false)? {
        docs_cli::OverrideChecksumStatus::MissingOverride
        | docs_cli::OverrideChecksumStatus::Unchanged => Ok(None),
        docs_cli::OverrideChecksumStatus::Cached => Ok(Some(ValidationHealAction {
            action: "heal_override_checksum".to_string(),
            outcome: "cached".to_string(),
            detail: "Cached OVERRIDE.md checksum for deterministic governance reads.".to_string(),
        })),
        docs_cli::OverrideChecksumStatus::Updated => Ok(Some(ValidationHealAction {
            action: "heal_override_checksum".to_string(),
            outcome: "refreshed".to_string(),
            detail: "Refreshed OVERRIDE.md checksum after local override drift.".to_string(),
        })),
    }
}

fn heal_container_runtime_override(
    project_root: &Path,
) -> Result<Option<ValidationHealAction>, error::DecapodError> {
    match container::heal_container_runtime_override(project_root)? {
        container::ContainerRuntimeOverrideHeal::Cleared => Ok(Some(ValidationHealAction {
            action: "heal_container_runtime_override".to_string(),
            outcome: "cleared".to_string(),
            detail: "Removed stale container-runtime override because Docker/Podman support is available.".to_string(),
        })),
        container::ContainerRuntimeOverrideHeal::Unchanged => Ok(None),
    }
}

fn attempt_validation_failure_heal(
    report: &validate::ValidationReport,
    project_root: &Path,
    store: &Store,
) -> Result<Vec<ValidationHealAction>, error::DecapodError> {
    let mut actions = Vec::new();

    if report.failures.iter().any(|msg| {
        msg.contains("Repo store missing todo.db")
            || msg.contains("Repo todo.db does NOT match rebuild from todo.events.jsonl")
    }) {
        let rebuild = todo::rebuild_from_events(&store.root)?;
        actions.push(ValidationHealAction {
            action: "todo.rebuild".to_string(),
            outcome: "repaired".to_string(),
            detail: format!("Rebuilt todo.db from event log: {}", rebuild),
        });
    }

    if report
        .failures
        .iter()
        .any(|msg| msg.contains("AGENTS.md missing") || msg.contains("Invariant missing:"))
        && let Some(action) = heal_agents_contract(project_root)?
    {
        actions.push(action);
    }

    if report.failures.iter().any(|msg| {
        msg.contains("Missing required project specs file:")
            || msg.contains("Context capsule policy schema mismatch")
    }) && let Some(action) = heal_validation_scaffold(project_root)?
    {
        actions.push(action);
    }

    if report
        .failures
        .iter()
        .any(|msg| msg.contains("claim.git.container_workspace_required"))
        && let Some(action) = heal_container_runtime_override(project_root)?
    {
        actions.push(action);
    }

    Ok(actions)
}

fn render_validation_text(
    report: &validate::ValidationReport,
    actions: &[ValidationHealAction],
    verbose: bool,
) {
    use crate::core::ansi::AnsiExt;

    validate::render_validation_report(report, verbose);
    if !actions.is_empty() {
        if verbose {
            println!(
                "  {} {}",
                "repair".bright_blue().bold(),
                format!("{} action(s)", actions.len()).bright_white()
            );
            for action in actions {
                println!(
                    "  {} {} {}",
                    "↺".bright_blue(),
                    action.action.bright_cyan(),
                    action.detail
                );
            }
        } else {
            println!(
                "  {} {} action(s) applied; use `-v` for repair details",
                "repair".bright_blue().bold(),
                actions.len().to_string().bright_white()
            );
        }
    }
}

fn run_validate_command(
    validate_cli: ValidateCli,
    project_root: &Path,
    project_store: &Store,
) -> Result<(), error::DecapodError> {
    use crate::core::workspace;

    if std::env::var("DECAPOD_VALIDATE_SKIP_GIT_GATES").is_ok() {
        // Skip workspace check if gates are explicitly skipped
    } else {
        // FIRST: Check workspace enforcement (non-negotiable)
        let workspace_status = workspace::get_workspace_status(project_root)?;

        if !workspace_status.can_work {
            let blocker = workspace_status
                .blockers
                .first()
                .expect("Workspace should have a blocker if can_work is false");

            let response = serde_json::json!({
                "success": false,
                "gate": "workspace_protection",
                "error": blocker.message,
                "resolve_hint": blocker.resolve_hint,
                "branch": workspace_status.git.current_branch,
                "is_protected": workspace_status.git.is_protected,
                "in_container": workspace_status.container.in_container,
            });

            if validate_cli.format == "json" {
                println!("{}", serde_json::to_string_pretty(&response).unwrap());
            } else {
                eprintln!("validation needs attention: workspace protection");
                eprintln!("  branch: {}", workspace_status.git.current_branch);
                eprintln!("  reason: {}", blocker.message);
                eprintln!("  next: {}", blocker.resolve_hint);
            }

            std::process::exit(1);
        }
    }

    let decapod_root = project_root.to_path_buf();
    let store = match validate_cli.store.as_str() {
        "user" => {
            // User store uses a temp directory for blank-slate validation
            let tmp_root = std::env::temp_dir().join(format!(
                "decapod_validate_user_{}",
                crate::core::ulid::new_ulid()
            ));
            std::fs::create_dir_all(&tmp_root).map_err(error::DecapodError::IoError)?;
            Store {
                kind: StoreKind::User,
                root: tmp_root,
            }
        }
        _ => project_store.clone(),
    };

    let mut heal_actions = Vec::new();
    if let Some(action) = heal_override_checksum(project_root)? {
        heal_actions.push(action);
    }
    if let Some(action) = heal_validation_scaffold(project_root)? {
        heal_actions.push(action);
    }
    if let Some(action) = heal_agents_contract(project_root)? {
        heal_actions.push(action);
    }
    if let Some(action) = heal_container_runtime_override(project_root)? {
        heal_actions.push(action);
    }

    let mut report = run_validation_bounded(&store, &decapod_root, validate_cli.verbose)?;
    for _ in 0..2 {
        if report.fail_count == 0 {
            break;
        }
        let mut round_actions = attempt_validation_failure_heal(&report, project_root, &store)?;
        if round_actions.is_empty() {
            break;
        }
        heal_actions.append(&mut round_actions);
        report = run_validation_bounded(&store, &decapod_root, validate_cli.verbose)?;
    }

    if validate_cli.format == "json" {
        println!(
            "{}",
            serde_json::to_string_pretty(&serde_json::json!({
                "status": report.status,
                "self_heal": heal_actions,
                "report": report,
            }))
            .map_err(|e| error::DecapodError::ValidationError(format!(
                "validate JSON encode failed: {e}"
            )))?,
        );
    } else {
        render_validation_text(&report, &heal_actions, validate_cli.verbose);
    }

    if report.fail_count > 0 {
        std::process::exit(1);
    }
    mark_validation_completed(project_root)?;
    Ok(())
}

fn validate_timeout_secs() -> u64 {
    std::env::var("DECAPOD_VALIDATE_TIMEOUT_SECS")
        .ok()
        .or_else(|| std::env::var("DECAPOD_VALIDATE_TIMEOUT_SECONDS").ok())
        .and_then(|v| v.parse::<u64>().ok())
        .filter(|v| *v > 0)
        .unwrap_or(120)
}

fn validate_diagnostics_enabled() -> bool {
    std::env::var("DECAPOD_DIAGNOSTICS")
        .ok()
        .map(|v| matches!(v.as_str(), "1" | "true" | "TRUE" | "yes" | "YES"))
        .unwrap_or(false)
}

fn classify_validate_failure_reason(message: &str) -> &'static str {
    let lower = message.to_ascii_lowercase();
    if lower.contains("sqlite contention") || lower.contains("database is locked") {
        return "timeout_acquiring_lock";
    }
    if lower.contains("exceeded timeout") {
        return "timeout_running_validations";
    }
    if lower.contains("worker disconnected") {
        return "worker_disconnected";
    }
    "validate_failure"
}

fn lock_age_ms(project_root: &Path) -> Option<u64> {
    let data_dir = project_root.join(".decapod").join("data");
    let entries = fs::read_dir(data_dir).ok()?;
    let now = SystemTime::now();
    let mut max_age_ms: Option<u64> = None;
    for entry in entries.flatten() {
        let file_name = entry.file_name();
        let file_name = file_name.to_string_lossy();
        if !(file_name.ends_with("-wal")
            || file_name.ends_with("-shm")
            || file_name.ends_with("-journal"))
        {
            continue;
        }
        let Ok(meta) = entry.metadata() else {
            continue;
        };
        let Ok(modified) = meta.modified() else {
            continue;
        };
        let Ok(age) = now.duration_since(modified) else {
            continue;
        };
        let age_ms = age.as_millis() as u64;
        max_age_ms = Some(max_age_ms.map_or(age_ms, |existing| existing.max(age_ms)));
    }
    max_age_ms
}

fn write_validate_diagnostic_artifact(
    project_root: &Path,
    reason_code: &str,
    elapsed_ms: u64,
    timeout_secs: u64,
) -> Result<PathBuf, error::DecapodError> {
    let mut run_id_hasher = Sha256::new();
    run_id_hasher.update(crate::core::ulid::new_ulid().as_bytes());
    let run_id = hash_bytes_hex(&run_id_hasher.finalize())[..32].to_string();
    let diagnostics_dir = project_root.join(".decapod/generated/artifacts/diagnostics/validate");
    fs::create_dir_all(&diagnostics_dir).map_err(error::DecapodError::IoError)?;

    let mut payload = serde_json::json!({
        "schema_version": "1.0.0",
        "kind": "validate_diagnostic",
        "run_id": run_id,
        "op": "validate",
        "reason_code": reason_code,
        "elapsed_ms": elapsed_ms,
        "timeout_secs": timeout_secs,
        "lock_age_ms": lock_age_ms(project_root),
        "stale_lock_recovery_triggered": false
    });

    let payload_bytes = serde_json::to_vec(&payload).map_err(|e| {
        error::DecapodError::ValidationError(format!("Failed to encode validate diagnostics: {e}"))
    })?;
    let mut hasher = Sha256::new();
    hasher.update(payload_bytes);
    let artifact_hash = hash_bytes_hex(&hasher.finalize());
    payload["artifact_hash"] = serde_json::json!(artifact_hash);

    let relative_path = PathBuf::from(format!(
        ".decapod/generated/artifacts/diagnostics/validate/{run_id}.json"
    ));
    let artifact_path = project_root.join(&relative_path);
    let pretty = serde_json::to_vec_pretty(&payload).map_err(|e| {
        error::DecapodError::ValidationError(format!(
            "Failed to serialize validate diagnostics artifact: {e}"
        ))
    })?;
    fs::write(&artifact_path, pretty).map_err(error::DecapodError::IoError)?;
    Ok(relative_path)
}

fn attach_validate_diagnostic_if_enabled(
    err: error::DecapodError,
    project_root: &Path,
    elapsed_ms: u64,
    timeout_secs: u64,
) -> error::DecapodError {
    if !validate_diagnostics_enabled() {
        return err;
    }
    let error::DecapodError::ValidationError(message) = err else {
        return err;
    };
    if !message.contains("VALIDATE_TIMEOUT_OR_LOCK") {
        return error::DecapodError::ValidationError(message);
    }
    let reason_code = classify_validate_failure_reason(&message);
    match write_validate_diagnostic_artifact(project_root, reason_code, elapsed_ms, timeout_secs) {
        Ok(relative_path) => error::DecapodError::ValidationError(format!(
            "{} Diagnostics: {}",
            message,
            relative_path.display()
        )),
        Err(diag_err) => error::DecapodError::ValidationError(format!(
            "{} DiagnosticsWriteError: {}",
            message, diag_err
        )),
    }
}

fn normalize_validate_error(err: error::DecapodError) -> error::DecapodError {
    match err {
        error::DecapodError::RusqliteError(rusqlite::Error::SqliteFailure(code, msg)) => {
            let is_lock = code.code == rusqlite::ErrorCode::DatabaseBusy
                || code.extended_code == 522
                || msg
                    .as_deref()
                    .unwrap_or_default()
                    .to_ascii_lowercase()
                    .contains("locked");
            if is_lock {
                return error::DecapodError::ValidationError(
                    "VALIDATE_TIMEOUT_OR_LOCK: SQLite contention detected. Retry with backoff or inspect concurrent decapod processes.".to_string(),
                );
            }
            error::DecapodError::RusqliteError(rusqlite::Error::SqliteFailure(code, msg))
        }
        error::DecapodError::ValidationError(message) => {
            let lower = message.to_ascii_lowercase();
            if lower.contains("database is locked")
                || lower.contains("databasebusy")
                || lower.contains("sqlite_code=databasebusy")
            {
                return error::DecapodError::ValidationError(
                    "VALIDATE_TIMEOUT_OR_LOCK: SQLite contention detected. Retry with backoff or inspect concurrent decapod processes.".to_string(),
                );
            }
            error::DecapodError::ValidationError(message)
        }
        other => other,
    }
}

fn retry_transient_sqlite<T, F>(mut op: F, max_attempts: u32) -> Result<T, error::DecapodError>
where
    F: FnMut() -> Result<T, error::DecapodError>,
{
    let mut attempt = 0u32;
    loop {
        match op() {
            Ok(v) => return Ok(v),
            Err(e) if is_transient_sqlite_contention_error(&e) && attempt + 1 < max_attempts => {
                let delay_ms = (50u64 * 2u64.pow(attempt)).min(800);
                attempt += 1;
                thread::sleep(std::time::Duration::from_millis(delay_ms));
            }
            Err(e) => return Err(e),
        }
    }
}

fn is_transient_sqlite_contention_error(err: &error::DecapodError) -> bool {
    match err {
        error::DecapodError::RusqliteError(rusqlite::Error::SqliteFailure(code, msg)) => {
            if matches!(
                code.code,
                rusqlite::ErrorCode::DatabaseBusy | rusqlite::ErrorCode::DatabaseLocked
            ) || code.extended_code == 522
            {
                return true;
            }
            let lower = msg.as_deref().unwrap_or_default().to_ascii_lowercase();
            lower.contains("locked") || lower.contains("disk i/o error")
        }
        error::DecapodError::ValidationError(message) => {
            let lower = message.to_ascii_lowercase();
            lower.contains("database is locked")
                || lower.contains("databasebusy")
                || lower.contains("sqlite contention")
                || lower.contains("disk i/o error")
                || lower.contains("extended_code: 522")
        }
        other => {
            let lower = other.to_string().to_ascii_lowercase();
            lower.contains("database is locked")
                || lower.contains("databasebusy")
                || lower.contains("disk i/o error")
                || lower.contains("extended_code: 522")
        }
    }
}

fn run_validation_bounded(
    store: &Store,
    project_root: &Path,
    verbose: bool,
) -> Result<validate::ValidationReport, error::DecapodError> {
    let timeout_secs = validate_timeout_secs();
    let started = std::time::Instant::now();
    let (tx, rx) = mpsc::channel();
    let store_cloned = store.clone();
    let root = project_root.to_path_buf();

    std::thread::spawn(move || {
        let mut result = validate::run_validation(&store_cloned, &root, &root, verbose);
        for attempt in 1..=2 {
            let should_retry = match &result {
                Err(error::DecapodError::RusqliteError(err)) => {
                    format!("{err}").to_ascii_lowercase().contains("locked")
                }
                Err(error::DecapodError::ValidationError(msg)) => {
                    let lower = msg.to_ascii_lowercase();
                    lower.contains("database is locked")
                        || lower.contains("databasebusy")
                        || lower.contains("sqlite_code=databasebusy")
                }
                _ => false,
            };
            if !should_retry {
                break;
            }
            let backoff_ms = 200_u64 * attempt as u64;
            std::thread::sleep(std::time::Duration::from_millis(backoff_ms));
            result = validate::run_validation(&store_cloned, &root, &root, verbose);
        }
        let _ = tx.send(result);
    });

    let result = match rx.recv_timeout(std::time::Duration::from_secs(timeout_secs)) {
        Ok(result) => result.map_err(normalize_validate_error),
        Err(mpsc::RecvTimeoutError::Timeout) => Err(error::DecapodError::ValidationError(format!(
            "VALIDATE_TIMEOUT_OR_LOCK: validate exceeded timeout ({}s). Terminated to preserve proof-gate liveness.",
            timeout_secs
        ))),
        Err(mpsc::RecvTimeoutError::Disconnected) => Err(error::DecapodError::ValidationError(
            "VALIDATE_TIMEOUT_OR_LOCK: validate worker disconnected unexpectedly.".to_string(),
        )),
    };
    result.map_err(|err| {
        attach_validate_diagnostic_if_enabled(
            err,
            project_root,
            started.elapsed().as_millis() as u64,
            timeout_secs,
        )
    })
}

fn rpc_op_requires_constitutional_awareness(op: &str) -> bool {
    matches!(
        op,
        "workspace.publish"
            | "store.upsert"
            | "scaffold.apply_answer"
            | "scaffold.generate_artifacts"
    )
}

fn rpc_op_skips_mandate_enforcement(op: &str) -> bool {
    matches!(
        op,
        "context.resolve"
            | "context.scope"
            | "context.bindings"
            | "context.capsule.query"
            | "constitution.get"
            | "schema.get"
    )
}

fn enforce_constitutional_awareness_for_rpc(
    op: &str,
    project_root: &Path,
) -> Result<(), error::DecapodError> {
    if !rpc_op_requires_constitutional_awareness(op) {
        return Ok(());
    }

    let agent_id = current_agent_id();
    let rec = read_awareness_record(project_root, &agent_id)?;
    let Some(rec) = rec else {
        return Err(error::DecapodError::ValidationError(
            r#"Constitutional awareness required before mutating operations. Run `decapod validate`, then `decapod session acquire`, `decapod rpc --op agent.init`, `decapod rpc --op constitution.get --params '{"section":"core/DECAPOD"}'`, and `decapod rpc --op context.resolve`."#
                .to_string(),
        ));
    };

    if rec.validated_at_epoch_secs.is_none() {
        return Err(error::DecapodError::ValidationError(
            "Constitutional awareness incomplete: `decapod validate` has not completed for this agent context. Run `decapod validate` first."
                .to_string(),
        ));
    }

    if rec.core_constitution_ingested_at_epoch_secs.is_none() {
        return Err(error::DecapodError::ValidationError(
            r#"Constitutional awareness incomplete: core constitution RPC resolution missing. Run `decapod rpc --op constitution.get --params '{"section":"core/DECAPOD"}'` before mutating operations."#
                .to_string(),
        ));
    }

    if rec.context_resolved_at_epoch_secs.is_none() {
        return Err(error::DecapodError::ValidationError(
            "Constitutional awareness incomplete: `context.resolve` has not been executed after initialization. Run `decapod rpc --op context.resolve`."
                .to_string(),
        ));
    }

    if let Some(session) = read_agent_session(project_root, &agent_id)?
        && rec.session_token.as_deref() != Some(session.token.as_str())
    {
        return Err(error::DecapodError::ValidationError(
            "Constitutional awareness is stale for the active session. Re-run `decapod rpc --op agent.init` and `decapod rpc --op context.resolve`."
                .to_string(),
        ));
    }

    Ok(())
}

fn run_govern_command(
    govern_cli: GovernCli,
    project_store: &Store,
    store_root: &Path,
) -> Result<(), error::DecapodError> {
    match govern_cli.command {
        GovernCommand::Policy(policy_cli) => policy::run_policy_cli(project_store, policy_cli)?,
        GovernCommand::Health(health_cli) => health::run_health_cli(project_store, health_cli)?,
        GovernCommand::Proof(proof_cli) => proof::execute_proof_cli(&proof_cli, store_root)?,
        GovernCommand::Watcher(watcher_cli) => match watcher_cli.command {
            WatcherCommand::Run => {
                let report = watcher::run_watcher(project_store)?;
                println!("{}", serde_json::to_string_pretty(&report).unwrap());
            }
        },
        GovernCommand::Feedback(feedback_cli) => {
            feedback::initialize_feedback_db(store_root)?;
            match feedback_cli.command {
                FeedbackCommand::Add {
                    source,
                    text,
                    links,
                } => {
                    let id =
                        feedback::add_feedback(project_store, &source, &text, links.as_deref())?;
                    println!("Feedback recorded: {}", id);
                }
                FeedbackCommand::Propose => {
                    let proposal = feedback::propose_prefs(project_store)?;
                    println!("{}", proposal);
                }
            }
        }
        GovernCommand::Gatekeeper(gk_cli) => match gk_cli.command {
            GatekeeperCommand::Check {
                paths,
                max_diff_bytes,
                no_secrets,
                no_dangerous,
            } => {
                use crate::core::gatekeeper;

                let repo_root = project_store
                    .root
                    .parent()
                    .and_then(|p| p.parent())
                    .unwrap_or(&project_store.root);

                // Collect paths: explicit or git staged files
                let check_paths: Vec<std::path::PathBuf> = if let Some(explicit) = paths {
                    explicit.into_iter().map(std::path::PathBuf::from).collect()
                } else {
                    // Get staged files from git
                    let output = std::process::Command::new("git")
                        .args(["diff", "--cached", "--name-only"])
                        .current_dir(repo_root)
                        .output()
                        .map_err(error::DecapodError::IoError)?;
                    String::from_utf8_lossy(&output.stdout)
                        .lines()
                        .filter(|l| !l.is_empty())
                        .map(std::path::PathBuf::from)
                        .collect()
                };

                // Get diff size
                let diff_output = std::process::Command::new("git")
                    .args(["diff", "--cached", "--stat"])
                    .current_dir(repo_root)
                    .output()
                    .map_err(error::DecapodError::IoError)?;
                let diff_bytes = diff_output.stdout.len() as u64;

                let mut config = gatekeeper::GatekeeperConfig::default();
                if let Some(max) = max_diff_bytes {
                    config.max_diff_bytes = max;
                }
                config.scan_secrets = !no_secrets;
                config.scan_dangerous_patterns = !no_dangerous;

                let result =
                    gatekeeper::run_gatekeeper(repo_root, &check_paths, diff_bytes, &config)?;

                if result.passed {
                    println!(
                        "Gatekeeper: all checks passed ({} files scanned)",
                        check_paths.len()
                    );
                } else {
                    println!(
                        "Gatekeeper: {} violation(s) found:",
                        result.violations.len()
                    );
                    for v in &result.violations {
                        let loc = v.line.map(|l| format!(":{}", l)).unwrap_or_default();
                        println!("  [{}] {}{}: {}", v.kind, v.path.display(), loc, v.message);
                    }
                    return Err(error::DecapodError::ValidationError(format!(
                        "Gatekeeper: {} violation(s)",
                        result.violations.len()
                    )));
                }
            }
        },
        GovernCommand::Plan(plan_cli) => run_plan_command(plan_cli, project_store)?,
        GovernCommand::Workunit(workunit_cli) => run_workunit_command(workunit_cli, project_store)?,
        GovernCommand::Capsule(capsule_cli) => run_capsule_command(capsule_cli, project_store)?,
    }

    Ok(())
}

fn run_capsule_command(
    capsule_cli: CapsuleCli,
    project_store: &Store,
) -> Result<(), error::DecapodError> {
    let project_root = project_store
        .root
        .parent()
        .and_then(|p| p.parent())
        .ok_or_else(|| {
            error::DecapodError::ValidationError(
                "unable to resolve project root from store root".to_string(),
            )
        })?;

    match capsule_cli.command {
        CapsuleCommand::Query {
            topic,
            scope,
            risk_tier,
            task_id,
            workunit_id,
            limit,
            write,
        } => {
            let resolved_policy = core::capsule_policy::resolve_capsule_policy(
                project_root,
                &scope,
                risk_tier.as_deref(),
                limit,
                write,
            )?;
            let capsule = core::context_capsule::query_embedded_capsule_governed(
                project_root,
                &topic,
                &scope,
                task_id.as_deref(),
                workunit_id.as_deref(),
                resolved_policy.effective_limit,
                resolved_policy.binding,
            )?;
            if write {
                let path = core::context_capsule::write_context_capsule(project_root, &capsule)?;
                let workunit_binding = maybe_bind_capsule_to_workunit_state_ref(
                    project_root,
                    task_id.as_deref().or(workunit_id.as_deref()),
                    &path,
                )?;
                println!(
                    "{}",
                    serde_json::to_string_pretty(&serde_json::json!({
                        "status": "ok",
                        "path": path,
                        "workunit_state_ref_binding": workunit_binding,
                        "capsule": capsule,
                    }))
                    .unwrap()
                );
            } else {
                println!("{}", serde_json::to_string_pretty(&capsule).unwrap());
            }
        }
    }

    Ok(())
}

fn run_workunit_command(
    workunit_cli: WorkunitCli,
    project_store: &Store,
) -> Result<(), error::DecapodError> {
    let project_root = project_store
        .root
        .parent()
        .and_then(|p| p.parent())
        .ok_or_else(|| {
            error::DecapodError::ValidationError(
                "unable to resolve project root from store root".to_string(),
            )
        })?;

    match workunit_cli.command {
        WorkunitCommand::Init {
            task_id,
            intent_ref,
        } => {
            let manifest = core::workunit::init_workunit(project_root, &task_id, &intent_ref)?;
            let path = core::workunit::workunit_path(project_root, &task_id)?;
            println!(
                "{}",
                serde_json::to_string_pretty(&serde_json::json!({
                    "status": "ok",
                    "marker": "WORKUNIT_INITIALIZED",
                    "path": path,
                    "workunit": manifest,
                }))
                .unwrap()
            );
        }
        WorkunitCommand::Get { task_id } => {
            let manifest = core::workunit::load_workunit(project_root, &task_id)?;
            println!("{}", serde_json::to_string_pretty(&manifest).unwrap());
        }
        WorkunitCommand::Status { task_id } => {
            let manifest = core::workunit::load_workunit(project_root, &task_id)?;
            let path = core::workunit::workunit_path(project_root, &task_id)?;
            let hash = manifest.canonical_hash_hex().map_err(|e| {
                error::DecapodError::ValidationError(format!(
                    "failed to compute workunit hash: {}",
                    e
                ))
            })?;
            println!(
                "{}",
                serde_json::to_string_pretty(&serde_json::json!({
                    "status": "ok",
                    "task_id": manifest.task_id,
                    "workunit_status": manifest.status,
                    "manifest_hash": hash,
                    "path": path,
                }))
                .unwrap()
            );
        }
        WorkunitCommand::AttachSpec { task_id, reference } => {
            let manifest = core::workunit::add_spec_ref(project_root, &task_id, &reference)?;
            println!("{}", serde_json::to_string_pretty(&manifest).unwrap());
        }
        WorkunitCommand::AttachState { task_id, reference } => {
            let manifest = core::workunit::add_state_ref(project_root, &task_id, &reference)?;
            println!("{}", serde_json::to_string_pretty(&manifest).unwrap());
        }
        WorkunitCommand::SetProofPlan { task_id, gates } => {
            let manifest = core::workunit::set_proof_plan(project_root, &task_id, &gates)?;
            println!("{}", serde_json::to_string_pretty(&manifest).unwrap());
        }
        WorkunitCommand::RecordProof {
            task_id,
            gate,
            status,
            artifact,
        } => {
            let manifest = core::workunit::record_proof_result(
                project_root,
                &task_id,
                &gate,
                &status,
                artifact,
            )?;
            println!("{}", serde_json::to_string_pretty(&manifest).unwrap());
        }
        WorkunitCommand::Transition { task_id, to } => {
            let manifest = core::workunit::transition_status(project_root, &task_id, to.into())?;
            println!("{}", serde_json::to_string_pretty(&manifest).unwrap());
        }
    }

    Ok(())
}

fn run_plan_command(plan_cli: PlanCli, project_store: &Store) -> Result<(), error::DecapodError> {
    let project_root = project_store
        .root
        .parent()
        .and_then(|p| p.parent())
        .ok_or_else(|| {
            error::DecapodError::ValidationError(
                "unable to resolve project root from store root".to_string(),
            )
        })?;

    match plan_cli.command {
        PlanCommand::Init {
            title,
            intent,
            todo_ids,
            proof_hooks,
            unknowns,
            human_questions,
            forbidden_paths,
            file_touch_budget,
        } => {
            let plan = plan_governance::init_plan(
                project_root,
                plan_governance::InitPlanInput {
                    title,
                    intent,
                    todo_ids,
                    proof_hooks,
                    unknowns,
                    human_questions,
                    constraints: plan_governance::ScopeConstraints {
                        forbidden_paths,
                        file_touch_budget,
                    },
                },
            )?;
            println!("{}", serde_json::to_string_pretty(&plan).unwrap());
        }
        PlanCommand::Update {
            title,
            intent,
            todo_ids,
            proof_hooks,
            unknowns,
            human_questions,
            clear_unknowns,
            clear_questions,
            forbidden_paths,
            file_touch_budget,
        } => {
            let plan = plan_governance::patch_plan(
                project_root,
                plan_governance::PlanPatch {
                    title,
                    intent,
                    state: None,
                    todo_ids: if todo_ids.is_empty() {
                        None
                    } else {
                        Some(todo_ids)
                    },
                    proof_hooks: if proof_hooks.is_empty() {
                        None
                    } else {
                        Some(proof_hooks)
                    },
                    unknowns: if clear_unknowns {
                        Some(vec![])
                    } else if unknowns.is_empty() {
                        None
                    } else {
                        Some(unknowns)
                    },
                    human_questions: if clear_questions {
                        Some(vec![])
                    } else if human_questions.is_empty() {
                        None
                    } else {
                        Some(human_questions)
                    },
                    constraints: if forbidden_paths.is_empty() && file_touch_budget.is_none() {
                        None
                    } else {
                        Some(plan_governance::ScopeConstraints {
                            forbidden_paths,
                            file_touch_budget,
                        })
                    },
                },
            )?;
            println!("{}", serde_json::to_string_pretty(&plan).unwrap());
        }
        PlanCommand::SetState { state } => {
            let plan = plan_governance::patch_plan(
                project_root,
                plan_governance::PlanPatch {
                    state: Some(state.into()),
                    ..Default::default()
                },
            )?;
            println!("{}", serde_json::to_string_pretty(&plan).unwrap());
        }
        PlanCommand::Approve => {
            let plan = plan_governance::patch_plan(
                project_root,
                plan_governance::PlanPatch {
                    state: Some(plan_governance::PlanState::Approved),
                    ..Default::default()
                },
            )?;
            println!("{}", serde_json::to_string_pretty(&plan).unwrap());
        }
        PlanCommand::Status => {
            let plan = plan_governance::load_plan(project_root)?;
            println!(
                "{}",
                serde_json::to_string_pretty(&serde_json::json!({
                    "status": if plan.is_some() { "ok" } else { "missing" },
                    "plan": plan
                }))
                .unwrap()
            );
        }
        PlanCommand::CheckExecute { todo_id } => {
            let plan = plan_governance::ensure_execute_ready(plan_governance::ExecuteCheckInput {
                project_root,
                store_root: &project_store.root,
                todo_id: todo_id.as_deref(),
            })?;
            println!(
                "{}",
                serde_json::to_string_pretty(&serde_json::json!({
                    "status": "ok",
                    "marker": "EXECUTION_READY",
                    "state": format!("{:?}", plan.state).to_uppercase(),
                    "todo_ids": plan.todo_ids,
                    "proof_hooks": plan.proof_hooks,
                }))
                .unwrap()
            );
        }
    }

    Ok(())
}

fn run_data_command(
    data_cli: DataCli,
    project_store: &Store,
    project_root: &Path,
    store_root: &Path,
) -> Result<(), error::DecapodError> {
    match data_cli.command {
        DataCommand::Archive(archive_cli) => {
            archive::initialize_archive_db(store_root)?;
            match archive_cli.command {
                ArchiveCommand::List => {
                    let items = archive::list_archives(project_store)?;
                    println!("{}", serde_json::to_string_pretty(&items).unwrap());
                }
                ArchiveCommand::Verify => {
                    let failures = archive::verify_archives(project_store)?;
                    if failures.is_empty() {
                        println!("All archives verified successfully.");
                    } else {
                        println!("Archive verification failed:");
                        for f in failures {
                            println!("- {}", f);
                        }
                    }
                }
            }
        }
        DataCommand::Knowledge(knowledge_cli) => {
            db::initialize_knowledge_db(store_root)?;
            match knowledge_cli.command {
                KnowledgeCommand::Add {
                    id,
                    title,
                    text,
                    provenance,
                    claim_id,
                } => {
                    let result = knowledge::add_knowledge(
                        project_store,
                        knowledge::AddKnowledgeParams {
                            id: &id,
                            title: &title,
                            content: &text,
                            provenance: &provenance,
                            claim_id: claim_id.as_deref(),
                            merge_key: None,
                            conflict_policy: knowledge::KnowledgeConflictPolicy::Merge,
                            status: "active",
                            ttl_policy: "persistent",
                            expires_ts: None,
                        },
                    )?;
                    println!(
                        "Knowledge entry {}: {} (action: {})",
                        result.id, id, result.action
                    );
                }
                KnowledgeCommand::Search { query } => {
                    let results = knowledge::search_knowledge(
                        project_store,
                        &query,
                        knowledge::SearchOptions {
                            as_of: None,
                            window_days: None,
                            rank: "relevance",
                        },
                    )?;
                    println!("{}", serde_json::to_string_pretty(&results).unwrap());
                }
                KnowledgeCommand::Promote {
                    source_entry_id,
                    evidence_refs,
                    approved_by,
                    reason,
                } => {
                    let actor = current_agent_id();
                    let event = knowledge::record_promotion_event(
                        project_store,
                        knowledge::KnowledgePromotionEventInput {
                            source_entry_id: &source_entry_id,
                            evidence_refs: &evidence_refs,
                            approved_by: &approved_by,
                            actor: &actor,
                            reason: &reason,
                        },
                    )?;
                    println!("{}", serde_json::to_string_pretty(&event).unwrap());
                }
            }
        }
        DataCommand::Context(context_cli) => {
            let manager = context::ContextManager::new(store_root)?;
            match context_cli.command {
                ContextCommand::Audit { profile, files } => {
                    let total = manager.audit_session(&files)?;
                    match manager.get_profile(&profile) {
                        Some(p) => {
                            println!(
                                "Total tokens for profile '{}': {} / {} (budget)",
                                profile, total, p.budget_tokens
                            );
                            if total > p.budget_tokens {
                                println!("⚠ OVER BUDGET");
                            }
                        }
                        None => {
                            println!("Total tokens: {} (Profile '{}' not found)", total, profile);
                        }
                    }
                }
                ContextCommand::Pack { path, summary } => {
                    let archive_path = manager
                        .pack_and_archive(project_store, &path, &summary)
                        .map_err(|err| match err {
                            error::DecapodError::ContextPackError(msg) => {
                                error::DecapodError::ContextPackError(format!(
                                    "Context pack failed: {}",
                                    msg
                                ))
                            }
                            other => other,
                        })?;
                    println!("Session archived to: {}", archive_path.display());
                }
                ContextCommand::Restore {
                    id,
                    profile,
                    current_files,
                } => {
                    let content = manager.restore_archive(&id, &profile, &current_files)?;
                    println!(
                        "--- RESTORED CONTENT (Archive: {}) ---\n{}\n--- END RESTORED ---",
                        id, content
                    );
                }
            }
        }
        DataCommand::Schema(schema_cli) => {
            let schemas = schema_catalog();

            let output = if let Some(sub) = schema_cli.subsystem {
                schemas
                    .get(sub.as_str())
                    .cloned()
                    .unwrap_or(serde_json::json!({ "error": "subsystem not found" }))
            } else {
                let mut envelope = deterministic_schema_envelope();
                if !schema_cli.deterministic {
                    envelope.as_object_mut().unwrap().insert(
                        "generated_at".to_string(),
                        serde_json::json!(format!("{:?}", std::time::SystemTime::now())),
                    );
                }
                envelope
            };

            match schema_cli.format.as_str() {
                "json" => println!("{}", serde_json::to_string_pretty(&output).unwrap()),
                "md" => {
                    println!("{}", schema_to_markdown(&output));
                }
                other => {
                    return Err(error::DecapodError::ValidationError(format!(
                        "Unsupported schema format '{}'. Use 'json' or 'md'.",
                        other
                    )));
                }
            }
        }
        DataCommand::Repo(repo_cli) => match repo_cli.command {
            RepoCommand::Map => {
                let map = repomap::generate_map(project_root);
                println!("{}", serde_json::to_string_pretty(&map).unwrap());
            }
            RepoCommand::Graph => {
                let graph = repomap::generate_doc_graph(project_root);
                println!("{}", graph.mermaid);
            }
        },
        DataCommand::Broker(broker_cli) => match broker_cli.command {
            BrokerCommand::Audit => {
                let audit_log = store_root.join("broker.events.jsonl");
                if audit_log.exists() {
                    let content = std::fs::read_to_string(audit_log)?;
                    println!("{}", content);
                } else {
                    println!("No audit log found.");
                }
            }
            BrokerCommand::Verify => {
                let broker = core::broker::DbBroker::new(store_root);
                let report = broker.verify_replay()?;
                println!("{}", serde_json::to_string_pretty(&report).unwrap());
                if !report.divergences.is_empty() {
                    return Err(error::DecapodError::ValidationError(format!(
                        "Audit log integrity check failed: {} divergence(s) detected",
                        report.divergences.len()
                    )));
                }
            }
        },
        DataCommand::Aptitude(aptitude_cli) => {
            aptitude::run_aptitude_cli(project_store, aptitude_cli)?;
        }
        DataCommand::Federation(federation_cli) => {
            federation::run_federation_cli(project_store, federation_cli)?;
        }
        DataCommand::Primitives(primitives_cli) => {
            primitives::run_primitives_cli(project_store, primitives_cli)?;
        }
    }

    Ok(())
}

fn schema_to_markdown(schema: &serde_json::Value) -> String {
    fn render_value(v: &serde_json::Value) -> String {
        match v {
            serde_json::Value::Object(map) => {
                let mut keys: Vec<_> = map.keys().cloned().collect();
                keys.sort();
                let mut out = String::new();
                for key in keys {
                    let value = &map[&key];
                    match value {
                        serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
                            out.push_str(&format!("- **{}**:\n", key));
                            for line in render_value(value).lines() {
                                out.push_str(&format!("  {}\n", line));
                            }
                        }
                        _ => out.push_str(&format!("- **{}**: `{}`\n", key, value)),
                    }
                }
                out
            }
            serde_json::Value::Array(items) => {
                let mut out = String::new();
                for item in items {
                    match item {
                        serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
                            out.push_str("- item:\n");
                            for line in render_value(item).lines() {
                                out.push_str(&format!("  {}\n", line));
                            }
                        }
                        _ => out.push_str(&format!("- `{}`\n", item)),
                    }
                }
                out
            }
            _ => format!("- `{}`\n", v),
        }
    }

    let mut out = String::from("# Decapod Schema\n\n");
    out.push_str(&render_value(schema));
    out
}

pub(crate) fn deterministic_schema_envelope() -> serde_json::Value {
    let root = cli_command_registry();
    let command_registry = root
        .get("subcommands")
        .cloned()
        .unwrap_or(serde_json::Value::Array(vec![]));
    serde_json::json!({
        "schema_version": "1.0.0",
        "subsystems": schema_catalog(),
        "deprecations": deprecation_metadata(),
        "command_registry": command_registry
    })
}

fn schema_catalog() -> std::collections::BTreeMap<&'static str, serde_json::Value> {
    let mut schemas = std::collections::BTreeMap::new();
    schemas.insert("todo", todo::schema());
    schemas.insert("cron", cron::schema());
    schemas.insert("reflex", reflex::schema());
    schemas.insert("workflow", workflow::schema());
    schemas.insert("container", container::schema());
    schemas.insert("health", health::health_schema());
    schemas.insert("broker", core::broker::schema());
    schemas.insert("external_action", core::external_action::schema());
    schemas.insert("context", context::schema());
    schemas.insert("policy", policy::schema());
    schemas.insert("knowledge", knowledge::schema());
    schemas.insert("repomap", repomap::schema());
    schemas.insert("watcher", watcher::schema());
    schemas.insert("archive", archive::schema());
    schemas.insert("feedback", feedback::schema());
    schemas.insert("aptitude", aptitude::schema());
    schemas.insert("memory", aptitude::schema());
    schemas.insert("federation", federation::schema());
    schemas.insert("primitives", primitives::schema());
    schemas.insert("decide", decide::schema());
    schemas.insert("docs", docs_cli::schema());
    schemas.insert("deprecations", deprecation_metadata());
    schemas.insert("lcm", lcm::schema());
    schemas.insert("map", map_ops::schema());
    schemas.insert("eval", eval::schema());
    schemas.insert("internalize", internalize::schema());
    schemas.insert(
        "command_registry",
        serde_json::json!({
            "name": "command_registry",
            "version": "0.1.0",
            "description": "Machine-readable CLI command registry generated from clap command definitions",
            "root": cli_command_registry()
        }),
    );
    schemas
}

fn deprecation_metadata() -> serde_json::Value {
    serde_json::json!({
        "name": "deprecations",
        "version": "0.1.0",
        "description": "Deprecated command surfaces and replacement pointers",
        "entries": [
            {
                "surface": "command",
                "path": "decapod heartbeat",
                "status": "deprecated",
                "replacement": "decapod govern health summary",
                "notes": "Heartbeat command family was consolidated into govern health"
            },
            {
                "surface": "command",
                "path": "decapod trust",
                "status": "deprecated",
                "replacement": "decapod govern health autonomy",
                "notes": "Trust command family was consolidated into govern health"
            },
            {
                "surface": "module",
                "path": "src/plugins/heartbeat.rs",
                "status": "deprecated",
                "replacement": "src/plugins/health.rs"
            }
        ]
    })
}

fn cli_command_registry() -> serde_json::Value {
    let command = Cli::command();
    command_to_registry(&command)
}

fn command_to_registry(command: &clap::Command) -> serde_json::Value {
    let mut subcommands: Vec<serde_json::Value> = command
        .get_subcommands()
        .filter(|sub| !sub.is_hide_set())
        .map(command_to_registry)
        .collect();
    subcommands.sort_by(|a, b| {
        let a_name = a
            .get("name")
            .and_then(serde_json::Value::as_str)
            .unwrap_or_default();
        let b_name = b
            .get("name")
            .and_then(serde_json::Value::as_str)
            .unwrap_or_default();
        a_name.cmp(b_name)
    });

    let mut options: Vec<serde_json::Value> = command
        .get_arguments()
        .filter(|arg| !arg.is_hide_set())
        .map(|arg| {
            let mut flags = Vec::new();
            if let Some(long) = arg.get_long() {
                flags.push(format!("--{}", long));
            }
            if let Some(short) = arg.get_short() {
                flags.push(format!("-{}", short));
            }
            if flags.is_empty() {
                flags.push(arg.get_id().to_string());
            }

            let value_names = arg
                .get_value_names()
                .map(|values| values.iter().map(|v| v.to_string()).collect::<Vec<_>>())
                .unwrap_or_default();

            serde_json::json!({
                "id": arg.get_id().to_string(),
                "flags": flags,
                "required": arg.is_required_set(),
                "help": arg.get_help().map(|help| help.to_string()),
                "value_names": value_names
            })
        })
        .collect();

    options.sort_by(|a, b| {
        let a_id = a
            .get("id")
            .and_then(serde_json::Value::as_str)
            .unwrap_or_default();
        let b_id = b
            .get("id")
            .and_then(serde_json::Value::as_str)
            .unwrap_or_default();
        a_id.cmp(b_id)
    });

    let aliases: Vec<String> = command.get_all_aliases().map(str::to_string).collect();

    serde_json::json!({
        "name": command.get_name(),
        "about": command.get_about().map(|about| about.to_string()),
        "aliases": aliases,
        "options": options,
        "subcommands": subcommands
    })
}

fn run_auto_command(auto_cli: AutoCli, project_store: &Store) -> Result<(), error::DecapodError> {
    match auto_cli.command {
        AutoCommand::Cron(cron_cli) => cron::run_cron_cli(project_store, cron_cli)?,
        AutoCommand::Reflex(reflex_cli) => reflex::run_reflex_cli(project_store, reflex_cli),
        AutoCommand::Workflow(workflow_cli) => {
            workflow::run_workflow_cli(project_store, workflow_cli)?
        }
        AutoCommand::Container(container_cli) => {
            container::run_container_cli(project_store, container_cli)?
        }
    }

    Ok(())
}

fn run_qa_command(
    qa_cli: QaCli,
    project_store: &Store,
    project_root: &Path,
) -> Result<(), error::DecapodError> {
    match qa_cli.command {
        QaCommand::Verify(verify_cli) => {
            verify::run_verify_cli(project_store, project_root, verify_cli)?
        }
        QaCommand::Check {
            crate_description,
            commands,
            all,
        } => run_check(crate_description, commands, all)?,
        QaCommand::Gatling(ref gatling_cli) => plugins::gatling::run_gatling_cli(gatling_cli)?,
    }

    Ok(())
}

fn run_hook_install(
    commit_msg: bool,
    pre_commit: bool,
    uninstall: bool,
) -> Result<(), error::DecapodError> {
    let git_dir_output = std::process::Command::new("git")
        .args(["rev-parse", "--git-dir"])
        .output()
        .map_err(error::DecapodError::IoError)?;

    if !git_dir_output.status.success() {
        return Err(error::DecapodError::ValidationError(
            "Not in a git repository".to_string(),
        ));
    }

    let git_dir = String::from_utf8_lossy(&git_dir_output.stdout)
        .trim()
        .to_string();
    let hooks_dir = PathBuf::from(git_dir).join("hooks");
    fs::create_dir_all(&hooks_dir).map_err(error::DecapodError::IoError)?;

    if uninstall {
        let commit_msg_path = hooks_dir.join("commit-msg");
        let pre_commit_path = hooks_dir.join("pre-commit");
        let mut removed_any = false;

        if commit_msg_path.exists() {
            fs::remove_file(&commit_msg_path).map_err(error::DecapodError::IoError)?;
            println!("✓ Removed commit-msg hook");
            removed_any = true;
        }
        if pre_commit_path.exists() {
            fs::remove_file(&pre_commit_path).map_err(error::DecapodError::IoError)?;
            println!("✓ Removed pre-commit hook");
            removed_any = true;
        }
        if !removed_any {
            println!("No hooks found to remove");
        }
        return Ok(());
    }

    if commit_msg {
        let hook_content = r#"#!/bin/sh
MSG_FILE="$1"
SUBJECT="$(head -n1 "$MSG_FILE")"
if printf '%s' "$SUBJECT" | grep -Eq '^(feat|fix|docs|style|refactor|test|chore|ci|build|perf|revert)(\([^)]+\))?: .+'; then
  exit 0
fi
echo "commit-msg hook: expected conventional commit subject"
echo "got: $SUBJECT"
exit 1
"#;
        let hook_path = hooks_dir.join("commit-msg");
        let mut file = fs::File::create(&hook_path).map_err(error::DecapodError::IoError)?;
        file.write_all(hook_content.as_bytes())
            .map_err(error::DecapodError::IoError)?;
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mut perms = fs::metadata(&hook_path)
                .map_err(error::DecapodError::IoError)?
                .permissions();
            perms.set_mode(0o755);
            fs::set_permissions(&hook_path, perms).map_err(error::DecapodError::IoError)?;
        }
        println!("✓ Installed commit-msg hook for conventional commits");
    }

    if pre_commit {
        let hook_content = r#"#!/bin/sh
set -e
cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
"#;
        let hook_path = hooks_dir.join("pre-commit");
        let mut file = fs::File::create(&hook_path).map_err(error::DecapodError::IoError)?;
        file.write_all(hook_content.as_bytes())
            .map_err(error::DecapodError::IoError)?;
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mut perms = fs::metadata(&hook_path)
                .map_err(error::DecapodError::IoError)?
                .permissions();
            perms.set_mode(0o755);
            fs::set_permissions(&hook_path, perms).map_err(error::DecapodError::IoError)?;
        }
        println!("✓ Installed pre-commit hook (fmt + clippy)");
    }

    if !commit_msg && !pre_commit {
        println!("No hooks specified. Use --commit-msg and/or --pre-commit");
    }

    Ok(())
}

fn run_check(
    crate_description: bool,
    commands: bool,
    all: bool,
) -> Result<(), error::DecapodError> {
    if crate_description || all {
        let expected = "Decapod is a Rust-built governance runtime for AI agents: repo-native state, enforced workflow, proof gates, safe coordination.";

        let output = std::process::Command::new("cargo")
            .args(["metadata", "--no-deps", "--format-version", "1"])
            .output()
            .map_err(|e| error::DecapodError::IoError(std::io::Error::other(e)))?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(error::DecapodError::ValidationError(format!(
                "cargo metadata failed: {}",
                stderr.trim()
            )));
        }

        let json_str = String::from_utf8_lossy(&output.stdout);

        if json_str.contains(expected) {
            println!("✓ Crate description matches");
        } else {
            println!("✗ Crate description mismatch!");
            println!("  Expected: {}", expected);
            return Err(error::DecapodError::ValidationError(
                "Crate description check failed".into(),
            ));
        }
    }

    if commands || all {
        run_command_help_smoke()?;
        println!("✓ Command help surfaces are valid");
    }

    if all && !(crate_description || commands) {
        println!("Note: --all enables all checks");
    }

    Ok(())
}

fn run_command_help_smoke() -> Result<(), error::DecapodError> {
    fn walk(cmd: &clap::Command, prefix: Vec<String>, all_paths: &mut Vec<Vec<String>>) {
        if cmd.get_name() != "help" {
            all_paths.push(prefix.clone());
        }
        for sub in cmd.get_subcommands().filter(|sub| !sub.is_hide_set()) {
            let mut next = prefix.clone();
            next.push(sub.get_name().to_string());
            walk(sub, next, all_paths);
        }
    }

    let exe = std::env::current_exe().map_err(error::DecapodError::IoError)?;
    let mut command_paths = Vec::new();
    walk(&Cli::command(), Vec::new(), &mut command_paths);
    command_paths.sort();
    command_paths.dedup();

    let mut handles = Vec::new();
    for path in &command_paths {
        handles.push(std::thread::spawn({
            let path = path.clone();
            let exe = exe.clone();
            move || {
                let mut args = path.clone();
                args.push("--help".to_string());
                let output = std::process::Command::new(&exe)
                    .args(&args)
                    .output()
                    .map_err(error::DecapodError::IoError)?;
                if !output.status.success() {
                    return Err(error::DecapodError::ValidationError(format!(
                        "help smoke failed for `decapod {}`: {}",
                        path.join(" "),
                        String::from_utf8_lossy(&output.stderr).trim()
                    )));
                }
                Ok(())
            }
        }));
    }
    for handle in handles {
        handle
            .join()
            .map_err(|_| error::DecapodError::ValidationError("thread panicked".into()))??;
    }
    Ok(())
}

/// Show version information
fn show_version_info() -> Result<(), error::DecapodError> {
    println!("Decapod version: {}", migration::DECAPOD_VERSION);
    println!("  Update: cargo install decapod");

    Ok(())
}

/// Run workspace command
fn run_workspace_command(
    cli: WorkspaceCli,
    project_root: &Path,
) -> Result<(), error::DecapodError> {
    use crate::core::workspace;

    match cli.command {
        WorkspaceCommand::Ensure { branch, container } => {
            let agent_id =
                std::env::var("DECAPOD_AGENT_ID").unwrap_or_else(|_| "unknown".to_string());

            let config = if branch.is_some() || container {
                Some(workspace::WorkspaceConfig {
                    branch: branch.unwrap_or_else(|| "".to_string()), // ensure_workspace handles empty branch if needed, but actually it creates one
                    use_container: container,
                    base_image: if container {
                        Some("rust:1.91-slim".to_string())
                    } else {
                        None
                    },
                })
            } else {
                None
            };

            let status = workspace::ensure_workspace(project_root, config, &agent_id)?;

            println!(
                "{}",
                serde_json::json!({
                    "status": if status.can_work { "ok" } else { "pending" },
                    "branch": status.git.current_branch,
                    "is_protected": status.git.is_protected,
                    "can_work": status.can_work,
                    "in_container": status.container.in_container,
                    "docker_available": status.container.docker_available,
                    "worktree_path": status.git.worktree_path,
                    "required_actions": status.required_actions,
                })
            );
        }
        WorkspaceCommand::Status => {
            let status = workspace::get_workspace_status(project_root)?;

            println!(
                "{}",
                serde_json::json!({
                    "can_work": status.can_work,
                    "git_branch": status.git.current_branch,
                    "git_is_protected": status.git.is_protected,
                    "git_has_local_mods": status.git.has_local_mods,
                    "in_container": status.container.in_container,
                    "container_image": status.container.image,
                    "docker_available": status.container.docker_available,
                    "blockers": status.blockers.len(),
                    "required_actions": status.required_actions,
                })
            );
        }
        WorkspaceCommand::Publish { title, description } => {
            let project_store = Store {
                kind: StoreKind::Repo,
                root: project_root.join(".decapod").join("data"),
            };
            plan_governance::ensure_execute_ready(plan_governance::ExecuteCheckInput {
                project_root,
                store_root: &project_store.root,
                todo_id: None,
            })?;
            let report = run_validation_bounded(&project_store, project_root, false)?;
            if report.fail_count > 0 {
                return Err(error::DecapodError::ValidationError(format!(
                    "{} test(s) failed before workspace publish.",
                    report.fail_count
                )));
            }
            let result = workspace::publish_workspace(project_root, title, description)?;
            println!(
                "{}",
                serde_json::json!({
                    "status": "ok",
                    "branch": result.branch,
                    "commit_hash": result.commit_hash,
                    "remote_url": result.remote_url,
                    "pr_url": result.pr_url,
                })
            );
        }
    }

    Ok(())
}

/// Run STATE_COMMIT commands (prove/verify)
fn run_state_commit_command(
    cli: StateCommitCli,
    project_root: &Path,
) -> Result<(), error::DecapodError> {
    match cli.command {
        StateCommitCommand::Prove { base, head, output } => {
            let head = head.unwrap_or_else(|| {
                state_commit::run_git(project_root, &["rev-parse", "HEAD"])
                    .unwrap_or_else(|_| "HEAD".to_string())
            });

            println!("Computing STATE_COMMIT:");
            println!("  base: {}", base);
            println!("  head: {}", head);

            // Use library function
            let input = state_commit::StateCommitInput {
                base_sha: base,
                head_sha: head.clone(),
                ignore_policy_hash: "da39a3ee5e6b4b0d3255bfef95601890afd80709".to_string(), // empty
            };

            let result = state_commit::prove(&input, project_root)
                .map_err(error::DecapodError::ValidationError)?;

            println!("  files: {}", result.entries.len());

            // Write output
            std::fs::write(&output, &result.scope_record_bytes)
                .map_err(error::DecapodError::IoError)?;

            println!("  scope_record_hash: {}", result.scope_record_hash);
            println!("  state_commit_root: {}", result.state_commit_root);
            println!("  output: {}", output.display());

            Ok(())
        }
        StateCommitCommand::Verify {
            scope_record,
            expected_root,
        } => {
            // Read scope record
            let cbor_bytes = std::fs::read(&scope_record).map_err(error::DecapodError::IoError)?;

            // Use library function for verification
            let record_hash = if let Some(ref exp) = expected_root {
                match state_commit::verify(&cbor_bytes, exp) {
                    Ok(h) => h,
                    Err(e) => {
                        println!("STATE_COMMIT verification:");
                        println!("  scope_record: {}", scope_record.display());
                        println!("  ❌ MISMATCH: {}", e);
                        return Err(error::DecapodError::ValidationError(e));
                    }
                }
            } else {
                use sha2::{Digest, Sha256};
                let mut hasher = Sha256::new();
                hasher.update(&cbor_bytes);
                format!("{:x}", hasher.finalize())
            };

            println!("STATE_COMMIT verification:");
            println!("  scope_record: {}", scope_record.display());
            println!("  scope_record_hash: {}", record_hash);
            println!("  ✅ VERIFIED");

            Ok(())
        }
        StateCommitCommand::Explain { scope_record } => {
            // Read and parse scope_record
            let cbor_bytes = std::fs::read(&scope_record).map_err(error::DecapodError::IoError)?;

            // Compute hashes
            use sha2::{Digest, Sha256};
            let mut hasher = Sha256::new();
            hasher.update(&cbor_bytes);
            let scope_record_hash = format!("{:x}", hasher.finalize());

            // Parse basic structure (simplified - looks for embedded strings)
            let content = String::from_utf8_lossy(&cbor_bytes);

            println!("STATE_COMMIT Explanation:");
            println!("  File: {}", scope_record.display());
            println!("  Size: {} bytes", cbor_bytes.len());
            println!("  scope_record_hash: {}", scope_record_hash);
            println!();

            // Try to extract version and SHAs from the CBOR structure
            if let Some(version_pos) = content.find("state_commit.")
                && let Some(end_pos) = content[version_pos..].find('\0')
            {
                println!(
                    "  algo_version: {}",
                    &content[version_pos..version_pos + end_pos]
                );
            }

            // Count entries (looking for patterns in the binary data)
            let entry_count = content.matches("kind=").count();
            println!("  Estimated entries: {}", entry_count);
            println!();

            println!("Note: scope_record_hash is sha256(scope_record_bytes)");
            println!("      state_commit_root is the Merkle root of entry hashes");

            Ok(())
        }
    }
}

// --- RPC Handler Context and Extracted Handlers ---

/// Shared context threaded through all RPC handlers.
struct RpcCtx<'a> {
    project_root: &'a Path,
    store: &'a Store,
    request: &'a crate::core::rpc::RpcRequest,
    mandates: Vec<crate::core::docs::Mandate>,
}

mod rpc_handlers {
    use super::RpcCtx;
    use super::*;
    use crate::core::assurance::{AssuranceEngine, AssuranceEvaluateInput};
    use crate::core::interview;
    use crate::core::mentor;
    use crate::core::rpc::*;
    use crate::core::standards;
    use crate::core::workspace;

    pub(crate) fn handle_agent_init(ctx: &RpcCtx) -> Result<RpcResponse, error::DecapodError> {
        let _params: AgentInitParams = serde_json::from_value(ctx.request.params.clone())
            .map_err(|e| error::DecapodError::ValidationError(format!("Invalid params: {}", e)))?;

        let workspace_status = workspace::get_workspace_status(ctx.project_root)?;
        let mut allowed_ops = workspace::get_allowed_ops(&workspace_status);

        let agent_id = current_agent_id();
        if agent_id != "unknown"
            && let Ok(mut tasks) = todo::list_tasks(
                &ctx.store.root,
                Some("open".to_string()),
                None,
                None,
                None,
                None,
            )
        {
            tasks.retain(|t| t.assigned_to == agent_id);
            if tasks.is_empty() {
                allowed_ops.insert(
                    0,
                    AllowedOp {
                        op: "todo.add".to_string(),
                        reason: "MANDATORY: Create a task for your work".to_string(),
                        required_params: vec!["title".to_string()],
                    },
                );
            } else if tasks.iter().any(|t| t.assigned_to.is_empty()) {
                allowed_ops.insert(
                    0,
                    AllowedOp {
                        op: "todo.claim".to_string(),
                        reason: "MANDATORY: Claim your assigned task".to_string(),
                        required_params: vec!["id".to_string()],
                    },
                );
            }
        }

        let context_capsule = if workspace_status.can_work {
            Some(ContextCapsule {
                fragments: vec![],
                spec: Some("Agent initialized successfully".to_string()),
                architecture: None,
                security: None,
                standards: Some({
                    let resolved = standards::resolve_standards(ctx.project_root)?;
                    let mut map = std::collections::HashMap::new();
                    map.insert(
                        "project_name".to_string(),
                        serde_json::json!(resolved.project_name),
                    );
                    map
                }),
            })
        } else {
            None
        };

        let _blocked_by = if !workspace_status.can_work {
            workspace_status.blockers.clone()
        } else {
            vec![]
        };

        let result = AgentInitResult {
            environment_context: EnvironmentContext {
                repo_root: ctx.project_root.to_string_lossy().to_string(),
                workspace_path: ctx.project_root.to_string_lossy().to_string(),
                tool_summary: ToolSummary {
                    docker_available: workspace_status.container.docker_available,
                    in_container: workspace_status.container.in_container,
                },
                done_means: "decapod validate passes".to_string(),
            },
        };

        let response = success_response(
            ctx.request.id.clone(),
            ctx.request.op.clone(),
            ctx.request.params.clone(),
            Some(serde_json::to_value(result).unwrap()),
            vec![],
            context_capsule,
            allowed_ops,
            ctx.mandates.clone(),
        );

        mark_constitution_initialized(ctx.project_root)?;
        Ok(response)
    }

    pub(crate) fn handle_workspace_status(
        ctx: &RpcCtx,
    ) -> Result<RpcResponse, error::DecapodError> {
        let _params: WorkspaceStatusParams = serde_json::from_value(ctx.request.params.clone())
            .map_err(|e| error::DecapodError::ValidationError(format!("Invalid params: {}", e)))?;

        let status = workspace::get_workspace_status(ctx.project_root)?;
        let blocked_by = status.blockers.clone();
        let allowed_ops = workspace::get_allowed_ops(&status);

        let result = WorkspaceStatusResult {
            git_branch: status.git.current_branch,
            git_is_protected: status.git.is_protected,
            in_container: status.container.in_container,
            can_work: status.can_work,
        };

        let mut response = success_response(
            ctx.request.id.clone(),
            ctx.request.op.clone(),
            ctx.request.params.clone(),
            Some(serde_json::to_value(result).unwrap()),
            vec![],
            None,
            allowed_ops,
            ctx.mandates.clone(),
        );
        response.blocked_by = blocked_by;
        Ok(response)
    }

    pub(crate) fn handle_workspace_ensure(
        ctx: &RpcCtx,
    ) -> Result<RpcResponse, error::DecapodError> {
        let params: WorkspaceEnsureParams = serde_json::from_value(ctx.request.params.clone())
            .map_err(|e| error::DecapodError::ValidationError(format!("Invalid params: {}", e)))?;

        let agent_id = std::env::var("DECAPOD_AGENT_ID").unwrap_or_else(|_| "unknown".to_string());
        let config = params.branch.map(|b| workspace::WorkspaceConfig {
            branch: b,
            use_container: false,
            base_image: None,
        });

        let status = workspace::ensure_workspace(ctx.project_root, config, &agent_id)?;
        let allowed_ops = workspace::get_allowed_ops(&status);

        let result = WorkspaceEnsureResult {
            branch: status.git.current_branch.clone(),
            worktree_path: status
                .git
                .worktree_path
                .clone()
                .unwrap_or_default()
                .to_string_lossy()
                .to_string(),
        };

        Ok(success_response(
            ctx.request.id.clone(),
            ctx.request.op.clone(),
            ctx.request.params.clone(),
            Some(serde_json::to_value(result).unwrap()),
            vec![format!(".git/refs/heads/{}", status.git.current_branch)],
            None,
            allowed_ops,
            ctx.mandates.clone(),
        ))
    }

    pub(crate) fn handle_workspace_publish(
        ctx: &RpcCtx,
    ) -> Result<RpcResponse, error::DecapodError> {
        let params: WorkspacePublishParams = serde_json::from_value(ctx.request.params.clone())
            .map_err(|e| error::DecapodError::ValidationError(format!("Invalid params: {}", e)))?;

        let store_root = ctx.project_root.join(".decapod").join("data");
        plan_governance::ensure_execute_ready(plan_governance::ExecuteCheckInput {
            project_root: ctx.project_root,
            store_root: &store_root,
            todo_id: None,
        })?;

        let result =
            workspace::publish_workspace(ctx.project_root, params.title, params.description)?;

        let rpc_result = WorkspacePublishResult {
            branch: result.branch.clone(),
            commit_hash: result.commit_hash,
            remote_url: result.remote_url,
            pr_url: result.pr_url,
        };

        Ok(success_response(
            ctx.request.id.clone(),
            ctx.request.op.clone(),
            ctx.request.params.clone(),
            Some(serde_json::to_value(rpc_result).unwrap()),
            vec![format!(".git/refs/heads/{}", result.branch)],
            None,
            vec![AllowedOp {
                op: "validate".to_string(),
                reason: "Publish complete - run validation".to_string(),
                required_params: vec![],
            }],
            ctx.mandates.clone(),
        ))
    }

    pub(crate) fn handle_context_resolve(ctx: &RpcCtx) -> Result<RpcResponse, error::DecapodError> {
        let params: ContextResolveParams = serde_json::from_value(ctx.request.params.clone())
            .map_err(|e| error::DecapodError::ValidationError(format!("Invalid params: {}", e)))?;

        let limit = params.limit.unwrap_or(5);

        let mut fragments = Vec::new();
        let bindings = docs::get_bindings(ctx.project_root);

        if let Some(o) = &params.op
            && let Some(doc_ref) = bindings.ops.get(o)
        {
            let parts: Vec<&str> = doc_ref.split('#').collect();
            let path = parts[0];
            let anchor = parts.get(1).copied();
            if let Some(f) = docs::get_fragment(ctx.project_root, path, anchor) {
                fragments.push(f);
            }
        }

        if let Some(paths) = &params.touched_paths {
            for p in paths {
                for (prefix, doc_ref) in &bindings.paths {
                    if p.contains(prefix) {
                        let parts: Vec<&str> = doc_ref.split('#').collect();
                        let path = parts[0];
                        let anchor = parts.get(1).copied();
                        if let Some(f) = docs::get_fragment(ctx.project_root, path, anchor) {
                            fragments.push(f);
                        }
                    }
                }
            }
        }

        if let Some(tags) = &params.intent_tags {
            for t in tags {
                if let Some(doc_ref) = bindings.tags.get(t) {
                    let parts: Vec<&str> = doc_ref.split('#').collect();
                    let path = parts[0];
                    let anchor = parts.get(1).copied();
                    if let Some(f) = docs::get_fragment(ctx.project_root, path, anchor) {
                        fragments.push(f);
                    }
                }
            }
        }

        fragments.sort_by(|a, b| a.r#ref.cmp(&b.r#ref));
        fragments.dedup_by(|a, b| a.r#ref == b.r#ref);

        let touched_vec = params.touched_paths.clone().unwrap_or_default();
        let tags_vec = params.intent_tags.clone().unwrap_or_default();

        let scoped_fragments = docs::resolve_scoped_fragments(
            ctx.project_root,
            params.query.as_deref(),
            params.op.as_deref(),
            &touched_vec,
            &tags_vec,
            limit,
        );
        fragments.extend(scoped_fragments.clone());
        fragments.sort_by(|a, b| a.r#ref.cmp(&b.r#ref));
        fragments.dedup_by(|a, b| a.r#ref == b.r#ref);
        fragments.truncate(limit.max(1));

        let local_specs = core::project_specs::local_project_specs_context(ctx.project_root);

        let result = ContextResolveResult {
            fragments: fragments.clone(),
            scoped_fragments,
            local_project_specs: LocalProjectSpecs {
                canonical_paths: local_specs.canonical_paths.clone(),
                constitution_refs: local_specs.constitution_refs.clone(),
                intent: local_specs.intent.clone(),
                architecture: local_specs.architecture.clone(),
                interfaces: local_specs.interfaces.clone(),
                validation: local_specs.validation.clone(),
                update_guidance: Some(local_specs.update_guidance.clone()),
            },
        };
        mark_constitution_context_resolved(ctx.project_root)?;

        Ok(success_response(
            ctx.request.id.clone(),
            ctx.request.op.clone(),
            ctx.request.params.clone(),
            Some(serde_json::to_value(result).unwrap()),
            vec![],
            Some(ContextCapsule {
                fragments,
                spec: local_specs.intent.clone(),
                architecture: local_specs.architecture.clone(),
                security: None,
                standards: Some({
                    let mut m = std::collections::HashMap::new();
                    m.insert(
                        "local_project_specs".to_string(),
                        serde_json::json!({
                            "canonical_paths": local_specs.canonical_paths,
                            "constitution_refs": local_specs.constitution_refs,
                            "interfaces": local_specs.interfaces,
                            "validation": local_specs.validation,
                            "update_guidance": local_specs.update_guidance
                        }),
                    );
                    m
                }),
            }),
            vec![
                AllowedOp {
                    op: "store.upsert".to_string(),
                    reason: "Persist significant decisions for audit trail before proceeding"
                        .to_string(),
                    required_params: vec!["kind".to_string(), "data".to_string()],
                },
                AllowedOp {
                    op: "validate.run".to_string(),
                    reason: "Validate your changes against constitution before claiming done"
                        .to_string(),
                    required_params: vec![],
                },
                AllowedOp {
                    op: "store.query".to_string(),
                    reason: "Retrieve prior decisions and knowledge relevant to current task"
                        .to_string(),
                    required_params: vec!["kind".to_string()],
                },
            ],
            ctx.mandates.clone(),
        ))
    }

    pub(crate) fn handle_context_capsule_query(
        ctx: &RpcCtx,
    ) -> Result<RpcResponse, error::DecapodError> {
        let params: ContextCapsuleQueryParams = serde_json::from_value(ctx.request.params.clone())
            .map_err(|e| error::DecapodError::ValidationError(format!("Invalid params: {}", e)))?;

        let limit = params.limit.unwrap_or(6);
        let write = params.write.unwrap_or(false);

        let resolved_policy = core::capsule_policy::resolve_capsule_policy(
            ctx.project_root,
            &params.scope,
            params.risk_tier.as_deref(),
            limit,
            write,
        )?;
        let capsule = core::context_capsule::query_embedded_capsule_governed(
            ctx.project_root,
            &params.topic,
            &params.scope,
            params.task_id.as_deref(),
            params.workunit_id.as_deref(),
            resolved_policy.effective_limit,
            resolved_policy.binding,
        )?;

        let mut touched = Vec::new();
        if write {
            let path = core::context_capsule::write_context_capsule(ctx.project_root, &capsule)?;
            touched.push(path.to_string_lossy().to_string());
            if let Some(workunit_path) = maybe_bind_capsule_to_workunit_state_ref(
                ctx.project_root,
                params.task_id.as_deref().or(params.workunit_id.as_deref()),
                &path,
            )? {
                touched.push(workunit_path.to_string_lossy().to_string());
            }
        }

        Ok(success_response(
            ctx.request.id.clone(),
            ctx.request.op.clone(),
            ctx.request.params.clone(),
            Some(serde_json::to_value(&capsule).unwrap()),
            touched,
            Some(ContextCapsule {
                fragments: vec![],
                spec: Some("Deterministic context capsule query completed".to_string()),
                architecture: None,
                security: None,
                standards: None,
            }),
            vec![],
            ctx.mandates.clone(),
        ))
    }

    pub(crate) fn handle_context_bindings(
        ctx: &RpcCtx,
    ) -> Result<RpcResponse, error::DecapodError> {
        let _params: ContextBindingsParams = serde_json::from_value(ctx.request.params.clone())
            .map_err(|e| error::DecapodError::ValidationError(format!("Invalid params: {}", e)))?;

        let bindings = docs::get_bindings(ctx.project_root);
        Ok(success_response(
            ctx.request.id.clone(),
            ctx.request.op.clone(),
            ctx.request.params.clone(),
            Some(serde_json::to_value(bindings).unwrap()),
            vec![],
            None,
            vec![],
            ctx.mandates.clone(),
        ))
    }

    pub(crate) fn handle_constitution_get(
        ctx: &RpcCtx,
    ) -> Result<RpcResponse, error::DecapodError> {
        let params: ConstitutionGetParams = serde_json::from_value(ctx.request.params.clone())
            .map_err(|e| error::DecapodError::ValidationError(format!("Invalid params: {}", e)))?;

        let Some(raw_content) = core::assets::get_embedded_doc(&params.section) else {
            return Ok(error_response(
                ctx.request.id.clone(),
                ctx.request.op.clone(),
                ctx.request.params.clone(),
                "unknown_section".to_string(),
                format!("Unknown constitution section: {}", params.section),
                None,
                ctx.mandates.clone(),
            ));
        };

        let mut content: serde_json::Value = serde_json::from_str(&raw_content)
            .unwrap_or_else(|_| serde_json::json!({ "text": raw_content }));

        if let Some(subsection) = params.subsection.as_deref() {
            let selected = content
                .get("sections")
                .and_then(|sections| sections.get(subsection))
                .cloned();
            match selected {
                Some(section) => {
                    content = serde_json::json!({
                        "subsection": subsection,
                        "value": section
                    });
                }
                None => {
                    return Ok(error_response(
                        ctx.request.id.clone(),
                        ctx.request.op.clone(),
                        ctx.request.params.clone(),
                        "unknown_subsection".to_string(),
                        format!(
                            "Unknown subsection '{}' in constitution section '{}'",
                            subsection, params.section
                        ),
                        None,
                        ctx.mandates.clone(),
                    ));
                }
            }
        }

        let (category, title, dependencies) = core::assets::get_doc_metadata(&params.section)
            .unwrap_or(("unknown".to_string(), params.section.clone(), Vec::new()));

        mark_core_constitution_ingested(ctx.project_root, "constitution.get")?;

        Ok(success_response(
            ctx.request.id.clone(),
            ctx.request.op.clone(),
            ctx.request.params.clone(),
            Some(
                serde_json::to_value(ConstitutionGetResult {
                    section: params.section,
                    title,
                    category,
                    dependencies,
                    content,
                })
                .unwrap(),
            ),
            vec![],
            None,
            vec![],
            ctx.mandates.clone(),
        ))
    }

    pub(crate) fn handle_schema_get(ctx: &RpcCtx) -> Result<RpcResponse, error::DecapodError> {
        let params: SchemaGetParams = serde_json::from_value(ctx.request.params.clone())
            .map_err(|e| error::DecapodError::ValidationError(format!("Invalid params: {}", e)))?;

        match params.entity.as_deref() {
            Some("todo") => Ok(success_response(
                ctx.request.id.clone(),
                ctx.request.op.clone(),
                ctx.request.params.clone(),
                Some(serde_json::to_value(SchemaGetResult {
                    schema_version: "v1".to_string(),
                    json_schema: serde_json::json!({
                        "type": "object",
                        "properties": {
                            "title": { "type": "string" },
                            "description": { "type": "string" },
                            "priority": { "type": "string", "enum": ["low", "medium", "high", "critical"] },
                            "tags": { "type": "string" }
                        },
                        "required": ["title"]
                    }),
                }).unwrap()),
                vec![],
                None,
                vec![],
                ctx.mandates.clone(),
            )),
            Some("knowledge") => Ok(success_response(
                ctx.request.id.clone(),
                ctx.request.op.clone(),
                ctx.request.params.clone(),
                Some(serde_json::to_value(SchemaGetResult {
                    schema_version: "v1".to_string(),
                    json_schema: serde_json::json!({
                        "type": "object",
                        "properties": {
                            "id": { "type": "string" },
                            "title": { "type": "string" },
                            "text": { "type": "string" },
                            "provenance": { "type": "string" }
                        },
                        "required": ["id", "title", "text", "provenance"]
                    }),
                }).unwrap()),
                vec![],
                None,
                vec![],
                ctx.mandates.clone(),
            )),
            Some("decision") => Ok(success_response(
                ctx.request.id.clone(),
                ctx.request.op.clone(),
                ctx.request.params.clone(),
                Some(serde_json::to_value(SchemaGetResult {
                    schema_version: "v1".to_string(),
                    json_schema: serde_json::json!({
                        "type": "object",
                        "properties": {
                            "title": { "type": "string" },
                            "rationale": { "type": "string" },
                            "options": { "type": "array", "items": { "type": "string" } },
                            "chosen": { "type": "string" }
                        },
                        "required": ["title", "rationale", "chosen"]
                    }),
                }).unwrap()),
                vec![],
                None,
                vec![],
                ctx.mandates.clone(),
            )),
            _ => Ok(error_response(
                ctx.request.id.clone(),
                ctx.request.op.clone(),
                ctx.request.params.clone(),
                "invalid_entity".to_string(),
                format!("Invalid or missing entity: {:?}", params.entity),
                None,
                ctx.mandates.clone(),
            )),
        }
    }

    pub(crate) fn handle_store_upsert(ctx: &RpcCtx) -> Result<RpcResponse, error::DecapodError> {
        let params: StoreUpsertParams = serde_json::from_value(ctx.request.params.clone())
            .map_err(|e| error::DecapodError::ValidationError(format!("Invalid params: {}", e)))?;

        let payload = params.payload.as_ref();

        match params.entity.as_deref() {
            Some("todo") => {
                let title = payload
                    .and_then(|p| p.get("title"))
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();
                let description = payload
                    .and_then(|p| p.get("description"))
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();
                let priority = payload
                    .and_then(|p| p.get("priority"))
                    .and_then(|v| v.as_str())
                    .unwrap_or("medium")
                    .to_string();
                let tags = payload
                    .and_then(|p| p.get("tags"))
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();

                let args = todo::TodoCommand::Add {
                    title,
                    description,
                    priority,
                    tags,
                    owner: "".to_string(),
                    due: None,
                    r#ref: "".to_string(),
                    scope: "".to_string(),
                    dir: None,
                    depends_on: "".to_string(),
                    blocks: "".to_string(),
                    parent: None,
                    one_shot: 0,
                };
                let res = todo::add_task(&ctx.store.root, &args)?;
                let result = StoreUpsertResult {
                    id: res
                        .get("id")
                        .and_then(|v| v.as_str())
                        .unwrap_or_default()
                        .to_string(),
                    status: None,
                    stored: Some(true),
                    action: None,
                };
                Ok(success_response(
                    ctx.request.id.clone(),
                    ctx.request.op.clone(),
                    ctx.request.params.clone(),
                    Some(serde_json::to_value(result).unwrap()),
                    vec![],
                    None,
                    vec![],
                    ctx.mandates.clone(),
                ))
            }
            Some("knowledge") => {
                let id = payload
                    .and_then(|p| p.get("id"))
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();
                let title = payload
                    .and_then(|p| p.get("title"))
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();
                let text = payload
                    .and_then(|p| p.get("text"))
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();
                let provenance = payload
                    .and_then(|p| p.get("provenance"))
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();

                db::initialize_knowledge_db(&ctx.store.root)?;
                let res = knowledge::add_knowledge(
                    ctx.store,
                    knowledge::AddKnowledgeParams {
                        id: &id,
                        title: &title,
                        content: &text,
                        provenance: &provenance,
                        claim_id: None,
                        merge_key: None,
                        conflict_policy: knowledge::KnowledgeConflictPolicy::Merge,
                        status: "active",
                        ttl_policy: "persistent",
                        expires_ts: None,
                    },
                )?;
                let result = StoreUpsertResult {
                    id: res.id,
                    status: None,
                    stored: Some(true),
                    action: Some(res.action),
                };
                Ok(success_response(
                    ctx.request.id.clone(),
                    ctx.request.op.clone(),
                    ctx.request.params.clone(),
                    Some(serde_json::to_value(result).unwrap()),
                    vec![],
                    None,
                    vec![],
                    ctx.mandates.clone(),
                ))
            }
            Some("decision") => {
                let title = payload
                    .and_then(|p| p.get("title"))
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();
                let rationale = payload
                    .and_then(|p| p.get("rationale"))
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();
                let chosen = payload
                    .and_then(|p| p.get("chosen"))
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();

                let content = format!("Decision: {}\nRationale: {}", chosen, rationale);
                let node = federation::add_node(
                    ctx.store,
                    &title,
                    "decision",
                    "notable",
                    "agent_inferred",
                    &content,
                    "rpc:store.upsert",
                    "",
                    "repo",
                    None,
                    "agent",
                )?;
                let result = StoreUpsertResult {
                    id: node.id,
                    status: None,
                    stored: Some(true),
                    action: None,
                };
                Ok(success_response(
                    ctx.request.id.clone(),
                    ctx.request.op.clone(),
                    ctx.request.params.clone(),
                    Some(serde_json::to_value(result).unwrap()),
                    vec![],
                    None,
                    vec![],
                    ctx.mandates.clone(),
                ))
            }
            _ => Ok(error_response(
                ctx.request.id.clone(),
                ctx.request.op.clone(),
                ctx.request.params.clone(),
                "invalid_entity".to_string(),
                format!("Invalid or missing entity: {:?}", params.entity),
                None,
                ctx.mandates.clone(),
            )),
        }
    }

    pub(crate) fn handle_store_query(ctx: &RpcCtx) -> Result<RpcResponse, error::DecapodError> {
        let params: StoreQueryParams = serde_json::from_value(ctx.request.params.clone())
            .map_err(|e| error::DecapodError::ValidationError(format!("Invalid params: {}", e)))?;

        match params.entity.as_deref() {
            Some("todo") => {
                let status = params
                    .query
                    .as_ref()
                    .and_then(|q| q.get("status"))
                    .and_then(|v| v.as_str())
                    .map(|s| s.to_string());
                let tasks = todo::list_tasks(&ctx.store.root, status, None, None, None, None)?;
                let result = StoreQueryResult {
                    items: tasks
                        .into_iter()
                        .map(|t| serde_json::to_value(t).unwrap())
                        .collect(),
                    next_page: None,
                };
                Ok(success_response(
                    ctx.request.id.clone(),
                    ctx.request.op.clone(),
                    ctx.request.params.clone(),
                    Some(serde_json::to_value(result).unwrap()),
                    vec![],
                    None,
                    vec![],
                    ctx.mandates.clone(),
                ))
            }
            Some("knowledge") => {
                let text = params
                    .query
                    .as_ref()
                    .and_then(|q| q.get("text"))
                    .and_then(|v| v.as_str())
                    .unwrap_or("");
                db::initialize_knowledge_db(&ctx.store.root)?;
                let entries = knowledge::search_knowledge(
                    ctx.store,
                    text,
                    knowledge::SearchOptions {
                        as_of: None,
                        window_days: None,
                        rank: "relevance",
                    },
                )?;
                let result = StoreQueryResult {
                    items: entries
                        .into_iter()
                        .map(|e| serde_json::to_value(e).unwrap())
                        .collect(),
                    next_page: None,
                };
                Ok(success_response(
                    ctx.request.id.clone(),
                    ctx.request.op.clone(),
                    ctx.request.params.clone(),
                    Some(serde_json::to_value(result).unwrap()),
                    vec![],
                    None,
                    vec![],
                    ctx.mandates.clone(),
                ))
            }
            Some("decision") => {
                let nodes = plugins::federation_ext::list_nodes(
                    &ctx.store.root,
                    Some("decision".to_string()),
                    None,
                    None,
                    None,
                )?;
                let result = StoreQueryResult {
                    items: nodes
                        .into_iter()
                        .map(|n| serde_json::to_value(n).unwrap())
                        .collect(),
                    next_page: None,
                };
                Ok(success_response(
                    ctx.request.id.clone(),
                    ctx.request.op.clone(),
                    ctx.request.params.clone(),
                    Some(serde_json::to_value(result).unwrap()),
                    vec![],
                    None,
                    vec![],
                    ctx.mandates.clone(),
                ))
            }
            _ => Ok(error_response(
                ctx.request.id.clone(),
                ctx.request.op.clone(),
                ctx.request.params.clone(),
                "invalid_entity".to_string(),
                format!("Invalid or missing entity: {:?}", params.entity),
                None,
                ctx.mandates.clone(),
            )),
        }
    }

    pub(crate) fn handle_validate_run(ctx: &RpcCtx) -> Result<RpcResponse, error::DecapodError> {
        let _params: ValidateRunParams = serde_json::from_value(ctx.request.params.clone())
            .map_err(|e| error::DecapodError::ValidationError(format!("Invalid params: {}", e)))?;

        let project_store = Store {
            kind: StoreKind::Repo,
            root: ctx.project_root.join(".decapod").join("data"),
        };
        let res = run_validation_bounded(&project_store, ctx.project_root, false);
        match res {
            Ok(report) if report.fail_count == 0 => {
                let result = ValidateRunResult {
                    success: true,
                    report: "All validation gates passed".to_string(),
                };
                Ok(success_response(
                    ctx.request.id.clone(),
                    ctx.request.op.clone(),
                    ctx.request.params.clone(),
                    Some(serde_json::to_value(result).unwrap()),
                    vec![],
                    None,
                    vec![],
                    ctx.mandates.clone(),
                ))
            }
            Ok(report) => Ok(error_response(
                ctx.request.id.clone(),
                ctx.request.op.clone(),
                ctx.request.params.clone(),
                "validation_failed".to_string(),
                format!("{} validation gate(s) failed", report.fail_count),
                None,
                ctx.mandates.clone(),
            )),
            Err(e) => Ok(error_response(
                ctx.request.id.clone(),
                ctx.request.op.clone(),
                ctx.request.params.clone(),
                "validation_failed".to_string(),
                e.to_string(),
                None,
                ctx.mandates.clone(),
            )),
        }
    }

    pub(crate) fn handle_scaffold_next_question(
        ctx: &RpcCtx,
    ) -> Result<RpcResponse, error::DecapodError> {
        let params: ScaffoldNextQuestionParams = serde_json::from_value(ctx.request.params.clone())
            .map_err(|e| error::DecapodError::ValidationError(format!("Invalid params: {}", e)))?;

        let project_name = params
            .project_name
            .unwrap_or_else(|| "Untitled".to_string());

        let interview_state = interview::init_interview(project_name);
        let question = interview::next_question(&interview_state);

        let mut response = success_response(
            ctx.request.id.clone(),
            ctx.request.op.clone(),
            ctx.request.params.clone(),
            None,
            vec![],
            None,
            vec![AllowedOp {
                op: "scaffold.apply_answer".to_string(),
                reason: "Provide answer to continue interview".to_string(),
                required_params: vec!["question_id".to_string(), "value".to_string()],
            }],
            ctx.mandates.clone(),
        );

        let is_complete = question.is_none();
        let result = ScaffoldNextQuestionResult {
            interview_id: interview_state.id,
            question,
            complete: if is_complete { Some(true) } else { None },
        };
        response.result = Some(serde_json::to_value(result).unwrap());

        Ok(response)
    }

    pub(crate) fn handle_scaffold_apply_answer(
        ctx: &RpcCtx,
    ) -> Result<RpcResponse, error::DecapodError> {
        let params: ScaffoldApplyAnswerParams = serde_json::from_value(ctx.request.params.clone())
            .map_err(|e| error::DecapodError::ValidationError(format!("Invalid params: {}", e)))?;

        let mut interview_state = interview::init_interview("project".to_string());
        interview::apply_answer(&mut interview_state, &params.question_id, params.value)?;

        let next_q = interview::next_question(&interview_state);

        let mut response = success_response(
            ctx.request.id.clone(),
            ctx.request.op.clone(),
            ctx.request.params.clone(),
            None,
            vec![],
            None,
            vec![AllowedOp {
                op: if next_q.is_some() {
                    "scaffold.next_question".to_string()
                } else {
                    "scaffold.generate_artifacts".to_string()
                },
                reason: if next_q.is_some() {
                    "Continue interview".to_string()
                } else {
                    "Interview complete - generate artifacts".to_string()
                },
                required_params: vec![],
            }],
            ctx.mandates.clone(),
        );

        let result = ScaffoldApplyAnswerResult {
            answers_count: interview_state.answers.len(),
            is_complete: interview_state.is_complete,
        };
        response.result = Some(serde_json::to_value(result).unwrap());

        Ok(response)
    }

    pub(crate) fn handle_scaffold_generate_artifacts(
        ctx: &RpcCtx,
    ) -> Result<RpcResponse, error::DecapodError> {
        let _params: ScaffoldGenerateArtifactsParams =
            serde_json::from_value(ctx.request.params.clone()).map_err(|e| {
                error::DecapodError::ValidationError(format!("Invalid params: {}", e))
            })?;

        let interview_state = interview::init_interview("project".to_string());
        let output_dir = ctx.project_root.to_path_buf();

        let artifacts = interview::generate_artifacts(&interview_state, &output_dir)?;
        let touched_paths: Vec<String> = artifacts
            .iter()
            .map(|a| a.path.to_string_lossy().to_string())
            .collect();

        Ok(success_response(
            ctx.request.id.clone(),
            ctx.request.op.clone(),
            ctx.request.params.clone(),
            None,
            touched_paths,
            None,
            vec![AllowedOp {
                op: "validate".to_string(),
                reason: "Artifacts generated - validate before claiming done".to_string(),
                required_params: vec![],
            }],
            ctx.mandates.clone(),
        ))
    }

    pub(crate) fn handle_standards_resolve(
        ctx: &RpcCtx,
    ) -> Result<RpcResponse, error::DecapodError> {
        let _params: StandardsResolveParams = serde_json::from_value(ctx.request.params.clone())
            .map_err(|e| error::DecapodError::ValidationError(format!("Invalid params: {}", e)))?;

        let resolved = standards::resolve_standards(ctx.project_root)?;

        let mut standards_map = std::collections::HashMap::new();
        standards_map.insert(
            "project_name".to_string(),
            serde_json::json!(resolved.project_name),
        );
        for (k, v) in &resolved.standards {
            standards_map.insert(k.clone(), v.clone());
        }

        let context_capsule = ContextCapsule {
            fragments: vec![],
            spec: None,
            architecture: None,
            security: None,
            standards: Some(standards_map),
        };

        Ok(success_response(
            ctx.request.id.clone(),
            ctx.request.op.clone(),
            ctx.request.params.clone(),
            None,
            vec![],
            Some(context_capsule),
            vec![],
            ctx.mandates.clone(),
        ))
    }

    pub(crate) fn handle_mentor_obligations(
        ctx: &RpcCtx,
    ) -> Result<RpcResponse, error::DecapodError> {
        use crate::core::mentor::{MentorEngine, ObligationsContext};

        let params: MentorObligationsParams = serde_json::from_value(ctx.request.params.clone())
            .map_err(|e| error::DecapodError::ValidationError(format!("Invalid params: {}", e)))?;

        let engine = MentorEngine::new(ctx.project_root);
        let obligations_ctx = ObligationsContext {
            op: params.op.unwrap_or_else(|| "unknown".to_string()),
            params: params.params.unwrap_or_else(|| serde_json::json!({})),
            touched_paths: params.touched_paths.unwrap_or_default(),
            diff_summary: params.diff_summary,
            project_profile_id: params.project_profile_id,
            session_id: params.session_id,
            high_risk: params.high_risk.unwrap_or(false),
        };

        let obligations = engine.compute_obligations(&obligations_ctx)?;

        let context_capsule = ContextCapsule {
            fragments: vec![],
            spec: None,
            architecture: None,
            security: None,
            standards: None,
        };

        let mut response = success_response(
            ctx.request.id.clone(),
            ctx.request.op.clone(),
            ctx.request.params.clone(),
            Some(
                serde_json::to_value(MentorObligationsResult {
                    obligations: obligations.clone(),
                })
                .unwrap(),
            ),
            vec![],
            Some(context_capsule),
            vec![AllowedOp {
                op: "mentor.obligations".to_string(),
                reason: "Obligations computed - review must list before proceeding".to_string(),
                required_params: vec![],
            }],
            ctx.mandates.clone(),
        );

        if !obligations.contradictions.is_empty() {
            response.blocked_by = mentor::contradictions_to_blockers(&obligations.contradictions);
        }

        Ok(response)
    }

    pub(crate) fn handle_assurance_evaluate(
        ctx: &RpcCtx,
    ) -> Result<RpcResponse, error::DecapodError> {
        let params: AssuranceEvaluateParams = serde_json::from_value(ctx.request.params.clone())
            .map_err(|e| error::DecapodError::ValidationError(format!("Invalid params: {}", e)))?;

        let input = AssuranceEvaluateInput {
            op: params.op.unwrap_or_else(|| "unknown".to_string()),
            params: params.params.unwrap_or_else(|| serde_json::json!({})),
            touched_paths: params.touched_paths.unwrap_or_default(),
            diff_summary: params.diff_summary,
            session_id: params.session_id,
            phase: params.phase,
            time_budget_s: params.time_budget_s,
        };

        let engine = AssuranceEngine::new(ctx.project_root);
        let evaluated = engine.evaluate(&input)?;
        let mut response = success_response(
            ctx.request.id.clone(),
            ctx.request.op.clone(),
            ctx.request.params.clone(),
            Some(
                serde_json::to_value(AssuranceEvaluateResult {
                    assurance_evaluated: true,
                    interlock_code: evaluated.interlock.as_ref().map(|i| i.code.clone()),
                })
                .unwrap(),
            ),
            input.touched_paths.clone(),
            None,
            if let Some(interlock) = &evaluated.interlock {
                interlock
                    .unblock_ops
                    .iter()
                    .map(|op| AllowedOp {
                        op: op.clone(),
                        reason: format!("Unblock path for {}", interlock.code),
                        required_params: vec![],
                    })
                    .collect()
            } else {
                vec![AllowedOp {
                    op: "assurance.evaluate".to_string(),
                    reason: "Re-evaluate after meaningful context changes".to_string(),
                    required_params: vec![],
                }]
            },
            ctx.mandates.clone(),
        );
        response.interlock = evaluated.interlock.clone();
        response.advisory = Some(evaluated.advisory.clone());
        response.attestation = Some(evaluated.attestation.clone());

        if let Some(interlock) = evaluated.interlock {
            response.blocked_by = vec![Blocker {
                kind: match interlock.code.as_str() {
                    "workspace_required" => BlockerKind::WorkspaceRequired,
                    "verification_required" => BlockerKind::MissingProof,
                    "store_boundary_violation" => BlockerKind::Unauthorized,
                    "decision_required" => BlockerKind::MissingAnswer,
                    _ => BlockerKind::ValidationFailed,
                },
                message: interlock.code,
                resolve_hint: interlock.message,
            }];
        }

        Ok(response)
    }
}

/// Run RPC command
fn run_rpc_command(cli: RpcCli, project_root: &Path) -> Result<(), error::DecapodError> {
    use crate::core::rpc::*;

    let request: RpcRequest = if cli.stdin {
        let mut buffer = String::new();
        std::io::stdin()
            .read_to_string(&mut buffer)
            .map_err(error::DecapodError::IoError)?;
        serde_json::from_str(&buffer)
            .map_err(|e| error::DecapodError::ValidationError(format!("Invalid JSON: {}", e)))?
    } else {
        let op = cli.op.ok_or_else(|| {
            error::DecapodError::ValidationError("Operation required".to_string())
        })?;
        let params = cli
            .params
            .as_ref()
            .and_then(|p| serde_json::from_str(p).ok())
            .unwrap_or(serde_json::json!({}));

        RpcRequest {
            op,
            params,
            id: default_request_id(),
            session: None,
        }
    };

    enforce_worktree_requirement_for_rpc(&request.op, project_root)?;

    if !rpc_op_bypasses_session(&request.op) {
        ensure_session_valid()?;
    }
    enforce_constitutional_awareness_for_rpc(&request.op, project_root)?;

    let project_store = Store {
        kind: StoreKind::Repo,
        root: project_root.join(".decapod").join("data"),
    };

    let mandates = docs::resolve_mandates(project_root, &request.op);
    let mandate_blockers = if rpc_op_skips_mandate_enforcement(&request.op) {
        Vec::new()
    } else {
        validate::evaluate_mandates(project_root, &project_store, &mandates)
    };

    // If any mandate is blocked, we fail the operation
    let blocked_mandate = mandates.iter().find(|m| {
        mandate_blockers
            .iter()
            .any(|b| b.message.contains(&m.fragment.title))
    });

    if let Some(mandate) = blocked_mandate {
        let blocker = mandate_blockers
            .iter()
            .find(|b| b.message.contains(&mandate.fragment.title))
            .unwrap();
        let response = error_response(
            request.id.clone(),
            request.op.clone(),
            request.params.clone(),
            "mandate_violation".to_string(),
            blocker.message.clone(),
            Some(blocker.clone()),
            mandates,
        );
        println!("{}", serde_json::to_string_pretty(&response).unwrap());
        return Ok(());
    }

    let rpc_ctx = RpcCtx {
        project_root,
        store: &project_store,
        request: &request,
        mandates: mandates.clone(),
    };

    let response = match request.op.as_str() {
        "agent.init" => rpc_handlers::handle_agent_init(&rpc_ctx)?,
        "workspace.status" => rpc_handlers::handle_workspace_status(&rpc_ctx)?,
        "workspace.ensure" => rpc_handlers::handle_workspace_ensure(&rpc_ctx)?,
        "workspace.publish" => rpc_handlers::handle_workspace_publish(&rpc_ctx)?,
        "context.resolve" | "context.scope" => rpc_handlers::handle_context_resolve(&rpc_ctx)?,
        "context.capsule.query" => rpc_handlers::handle_context_capsule_query(&rpc_ctx)?,
        "context.bindings" => rpc_handlers::handle_context_bindings(&rpc_ctx)?,
        "constitution.get" => rpc_handlers::handle_constitution_get(&rpc_ctx)?,
        "schema.get" => rpc_handlers::handle_schema_get(&rpc_ctx)?,
        "store.upsert" => rpc_handlers::handle_store_upsert(&rpc_ctx)?,
        "store.query" => rpc_handlers::handle_store_query(&rpc_ctx)?,
        "validate.run" => rpc_handlers::handle_validate_run(&rpc_ctx)?,
        "scaffold.next_question" => rpc_handlers::handle_scaffold_next_question(&rpc_ctx)?,
        "scaffold.apply_answer" => rpc_handlers::handle_scaffold_apply_answer(&rpc_ctx)?,
        "scaffold.generate_artifacts" => {
            rpc_handlers::handle_scaffold_generate_artifacts(&rpc_ctx)?
        }
        "standards.resolve" => rpc_handlers::handle_standards_resolve(&rpc_ctx)?,
        "mentor.obligations" => rpc_handlers::handle_mentor_obligations(&rpc_ctx)?,
        "assurance.evaluate" => rpc_handlers::handle_assurance_evaluate(&rpc_ctx)?,
        _ => error_response(
            request.id.clone(),
            request.op.clone(),
            request.params.clone(),
            "unknown_op".to_string(),
            format!("Unknown operation: {}", request.op),
            None,
            mandates.clone(),
        ),
    };

    // Trace the RPC call
    let trace_event = trace::TraceEvent {
        trace_id: request.id.clone(),
        ts: crate::core::time::now_epoch_z(),
        actor: current_agent_id(),
        op: request.op.clone(),
        request: serde_json::to_value(&request).unwrap_or(serde_json::Value::Null),
        response: serde_json::to_value(&response).unwrap_or(serde_json::Value::Null),
    };
    let _ = trace::append_trace(project_root, trace_event);

    println!("{}", serde_json::to_string_pretty(&response).unwrap());
    Ok(())
}

fn maybe_bind_capsule_to_workunit_state_ref(
    project_root: &Path,
    workunit_task_id: Option<&str>,
    capsule_path: &Path,
) -> Result<Option<PathBuf>, error::DecapodError> {
    let Some(task_id) = workunit_task_id else {
        return Ok(None);
    };
    match core::workunit::load_workunit(project_root, task_id) {
        Ok(_) => {
            let state_ref = capsule_path
                .strip_prefix(project_root)
                .unwrap_or(capsule_path)
                .to_string_lossy()
                .replace('\\', "/");
            core::workunit::add_state_ref(project_root, task_id, &state_ref)?;
            let path = core::workunit::workunit_path(project_root, task_id)?;
            Ok(Some(path))
        }
        Err(error::DecapodError::NotFound(_)) => Ok(None),
        Err(e) => Err(e),
    }
}

/// Run capabilities command
fn run_capabilities_command(cli: CapabilitiesCli) -> Result<(), error::DecapodError> {
    use crate::core::rpc::generate_capabilities;

    let report = generate_capabilities();

    match cli.format.as_str() {
        "json" => {
            println!("{}", serde_json::to_string_pretty(&report).unwrap());
        }
        _ => {
            println!("Decapod {}", report.version);
            println!("==================\n");

            println!("Capabilities:");
            for cap in &report.capabilities {
                println!("  {} [{}] - {}", cap.name, cap.stability, cap.description);
            }

            println!("\nSubsystems:");
            for sub in &report.subsystems {
                println!("  {} [{}]", sub.name, sub.status);
                println!("    Ops: {}", sub.ops.join(", "));
            }

            println!("\nWorkspace:");
            println!(
                "  Enforcement: {}",
                if report.workspace.enforcement_available {
                    "available"
                } else {
                    "unavailable"
                }
            );
            println!(
                "  Docker: {}",
                if report.workspace.docker_available {
                    "available"
                } else {
                    "unavailable"
                }
            );
            println!(
                "  Protected: {}",
                report.workspace.protected_patterns.join(", ")
            );

            println!("\nInterview:");
            println!(
                "  Available: {}",
                if report.interview.available {
                    "yes"
                } else {
                    "no"
                }
            );
            println!(
                "  Artifacts: {}",
                report.interview.artifact_types.join(", ")
            );
            println!("\nInterlocks:");
            println!("  Codes: {}", report.interlock_codes.join(", "));
        }
    }

    Ok(())
}

fn run_trace_command(cli: TraceCli, project_root: &Path) -> Result<(), error::DecapodError> {
    match cli.command {
        TraceCommand::Export { last } => {
            let traces = trace::get_last_traces(project_root, last)?;
            for t in traces {
                println!("{}", t);
            }
        }
    }
    Ok(())
}

fn run_preflight_command(
    cli: PreflightCli,
    project_root: &Path,
) -> Result<(), error::DecapodError> {
    use crate::core::workspace;

    let op = cli.op.unwrap_or_else(|| "unknown".to_string());

    let workspace_status = match workspace::get_workspace_status(project_root) {
        Ok(status) => status,
        Err(_) => {
            return Ok(());
        }
    };

    let mut risk_flags = Vec::new();
    let mut likely_failures = Vec::new();
    let mut required_capsules = Vec::new();
    let mut next_best_actions = Vec::new();

    if workspace_status.git.is_protected {
        risk_flags.push("protected_branch");
        likely_failures.push(serde_json::json!({
            "code": "WORKSPACE_REQUIRED",
            "message": "Cannot operate on protected branch",
            "current_branch": workspace_status.git.current_branch,
        }));
        next_best_actions.push("Run: decapod workspace ensure");
    }

    if !workspace_status.can_work {
        risk_flags.push("workspace_blocked");
        for blocker in &workspace_status.blockers {
            likely_failures.push(serde_json::json!({
                "code": "WORKSPACE_BLOCKED",
                "message": blocker.message,
                "resolve_hint": blocker.resolve_hint,
            }));
        }
    }

    match op.as_str() {
        "todo.add" | "todo.claim" | "todo.done" => {
            required_capsules.push("plugins/TODO");
            required_capsules.push("interfaces/STORE_MODEL");
        }
        "validate" => {
            required_capsules.push("plugins/VERIFY");
            required_capsules.push("interfaces/TESTING");
            if workspace_status.git.is_protected {}
        }
        "workspace.ensure" | "workspace.status" => {
            required_capsules.push("core/DECAPOD");
            required_capsules.push("core/PLUGINS");
        }
        "rpc" | "agent.init" => {
            required_capsules.push("core/INTERFACES");
            required_capsules.push("specs/INTENT");
        }
        _ => {
            required_capsules.push("core/DECAPOD");
        }
    }

    if risk_flags.is_empty() {
        next_best_actions.push("Proceed with operation");
    }

    let response = serde_json::json!({
        "op": op,
        "session_id": cli.session,
        "risk_flags": risk_flags,
        "likely_failures": likely_failures,
        "required_capsules": required_capsules,
        "next_best_actions": next_best_actions,
        "workspace": {
            "git_branch": workspace_status.git.current_branch,
            "git_is_protected": workspace_status.git.is_protected,
            "can_work": workspace_status.can_work,
        }
    });

    if cli.format == "json" {
        println!("{}", serde_json::to_string_pretty(&response).unwrap());
    } else {
        println!("Preflight Check for: {}", op);
        if risk_flags.is_empty() {
            println!("✓ No risks detected");
        } else {
            println!("⚠ Risks: {:?}", risk_flags);
            println!("Likely failures:");
            for failure in &likely_failures {
                println!("  - {}: {}", failure["code"], failure["message"]);
            }
        }
        println!("Required capsules: {:?}", required_capsules);
    }

    Ok(())
}

fn run_impact_command(cli: ImpactCli, project_root: &Path) -> Result<(), error::DecapodError> {
    use crate::core::workspace;

    let changed_files: Vec<String> = cli
        .changed_files
        .as_ref()
        .map(|s| s.split(',').map(|s| s.trim().to_string()).collect())
        .unwrap_or_default();

    let workspace_status = match workspace::get_workspace_status(project_root) {
        Ok(status) => status,
        Err(_) => {
            let response = serde_json::json!({
                "changed_files": changed_files,
                "will_fail_validate": false,
                "predicted_failures": [],
                "validation_predictions": [],
                "workspace": {
                    "git_branch": "unknown",
                    "git_is_protected": false,
                    "can_work": true,
                },
                "recommendation": "Could not determine workspace status"
            });
            println!("{}", serde_json::to_string_pretty(&response).unwrap());
            return Ok(());
        }
    };

    let mut predicted_failures = Vec::new();
    let mut validation_predictions = Vec::new();

    if workspace_status.git.is_protected {
        predicted_failures.push(serde_json::json!({
            "gate": "workspace_isolation",
            "status": "fail",
            "code": "WORKSPACE_REQUIRED",
            "message": "Operating on protected branch",
        }));
    } else {
        validation_predictions.push(serde_json::json!({
            "gate": "workspace_isolation",
            "status": "pass",
        }));
    }

    if !changed_files.is_empty() {
        validation_predictions.push(serde_json::json!({
            "gate": "file_changes_detected",
            "status": "pass",
            "changed_count": changed_files.len(),
        }));
    }

    let will_fail_validate = !predicted_failures.is_empty();

    let response = serde_json::json!({
        "changed_files": changed_files,
        "will_fail_validate": will_fail_validate,
        "predicted_failures": predicted_failures,
        "validation_predictions": validation_predictions,
        "workspace": {
            "git_branch": workspace_status.git.current_branch,
            "git_is_protected": workspace_status.git.is_protected,
            "can_work": workspace_status.can_work,
        },
        "recommendation": if will_fail_validate {
            "Fix workspace issues before running validate"
        } else if changed_files.is_empty() {
            "No changes detected - nothing to validate"
        } else {
            "Safe to run validate"
        }
    });

    if cli.format == "json" {
        println!("{}", serde_json::to_string_pretty(&response).unwrap());
    } else {
        println!("Impact Analysis");
        if will_fail_validate {
            println!("⚠ Validate will FAIL");
            for failure in &predicted_failures {
                println!("  - {}: {}", failure["code"], failure["message"]);
            }
        } else {
            println!("✓ Validate should pass");
        }
        if !changed_files.is_empty() {
            println!("Changed files: {:?}", changed_files);
        }
    }

    Ok(())
}

fn run_infer_command(cli: InferCli, project_root: &Path) -> Result<(), error::DecapodError> {
    let project_root = project_root.to_path_buf();

    match cli.command {
        InferCommand::Init(init_cli) => run_infer_init(init_cli, &project_root)?,
        InferCommand::Orientation(orientation_cli) => {
            run_infer_orientation(orientation_cli, &project_root)?
        }
        InferCommand::Validate(validate_cli) => run_infer_validate(validate_cli)?,
        InferCommand::Budget(budget_cli) => run_infer_budget(budget_cli, &project_root)?,
    }

    Ok(())
}

fn run_infer_init(cli: InferInitCli, project_root: &Path) -> Result<(), error::DecapodError> {
    use std::fs;

    let intent = cli.intent.trim().to_lowercase();
    let context_files: Vec<String> = cli
        .context
        .as_ref()
        .map(|s| s.split(',').map(|s| s.trim().to_string()).collect())
        .unwrap_or_default();

    let mut selected_context = Vec::new();
    let mut excluded_context = Vec::new();
    let excluded_extensions = ["md", "lock", "toml", "json", "yml", "yaml", "git"];

    let critical_keywords = ["fix", "bug", "error", "panic", "crash"];
    let docs_keywords = ["docs", "readme", "documentation", "guide"];
    let refactor_keywords = ["refactor", "rename", "restructure", "cleanup"];

    let intent_type = if critical_keywords.iter().any(|k| intent.contains(*k)) {
        "fix"
    } else if refactor_keywords.iter().any(|k| intent.contains(*k)) {
        "refactor"
    } else if docs_keywords.iter().any(|k| intent.contains(*k)) {
        "docs"
    } else {
        "unknown"
    };

    for file in &context_files {
        let path = project_root.join(file);
        if path.exists() {
            let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
            if excluded_extensions.contains(&ext) && intent_type != "docs" {
                excluded_context.push(file.clone());
                continue;
            }
            if file.contains("/tests/") && !intent.contains("test") {
                excluded_context.push(file.clone());
                continue;
            }
            selected_context.push(file.clone());
        }
    }

    if context_files.is_empty() {
        if let Ok(entries) = fs::read_dir(project_root.join("src")) {
            for entry in entries.flatten() {
                if let Ok(name) = entry.file_name().into_string()
                    && name.ends_with(".rs")
                    && !name.contains("_test")
                {
                    selected_context.push(format!("src/{}", name));
                }
            }
        }
        excluded_context = vec![
            "target/".to_string(),
            "build/".to_string(),
            ".git/".to_string(),
        ];
    }

    let token_budget = (selected_context.len() as u64 * 500).min(100_000);
    let clarification_required = intent.len() < 20 || intent_type == "unknown";

    let response = serde_json::json!({
        "intent": cli.intent,
        "intent_type": intent_type,
        "confidence": if clarification_required { "low" } else { "high" },
        "clarification_required": clarification_required,
        "clarification_question": if clarification_required {
            Some("Could you clarify what you'd like me to do?".to_string())
        } else { None },
        "selected_context": selected_context,
        "excluded_context": excluded_context,
        "selected_policies": ["default"],
        "token_budget": token_budget,
        "proof_required": intent_type == "fix",
        "boundaries": { "max_tokens": 100000, "context_files_limit": 20 }
    });

    if cli.format == "json" {
        println!("{}", serde_json::to_string_pretty(&response).unwrap());
    } else {
        println!("=== Inference Context ===");
        println!("Intent: {}", cli.intent);
        println!("Type: {}", intent_type);
        if clarification_required {
            println!("⚠ Clarification needed");
        }
        println!(
            "Selected files: {}",
            response["selected_context"]
                .as_array()
                .map(|a| a.len())
                .unwrap_or(0)
        );
        println!("Token budget: ~{}", token_budget);
    }

    Ok(())
}

fn run_infer_orientation(
    cli: InferOrientationCli,
    project_root: &Path,
) -> Result<(), error::DecapodError> {
    use crate::core::rpc::{DecisionGate, DecisionOption, OrientationPacket};

    let mut packet = OrientationPacket {
        user_goal: cli.intent.clone().unwrap_or_else(|| "Unknown".to_string()),
        task_id: cli.task_id.clone(),
        constraints: vec!["Strict adherence to AGENTS.md".to_string()],
        allowed_scope: vec![],
        forbidden_scope: vec![".decapod/".to_string()],
        relevant_areas: vec![],
        proof_required: vec!["decapod validate passes".to_string()],
        known_unknowns: vec![],
        decision_gates: vec![],
        next_action: "Perform research to map relevant files and symbols.".to_string(),
    };

    if let Some(ref id) = cli.task_id {
        let store_root = project_root.join(".decapod").join("data");
        if let Some(task) = todo::get_task(&store_root, id)? {
            packet.user_goal = task.title.clone();
            if !task.description.is_empty() {
                packet
                    .known_unknowns
                    .push(format!("Task detail: {}", task.description));
            }
            if !task.scope.is_empty() {
                packet.allowed_scope = task
                    .scope
                    .split(',')
                    .map(|s| s.trim().to_string())
                    .collect();
            }
        }
    }

    let intent_lower = packet.user_goal.to_lowercase();

    // Heuristics for precision
    if intent_lower.contains("fix") || intent_lower.contains("bug") {
        packet
            .proof_required
            .push("Reproduction test case".to_string());
        packet
            .constraints
            .push("Do not introduce regressions".to_string());
    }

    if intent_lower.contains("refactor")
        || intent_lower.contains("architecture")
        || intent_lower.contains("interface")
    {
        packet.decision_gates.push(DecisionGate {
            decision: "Architectural alignment".to_string(),
            rationale: "Changes to core structures or interfaces require human alignment on long-term maintainability.".to_string(),
            options: vec![
                DecisionOption {
                    label: "Conservative (wrapper/adapter)".to_string(),
                    impact: "Minimizes immediate breakage".to_string(),
                },
                DecisionOption {
                    label: "Aggressive (breaking change)".to_string(),
                    impact: "Cleaner long-term architecture, requires migration".to_string(),
                },
            ],
            recommendation: "Prefer conservative adaptation unless the current interface is fundamentally broken.".to_string(),
            validation_proof: "Contract conformance tests and migration guide".to_string(),
        });
        packet.next_action =
            "STOP: A decision gate is active. Present options to the human before proceeding."
                .to_string();
    }

    if intent_lower.contains("test") {
        packet.relevant_areas.push("tests/fixtures".to_string());
    }

    if packet.allowed_scope.is_empty() {
        if intent_lower.contains("test") {
            packet.allowed_scope = vec!["tests/".to_string()];
        } else {
            packet.allowed_scope = vec!["src/".to_string(), "tests/".to_string()];
        }
    }

    if cli.format == "json" {
        println!("{}", serde_json::to_string_pretty(&packet).unwrap());
    } else {
        println!("=== ORIENTATION PACKET ===");
        println!("Goal:    {}", packet.user_goal);
        if let Some(ref id) = packet.task_id {
            println!("Task:    {}", id);
        }
        println!("Next:    {}", packet.next_action);
        println!(
            "Scope:   +{:?} -{:?}",
            packet.allowed_scope, packet.forbidden_scope
        );
        println!("Proof:   {:?}", packet.proof_required);

        if !packet.decision_gates.is_empty() {
            println!("\n⚠ DECISION GATES REQUIRED:");
            for gate in &packet.decision_gates {
                println!("  - DECISION:       {}", gate.decision);
                println!("    RATIONALE:      {}", gate.rationale);
                println!("    RECOMMENDATION: {}", gate.recommendation);
                println!("    OPTIONS:");
                for opt in &gate.options {
                    println!("      * {}: {}", opt.label, opt.impact);
                }
            }
        }
    }

    Ok(())
}

fn run_infer_validate(cli: InferValidateCli) -> Result<(), error::DecapodError> {
    let result = cli.result.trim();
    let intent = cli.intent.trim().to_lowercase();

    let proof_provided =
        result.contains("fn ") || result.contains("struct ") || result.contains("impl ");
    let mut issues = Vec::new();

    if result.contains("error") || result.contains("panic") {
        issues.push("Potential error/panic in output");
    }

    let intent_match = if intent.contains("fix") || intent.contains("bug") {
        result.contains("fix") || result.contains("change")
    } else {
        true
    };

    let response = serde_json::json!({
        "intent": cli.intent,
        "intent_match": intent_match,
        "proof_provided": proof_provided,
        "issues": issues,
        "advisory": if issues.is_empty() { "ok" } else { "review recommended" }
    });

    if cli.format == "json" {
        println!("{}", serde_json::to_string_pretty(&response).unwrap());
    } else {
        println!("=== Validation ===");
        println!("Intent match: {}", if intent_match { "✓" } else { "✗" });
        println!("Proof provided: {}", if proof_provided { "✓" } else { "✗" });
    }

    Ok(())
}

fn run_infer_budget(cli: InferBudgetCli, project_root: &Path) -> Result<(), error::DecapodError> {
    use std::fs;

    let context_files: Vec<String> = cli
        .context
        .as_ref()
        .map(|s| s.split(',').map(|s| s.trim().to_string()).collect())
        .unwrap_or_default();

    let mut total_tokens = 0u64;
    for file in &context_files {
        let path = project_root.join(file);
        if let Ok(content) = fs::read_to_string(&path) {
            total_tokens += content.lines().count() as u64 * 8;
        }
    }

    let base_tokens = 500u64;
    let response = serde_json::json!({
        "intent": cli.intent,
        "context_tokens": total_tokens,
        "base_tokens": base_tokens,
        "estimated_total": total_tokens + base_tokens,
        "within_budget": total_tokens + base_tokens < 100000,
        "token_budget": { "soft_limit": 100000, "recommended": 80000 }
    });

    if cli.format == "json" {
        println!("{}", serde_json::to_string_pretty(&response).unwrap());
    } else {
        println!("=== Token Budget ===");
        println!("Context: ~{} tokens", total_tokens);
        println!("Total: ~{} tokens", total_tokens + base_tokens);
        println!(
            "Within 100k: {}",
            if total_tokens + base_tokens < 100000 {
                "✓"
            } else {
                "⚠"
            }
        );
    }

    Ok(())
}

fn run_demo_command(cli: DemoCli, project_root: &Path) -> Result<(), error::DecapodError> {
    use crate::core::workspace;

    println!("==============================================");
    println!("Decapod Interlock Demo: Predict Before You Fail");
    println!("==============================================\n");

    match cli.demo.as_str() {
        "interlock" => {
            println!("Step 1: Check workspace status");
            let status = workspace::get_workspace_status(project_root)?;
            println!("  Branch: {}", status.git.current_branch);
            println!("  Protected: {}", status.git.is_protected);
            println!("  Can work: {}\n", status.can_work);

            println!("Step 2: Run preflight to predict validate outcome");
            run_preflight_command(
                PreflightCli {
                    op: Some("validate".to_string()),
                    format: "json".to_string(),
                    session: None,
                },
                project_root,
            )?;
            println!();

            println!("Step 3: Run impact to predict what will happen with changes");
            run_impact_command(
                ImpactCli {
                    changed_files: Some("src/core/validate.rs,src/lib.rs".to_string()),
                    format: "json".to_string(),
                    predict: true,
                },
                project_root,
            )?;
            println!();

            println!("Step 4: Verify prediction matches reality");
            println!("  (Running validate would show WORKSPACE_REQUIRED on protected branch)\n");

            println!("==============================================");
            println!("Key insight: preflight told us:");
            println!("  - risk_flags: [protected_branch]");
            println!("  - likely_failures: [WORKSPACE_REQUIRED]");
            println!("  - next_best_actions: [Run: decapod workspace ensure]");
            println!();
            println!("Following that guidance prevents the failure instead of reacting to it.");
            println!("==============================================");

            Ok(())
        }
        _ => {
            println!("Available demos:");
            println!("  interlock  - Shows preflight + impact prediction");
            Ok(())
        }
    }
}

#[cfg(test)]
mod init_prompt_tests {
    use super::*;
    use tempfile::tempdir;

    #[test]
    fn arrow_keys_move_selection_without_entering_input_text() {
        let default = vec!["Rust".to_string()];
        let selected = selector_result_for_input(LANGUAGES, &default, b"\x1b[B\x1b[B\x1b[A\n");

        assert_eq!(selected, "TypeScript");
        assert!(!selected.contains("\x1b"));
        assert!(!selected.contains("[B"));
    }

    #[test]
    fn selector_render_tracks_current_selection() {
        let default = vec!["Python".to_string()];

        let default_lines = selector_render_for_input(LANGUAGES, None, &default, b"\n");
        assert_eq!(default_lines[0], "    choice: Python");
        assert!(
            default_lines
                .iter()
                .any(|line| line == "    > ✓  4. Python")
        );

        let typed_lines = selector_render_for_input(LANGUAGES, None, &default, b"go\n");
        assert_eq!(typed_lines[0], "    choice: go");
        assert!(typed_lines.iter().any(|line| line == "    >    5. Go"));

        let custom_lines =
            selector_render_for_input(LANGUAGES, None, &default, b"not-a-language\n");
        assert_eq!(custom_lines[0], "    choice: not-a-language");
    }

    #[test]
    fn enter_accepts_inferred_default_language() {
        let default = vec!["Python".to_string()];
        let selected = selector_result_for_input(LANGUAGES, &default, b"\n");

        assert_eq!(selected, "Python");
    }

    #[test]
    fn numeric_language_selection_targets_numbered_option() {
        let default = vec!["Rust".to_string()];
        let selected = selector_result_for_input(LANGUAGES, &default, b"4\n");

        assert_eq!(selected, "Python");
        assert_eq!(parse_language_choice(&selected), vec!["Python".to_string()]);
    }

    #[test]
    fn typed_language_selection_targets_matching_language() {
        let default = vec!["Rust".to_string()];
        let selected = selector_result_for_input(LANGUAGES, &default, b"python\n");

        assert_eq!(selected, "Python");
        assert_eq!(parse_language_choice("python"), vec!["Python".to_string()]);
    }

    #[test]
    fn selector_render_shows_one_navigable_option_list() {
        let default = vec!["cli".to_string()];
        let options = ARCH_DIRECTIONS
            .iter()
            .map(|(arch, _)| *arch)
            .collect::<Vec<_>>();
        let descriptions = ARCH_DIRECTIONS
            .iter()
            .map(|(_, description)| *description)
            .collect::<Vec<_>>();

        let lines = selector_render_for_input(&options, Some(&descriptions), &default, b"\x1b[B\n");

        assert_eq!(lines[0], "    choice: lambda");
        assert_eq!(
            lines
                .iter()
                .filter(|line| line.contains("webapp -> Web application"))
                .count(),
            1
        );
        assert!(lines.iter().any(|line| line.starts_with("    >")));
    }

    #[test]
    fn diagram_notation_selector_uses_terminal_readable_options() {
        let default = vec![String::from("ascii")];

        let lines = selector_render_for_input(
            DIAGRAM_NOTATION_OPTIONS,
            Some(DIAGRAM_NOTATION_DESCRIPTIONS),
            &default,
            b"\x1b[B\n",
        );

        assert_eq!(lines[0], "    choice: mermaid");
        assert_eq!(
            lines
                .iter()
                .filter(|line| line.contains("ascii -> ASCII/text blocks"))
                .count(),
            1
        );
        assert!(
            lines
                .iter()
                .any(|line| line.contains("mermaid -> Mermaid diagrams"))
        );
    }

    #[test]
    fn diagram_notation_choice_accepts_text_aliases() {
        assert_eq!(
            parse_diagram_style_choice("", InitDiagramStyle::Mermaid).unwrap(),
            InitDiagramStyle::Mermaid
        );
        assert_eq!(
            parse_diagram_style_choice("text", InitDiagramStyle::Mermaid).unwrap(),
            InitDiagramStyle::Ascii
        );
        assert_eq!(
            parse_diagram_style_choice("ascii/text", InitDiagramStyle::Mermaid).unwrap(),
            InitDiagramStyle::Ascii
        );
        assert_eq!(
            parse_diagram_style_choice("2", InitDiagramStyle::Ascii).unwrap(),
            InitDiagramStyle::Mermaid
        );
        assert!(parse_diagram_style_choice("plantuml", InitDiagramStyle::Ascii).is_err());
    }

    #[test]
    fn comma_separated_language_selection_is_preserved() {
        assert_eq!(
            parse_language_choice("4, shell, typescript"),
            vec![
                "Python".to_string(),
                "Shell".to_string(),
                "TypeScript".to_string()
            ]
        );
    }

    #[test]
    fn line_prompt_escape_backstop_strips_raw_ansi_sequences() {
        assert_eq!(
            strip_ansi_escape_sequences("^[[B\x1b[Bpython\x1b[A"),
            "^[[Bpython"
        );
    }

    #[test]
    fn inferred_language_wins_over_architecture_recommendation_for_default() {
        assert_eq!(
            language_choice_seed(&["Python".to_string()], &["Rust".to_string()]),
            vec!["Python".to_string()]
        );
    }

    #[test]
    fn mixed_scripts_repo_infers_multiple_languages_without_compiled_bias() {
        let tmp = tempdir().expect("tempdir");
        fs::write(tmp.path().join("task.py"), "print('ok')\n").expect("python fixture");
        fs::write(tmp.path().join("deploy.sh"), "#!/usr/bin/env bash\n").expect("shell fixture");
        fs::write(tmp.path().join("env.zsh"), "printenv\n").expect("zsh fixture");
        fs::write(tmp.path().join("tool.ts"), "export const ok = true;\n").expect("ts fixture");
        fs::write(tmp.path().join("probe.go"), "package main\n").expect("go fixture");

        let ctx = infer_repo_context(tmp.path());

        assert!(ctx.primary_languages.contains(&"go".to_string()));
        assert!(ctx.primary_languages.contains(&"python".to_string()));
        assert!(ctx.primary_languages.contains(&"shell".to_string()));
        assert!(ctx.primary_languages.contains(&"typescript".to_string()));
        assert_ne!(ctx.primary_languages, vec!["rust".to_string()]);
    }
}
