use std::collections::HashSet;
use std::env;
use std::fs;
use std::io::{self, Write};
use std::path::{Component, Path, PathBuf};
use std::process::{Command, Stdio};

use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use rho_core::{
    IdentityBundleManifest, LocalIdentityManifest, RhoResult, SignatureContext, SignatureManifest,
    Tool, ToolManifest, arg_value, ensure_parent, file_digest, from_json, from_yaml, has_flag,
    is_rho_encrypted_text, normalize_actor_id, normalize_repo_id, path_matches_pattern, providers,
    to_json_pretty, to_yaml, validate_actor_id, validate_relative_safe_path, validate_request_id,
    yaml_quote, yaml_top_level_kind,
};
use serde::{Deserialize, Serialize};

fn usage() -> ! {
    eprintln!(
        "usage:\n  rho repo create <owner/repo|repo> [--root <path>] (--public|--private|--internal) [--yes]\n  rho repo sync [<owner/repo>] [--root <path>] [--branch <name>]\n  rho repo init [--repo-id <rho://repo/...|owner/repo|github-url>] [--root <path>] [--owner <rho-id|github/handle|handle>] [--yes] [--force]\n  rho repo join <rho-id|github/handle|handle|owner/repo> [--root <path>] [--no-branch] [--commit] [--push] [--pr]\n  rho repo admit <rho-id|github/handle|handle> [--root <path>] [--rho-bin <path>] [--no-branch] [--commit] [--push] [--pr]\n  rho repo admit-pr <number> [--root <path>] [--rho-bin <path>] [--new-pr] [--commit] [--push] [--pr]\n  rho repo create-pr [--root <path>] --title <text> [--body <text>] [--web]\n  rho repo merge-pr <number> [--root <path>] [--merge|--squash|--rebase] [--delete-branch]\n  rho repo add-user <rho-id|github/handle|handle> [--root <path>]\n  rho repo import-users [--root <path>]\n  rho repo sign-governance [--root <path>] [--identity <rho-id>] [--no-stage]\n  rho repo protect-path <pattern> [--root <path>] [--recipient <rho-id>...] [--key-file <path>] [--rho-bin <path>]\n  rho repo install-filters [--root <path>] [--rho-bin <path>]\n  rho repo refresh-protected [--root <path>] [--rho-bin <path>]\n  rho repo migrate-inbox-paths [--root <path>] [--dry-run]\n  rho repo audit-encryption [--root <path>]\n  rho repo status [--root <path>]\n  rho repo doctor [--root <path>] [--flow] [--governance|--no-governance]\n  rho repo check-pr [--root <path>] [--event <github-event.json>] [--actor <login|rho-id>] [--changed-file <path>...] [--base-ref <ref>] [--format json]"
    );
    std::process::exit(2);
}

pub fn run(args: &[String]) -> RhoResult<()> {
    let Some(command) = args.first().map(String::as_str) else {
        usage();
    };
    match command {
        "create" => create(&args[1..]),
        "sync" => sync(&args[1..]),
        "init" => init(&args[1..]),
        "join" => join(&args[1..]),
        "admit" => admit(&args[1..]),
        "admit-pr" => admit_pr(&args[1..]),
        "create-pr" | "open-pr" => create_pr(&args[1..]),
        "merge-pr" => merge_pr(&args[1..]),
        "add-user" => add_user(&args[1..]),
        "import-users" => import_users(&args[1..]),
        "sign-governance" => sign_governance(&args[1..]),
        "protect-path" => protect_path(&args[1..]),
        "install-filters" => install_filters(&args[1..]),
        "refresh-protected" => refresh_protected(&args[1..]),
        "migrate-inbox-paths" => migrate_inbox_paths(&args[1..]),
        "audit-encryption" => audit_encryption(&args[1..]),
        "status" => status(&args[1..]),
        "doctor" => doctor(&args[1..]),
        "check-pr" => check_pr(&args[1..]),
        "--help" | "-h" => usage(),
        _ => usage(),
    }
}

fn import_users(args: &[String]) -> RhoResult<()> {
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    ensure_repo(&root)?;
    let participants_dir = root.join("rho/participants");
    let home = rho_home();
    let mut imported = 0usize;
    let mut skipped_local = 0usize;
    if !participants_dir.is_dir() {
        println!("imported users: 0");
        return Ok(());
    }
    for entry in fs::read_dir(&participants_dir)? {
        let path = entry?.path();
        if path.extension().and_then(|value| value.to_str()) != Some("yaml")
            || path
                .file_name()
                .and_then(|value| value.to_str())
                .is_some_and(|name| name.ends_with(".rhosig.yaml"))
        {
            continue;
        }
        let bundle: IdentityBundleManifest = from_yaml(&fs::read_to_string(&path)?)?;
        validate_actor_id(&bundle.identity.id)?;
        let handle = github_handle_from_identity_id(&bundle.identity.id)?;
        if bundle.identity.kind != "github" || bundle.identity.handle != handle {
            return Err(format!("participant identity mismatch: {}", path.display()).into());
        }
        let local_path = home
            .join("identities")
            .join("github")
            .join(format!("{handle}.yaml"));
        if local_path.is_file() {
            skipped_local += 1;
            println!("local {}", bundle.identity.id);
            continue;
        }
        let peer_path = home
            .join("peers")
            .join("github")
            .join(format!("{handle}.yaml"));
        ensure_parent(&peer_path)?;
        fs::write(&peer_path, to_yaml(&bundle)?)?;
        imported += 1;
        println!("{}", peer_path.display());
    }
    println!("imported users: {imported}");
    if skipped_local > 0 {
        println!("already local identities: {skipped_local}");
    }
    Ok(())
}

fn create(args: &[String]) -> RhoResult<()> {
    let Some(repo_arg) = args.first() else {
        usage();
    };
    let owner_identity = normalize_actor_id(
        &arg_value(args, "--owner")
            .or_else(active_identity)
            .ok_or("rho repo create requires --owner or a global --profile/--identity")?,
    )?;
    let owner_handle = github_handle_from_identity_id(&owner_identity)?;
    let repo_slug = github_repo_slug(repo_arg, &owner_handle)?;
    let repo_name = repo_slug
        .split_once('/')
        .map(|(_, repo)| repo)
        .ok_or("invalid GitHub repo slug")?;
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| default_project_root(&owner_handle, repo_name));
    let visibility = repo_visibility_arg(args)?;

    fs::create_dir_all(&root)?;
    if !root.join(".git").is_dir() {
        git_init_main(&root)?;
    }
    configure_profile_ssh(&root, &owner_handle)?;
    init(&[
        "--root".to_string(),
        root.display().to_string(),
        "--repo-id".to_string(),
        repo_slug.clone(),
        "--owner".to_string(),
        owner_identity.clone(),
        "--yes".to_string(),
    ])?;
    add_user(&[
        owner_identity.clone(),
        "--root".to_string(),
        root.display().to_string(),
    ])?;
    protect_path(&[
        format!(
            "rho/messages/inbox/{}/**",
            identity_inbox_relative_path(&owner_identity)?.display()
        ),
        "--root".to_string(),
        root.display().to_string(),
        "--recipient".to_string(),
        owner_identity.clone(),
    ])?;
    sign_governance(&["--root".to_string(), root.display().to_string()])?;
    doctor(&["--root".to_string(), root.display().to_string()])?;
    git_add_relative(&root, ".gitattributes")?;
    git_add_relative(&root, ".gitignore")?;
    git_add_relative(&root, "rho")?;
    git_add_relative(&root, "rho.yaml")?;
    git_commit(&root, &format!("Initialize Rho project {repo_slug}"))?;
    gh_repo_create(&root, &repo_slug, visibility)?;
    git_push(&root, "origin", "main")?;
    println!("repo: {repo_slug}");
    println!("root: {}", root.display());
    println!("pushed: main");
    Ok(())
}

fn sync(args: &[String]) -> RhoResult<()> {
    let active = normalize_actor_id(&active_identity().ok_or(
        "rho repo sync requires a global --profile/--identity so Rho knows where to write",
    )?)?;
    let handle = github_handle_from_identity_id(&active)?;
    let branch = arg_value(args, "--branch").unwrap_or_else(|| "main".to_string());
    let repo_arg = args.iter().find(|arg| !arg.starts_with("--")).cloned();
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .or_else(|| {
            repo_arg
                .as_deref()
                .and_then(repo_name_from_slug)
                .map(|repo_name| default_project_root(&handle, repo_name))
        })
        .unwrap_or_else(|| PathBuf::from("."));
    let source_slug = match repo_arg {
        Some(value) => github_repo_slug(&value, &handle)?,
        None => detect_remote_repo(&root, "upstream")
            .or_else(|| detect_remote_repo(&root, "origin"))
            .ok_or("could not infer source repo; pass owner/repo")?,
    };
    let (source_owner, repo_name) = source_slug
        .split_once('/')
        .ok_or("invalid GitHub repo slug")?;

    if !root.join(".git").is_dir() {
        if source_owner == handle {
            gh_repo_clone(&source_slug, &root)?;
        } else {
            ensure_join_clone(&root, &source_slug, &handle, repo_name)?;
        }
    }

    if source_owner != handle {
        let fork_slug = format!("{handle}/{repo_name}");
        let sync_status = Command::new("gh")
            .args([
                "repo",
                "sync",
                &fork_slug,
                "--source",
                &source_slug,
                "--branch",
                &branch,
            ])
            .status()?;
        if !sync_status.success() {
            return Err(format!("gh repo sync failed for {fork_slug} from {source_slug}").into());
        }
        ensure_remote(&root, "upstream", &github_ssh_url(&source_slug))?;
    }

    configure_profile_ssh(&root, &handle)?;
    git_fetch(&root, "origin", &branch)?;
    switch_branch(&root, &branch)?;
    git_merge_ff(&root, &format!("origin/{branch}"))?;
    init(&[
        "--root".to_string(),
        root.display().to_string(),
        "--yes".to_string(),
    ])?;
    println!("synced: {source_slug}");
    println!("root: {}", root.display());
    println!("read remote: upstream ({source_slug})");
    println!(
        "write remote: origin ({}/{repo_name})",
        if source_owner == handle {
            source_owner
        } else {
            &handle
        }
    );
    Ok(())
}

fn sign_governance(args: &[String]) -> RhoResult<()> {
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    ensure_repo(&root)?;
    let repo: RepoManifest = from_yaml(&fs::read_to_string(root.join("rho/repo.yaml"))?)?;
    let identity = match arg_value(args, "--identity") {
        Some(identity) => normalize_actor_id(&identity)?,
        None => repo
            .repo
            .owner
            .clone()
            .ok_or("repo owner missing; pass --identity explicitly")?,
    };
    validate_actor_id(&identity)?;

    let files = governance_manifest_files(&root)?;
    if files.is_empty() {
        println!("governance files: none");
        return Ok(());
    }

    for file in files {
        let signed_path = root.join(&file);
        let signature_path = governance_signature_path(&root, &file)?;
        sign_file(&signed_path, &signature_path, &identity)?;
        println!("{}", signature_path.display());
        if !has_flag(args, "--no-stage") {
            git_add_relative(&root, &file)?;
            git_add_path(&root, &signature_path)?;
        }
    }
    if !has_flag(args, "--no-stage") {
        println!("staged governance signatures");
    }
    Ok(())
}

fn status(args: &[String]) -> RhoResult<()> {
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    ensure_repo(&root)?;
    let permissions_path = root.join("rho/policy/permissions.yaml");
    let permissions: PermissionsManifest = from_yaml(&fs::read_to_string(&permissions_path)?)?;
    let files = tracked_and_untracked_files(&root)?;
    let mut protected_count = 0usize;
    let mut warning_count = 0usize;

    println!("rho status");
    println!("root: {}", root.display());

    for file in files {
        let Some(rule) = permissions.permissions.iter().find(|rule| {
            !rule.read.public
                && !rule.read.encrypted_to.is_empty()
                && path_matches_pattern(&file, &rule.pattern)
        }) else {
            continue;
        };
        protected_count += 1;
        println!();
        println!("{file}");
        println!("  policy: {}", rule.pattern);
        println!("  read: encrypted to {}", rule.read.encrypted_to.join(", "));
        match index_blob(&root, &file) {
            Some(content)
                if yaml_top_level_kind(&content).as_deref() == Some("rho_recipient_envelope") =>
            {
                let missing = missing_recipients(&content, &rule.read.encrypted_to);
                if missing.is_empty() {
                    println!("  staged: encrypted recipient envelope");
                } else {
                    warning_count += 1;
                    println!("  staged: encrypted, but missing {}", missing.join(", "));
                }
            }
            Some(content)
                if yaml_top_level_kind(&content).as_deref() == Some("rho_transparent_file") =>
            {
                warning_count += 1;
                println!("  staged: encrypted with repo key, not recipient envelope");
            }
            Some(_) => {
                warning_count += 1;
                println!("  staged: NOT encrypted");
            }
            None => {
                println!("  staged: not staged");
                println!("  next add: will use rho-crypt filter from .gitattributes");
            }
        }
    }

    if protected_count == 0 {
        println!("protected files: none");
    } else {
        println!();
        println!("protected files: {protected_count}");
    }
    if warning_count > 0 {
        return Err(format!("{warning_count} protected file warning(s)").into());
    }
    Ok(())
}

#[derive(Debug, Serialize)]
struct CheckPrResult {
    status: String,
    actor: String,
    role: String,
    reason: String,
    close_recommended: bool,
    changed_files: Vec<String>,
    violations: Vec<CheckPrViolation>,
}

#[derive(Debug, Serialize)]
struct CheckPrViolation {
    path: String,
    reason: String,
}

#[derive(Debug, Deserialize)]
struct RevocationsManifest {
    #[serde(default)]
    revocations: Vec<RevocationRecord>,
    #[serde(default)]
    blocks: Vec<RevocationRecord>,
}

#[derive(Debug, Deserialize)]
struct RevocationRecord {
    #[serde(default)]
    identity: String,
    #[serde(default)]
    actor: String,
    #[serde(default)]
    decision: String,
}

fn check_pr(args: &[String]) -> RhoResult<()> {
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    let changed_files = check_pr_changed_files(args, &root)?;
    if !root.join("rho/repo.yaml").is_file() {
        let actor = check_pr_actor(args).unwrap_or_else(|_| "unknown".to_string());
        let result = CheckPrResult {
            status: "pass".to_string(),
            actor,
            role: "unmanaged".to_string(),
            reason: "rho repo not initialized; policy check skipped".to_string(),
            close_recommended: false,
            changed_files,
            violations: Vec::new(),
        };
        print_check_pr_result(args, &result)?;
        return Ok(());
    }
    let actor = check_pr_actor(args)?;
    validate_actor_id(&actor)?;
    let role = check_pr_role(&root, &actor)?;
    let violations = check_pr_violations(&actor, &role, &changed_files);
    let blocked_actor = role == "blocked";
    let status = if blocked_actor || !violations.is_empty() {
        "blocked"
    } else if role == "unknown" {
        "pending"
    } else {
        "pass"
    };
    let reason = check_pr_reason(status, &role, blocked_actor, &violations);
    let result = CheckPrResult {
        status: status.to_string(),
        actor,
        role,
        reason,
        close_recommended: status == "blocked",
        changed_files,
        violations,
    };

    print_check_pr_result(args, &result)?;
    Ok(())
}

fn print_check_pr_result(args: &[String], result: &CheckPrResult) -> RhoResult<()> {
    if arg_value(args, "--format").as_deref() == Some("json") || has_flag(args, "--json") {
        print!("{}", to_json_pretty(&result)?);
    } else {
        println!("status: {}", result.status);
        println!("actor: {}", result.actor);
        println!("role: {}", result.role);
        println!("reason: {}", result.reason);
        for violation in &result.violations {
            println!("blocked: {}: {}", violation.path, violation.reason);
        }
    }
    Ok(())
}

fn check_pr_actor(args: &[String]) -> RhoResult<String> {
    if let Some(actor) = arg_value(args, "--actor") {
        return normalize_actor_id(&actor);
    }
    if let Some(event_path) = arg_value(args, "--event") {
        let text = fs::read_to_string(&event_path)
            .map_err(|error| format!("cannot read GitHub event file {event_path}: {error}"))?;
        let event: serde_json::Value = from_json(&text)?;
        for pointer in [
            "/pull_request/user/login",
            "/sender/login",
            "/issue/user/login",
        ] {
            if let Some(login) = event.pointer(pointer).and_then(|value| value.as_str())
                && !login.trim().is_empty()
            {
                return normalize_actor_id(login);
            }
        }
        return Err(format!("GitHub event has no usable actor login: {event_path}").into());
    }
    Err("check-pr requires --actor or --event".into())
}

fn check_pr_role(root: &Path, actor: &str) -> RhoResult<String> {
    if is_actor_blocked(root, actor)? {
        return Ok("blocked".to_string());
    }
    let membership_path = root.join("rho/membership.yaml");
    let membership: MembershipManifest = from_yaml(&fs::read_to_string(&membership_path)?)?;
    for member in membership.members {
        if member.identity == actor {
            return Ok(match member.role.as_str() {
                "owner" | "admin" => "admin".to_string(),
                "collaborator" | "member" => "member".to_string(),
                other => other.to_string(),
            });
        }
    }
    Ok("unknown".to_string())
}

fn is_actor_blocked(root: &Path, actor: &str) -> RhoResult<bool> {
    let path = root.join("rho/policy/revocations.yaml");
    if !path.is_file() {
        return Ok(false);
    }
    let manifest: RevocationsManifest = from_yaml(&fs::read_to_string(path)?)?;
    Ok(manifest
        .revocations
        .into_iter()
        .chain(manifest.blocks)
        .any(|record| {
            let identity = if record.identity.is_empty() {
                record.actor
            } else {
                record.identity
            };
            identity == actor
                && (record.decision.is_empty()
                    || matches!(
                        record.decision.as_str(),
                        "block" | "blocked" | "deny" | "denied" | "revoked"
                    ))
        }))
}

fn check_pr_changed_files(args: &[String], root: &Path) -> RhoResult<Vec<String>> {
    let mut files = repeated_arg(args, "--changed-file");
    if files.is_empty()
        && let Some(event_path) = arg_value(args, "--event")
    {
        files = changed_files_from_event(&event_path)?;
    }
    if files.is_empty()
        && let Some(base_ref) = arg_value(args, "--base-ref")
    {
        files = changed_files_from_git(root, &base_ref)?;
    }
    files.sort();
    files.dedup();
    for file in &files {
        validate_relative_safe_path(file)?;
    }
    Ok(files)
}

fn changed_files_from_event(event_path: &str) -> RhoResult<Vec<String>> {
    let text = fs::read_to_string(event_path)
        .map_err(|error| format!("cannot read GitHub event file {event_path}: {error}"))?;
    let event: serde_json::Value = from_json(&text)?;
    let Some(files) = event
        .get("rho_changed_files")
        .and_then(|value| value.as_array())
    else {
        return Ok(Vec::new());
    };
    Ok(files
        .iter()
        .filter_map(|value| value.as_str().map(ToOwned::to_owned))
        .collect())
}

fn changed_files_from_git(root: &Path, base_ref: &str) -> RhoResult<Vec<String>> {
    let output = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["diff", "--name-only", "-z", &format!("{base_ref}...HEAD")])
        .output()?;
    if !output.status.success() {
        return Err(format!("git diff failed against base ref {base_ref}").into());
    }
    Ok(output
        .stdout
        .split(|byte| *byte == 0)
        .filter(|entry| !entry.is_empty())
        .filter_map(|entry| String::from_utf8(entry.to_vec()).ok())
        .collect())
}

fn check_pr_violations(actor: &str, role: &str, changed_files: &[String]) -> Vec<CheckPrViolation> {
    let mut violations = Vec::new();
    for path in changed_files {
        let reason = if role == "blocked" {
            Some("actor is blocked".to_string())
        } else if role == "admin" || (role == "unknown" && is_unknown_join_path(actor, path)) {
            None
        } else if is_admin_only_path(path) {
            Some("only admins may change Rho policy, membership, participants, tools, actions, or workflows".to_string())
        } else if role == "unknown" && !is_unknown_join_path(actor, path) {
            Some("unknown actors may only submit their own join request files".to_string())
        } else {
            None
        };
        if let Some(reason) = reason {
            violations.push(CheckPrViolation {
                path: path.clone(),
                reason,
            });
        }
    }
    violations
}

fn is_admin_only_path(path: &str) -> bool {
    path == "rho.yaml"
        || path == ".gitmodules"
        || path.starts_with("rho/policy/")
        || path == "rho/membership.yaml"
        || path.starts_with("rho/participants/")
        || path.starts_with("rho/tools/")
        || path.starts_with(".github/workflows/")
        || path.starts_with("action/")
}

fn is_unknown_join_path(actor: &str, path: &str) -> bool {
    let Ok(handle) = github_handle_from_identity_id(actor) else {
        return false;
    };
    path == format!("rho/participants/{handle}.yaml")
        || path.starts_with("rho/requests/join-")
        || path.starts_with(&format!("rho/requests/{handle}/join-"))
}

fn check_pr_reason(
    status: &str,
    role: &str,
    blocked_actor: bool,
    violations: &[CheckPrViolation],
) -> String {
    if blocked_actor {
        return "actor is blocked by Rho policy".to_string();
    }
    if let Some(first) = violations.first() {
        return format!("{}: {}", first.path, first.reason);
    }
    match (status, role) {
        ("pending", "unknown") => "unknown actor requires admin review".to_string(),
        ("pass", "admin") => "admin actor is allowed".to_string(),
        ("pass", "member") => "member actor is allowed".to_string(),
        _ => "Rho PR policy evaluated".to_string(),
    }
}

fn audit_encryption(args: &[String]) -> RhoResult<()> {
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    ensure_repo(&root)?;
    let permissions_path = root.join("rho/policy/permissions.yaml");
    let permissions: PermissionsManifest = from_yaml(&fs::read_to_string(&permissions_path)?)?;
    let files = tracked_files(&root)?;
    let mut protected_count = 0usize;
    let mut checked_blobs = 0usize;
    let mut failures = Vec::new();

    println!("rho encryption audit");
    println!("root: {}", root.display());

    for file in files {
        let Some(rule) = permissions.permissions.iter().find(|rule| {
            !rule.read.public
                && !rule.read.encrypted_to.is_empty()
                && path_matches_pattern(&file, &rule.pattern)
        }) else {
            continue;
        };
        protected_count += 1;
        println!();
        println!("{file}");
        if let Some(content) = index_blob(&root, &file) {
            checked_blobs += 1;
            audit_encrypted_blob(
                "index",
                &file,
                &content,
                &rule.read.encrypted_to,
                &mut failures,
            );
        } else {
            println!("  index: not present");
        }
        if let Some(content) = head_blob(&root, &file) {
            checked_blobs += 1;
            audit_encrypted_blob(
                "HEAD",
                &file,
                &content,
                &rule.read.encrypted_to,
                &mut failures,
            );
        } else {
            println!("  HEAD: not present");
        }
    }

    if protected_count == 0 {
        println!("protected files: none");
    } else {
        println!();
        println!("protected files: {protected_count}");
        println!("checked encrypted blobs: {checked_blobs}");
    }
    if failures.is_empty() {
        println!("rho encryption audit passed");
        return Ok(());
    }

    eprintln!("rho encryption audit failed");
    for failure in &failures {
        eprintln!("- {failure}");
    }
    Err(format!("{} encryption audit failure(s)", failures.len()).into())
}

fn audit_encrypted_blob(
    label: &str,
    relative_path: &str,
    content: &str,
    recipients: &[String],
    failures: &mut Vec<String>,
) {
    match yaml_top_level_kind(content).as_deref() {
        Some("rho_recipient_envelope") => {
            let missing = missing_recipients(content, recipients);
            if missing.is_empty() {
                println!("  {label}: encrypted recipient envelope");
            } else {
                println!("  {label}: encrypted, but missing {}", missing.join(", "));
                failures.push(format!(
                    "{label} blob missing recipient(s) for {relative_path}: {}",
                    missing.join(", ")
                ));
            }
            return;
        }
        Some("rho_transparent_file") => {
            println!("  {label}: encrypted with repo key, not recipient envelope");
            failures.push(format!(
                "{label} blob uses repo-key encryption where recipients are required: {relative_path}"
            ));
            return;
        }
        _ => {}
    }
    println!("  {label}: NOT encrypted");
    failures.push(format!("{label} blob is not encrypted: {relative_path}"));
}

fn init(args: &[String]) -> RhoResult<()> {
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    if root.join("rho/repo.yaml").is_file() && !has_flag(args, "--force") {
        return init_existing_repo(args, &root);
    }
    let repo_id = arg_value(args, "--repo-id")
        .or_else(|| detect_github_remote(&root))
        .map(|value| normalize_repo_id(&value))
        .transpose()?
        .ok_or(
            "could not infer repo id; pass --repo-id owner/repo or https://github.com/owner/repo",
        )?;
    validate_repo_id(&repo_id)?;
    let owner = arg_value(args, "--owner")
        .or_else(active_identity)
        .map(|value| normalize_actor_id(&value))
        .transpose()?
        .unwrap_or_else(|| "repo-owner".to_string());
    validate_actor_id(&owner)?;
    let force = has_flag(args, "--force");
    if !has_flag(args, "--yes") {
        println!("Rho repo init");
        println!("  root: {}", root.display());
        println!("  repo id: {repo_id}");
        println!("  owner: {owner}");
        print!("Use these values? [Y/n] ");
        io::stdout().flush()?;
        let mut answer = String::new();
        io::stdin().read_line(&mut answer)?;
        let answer = answer.trim().to_ascii_lowercase();
        if answer == "n" || answer == "no" {
            return Err("rho repo init cancelled".into());
        }
    }

    fs::create_dir_all(root.join("rho/participants"))?;
    fs::create_dir_all(root.join("rho/policy"))?;
    fs::create_dir_all(root.join("rho/tools"))?;
    fs::create_dir_all(root.join("rho/requests"))?;
    fs::create_dir_all(root.join("rho/control/outbox"))?;
    fs::create_dir_all(root.join("rho/messages/inbox"))?;
    fs::create_dir_all(root.join("workspace"))?;

    write_if_missing_or_forced(
        &root.join("rho.yaml"),
        &format!(
            "rho: 1\nproject:\n  id: {}\n  owner: {}\ntransport:\n  kind: git\nmembers:\n  file: rho/membership.yaml\nparticipants:\n  path: rho/participants/\npermissions:\n  file: rho/policy/permissions.yaml\ncrypto:\n  file: rho/policy/crypto.yaml\nauth:\n  file: rho/policy/auth.yaml\ntools:\n  path: rho/tools/\nresources:\n  file: rho/resources.yaml\n",
            yaml_quote(&repo_id),
            yaml_quote(&owner),
        ),
        force,
    )?;
    write_if_missing_or_forced(
        &root.join("rho/repo.yaml"),
        &format!(
            "version: 1\nrepo:\n  id: {}\n  owner: {}\n  created_at: {}\n",
            yaml_quote(&repo_id),
            yaml_quote(&owner),
            yaml_quote(&rho_core::now_rfc3339()),
        ),
        force,
    )?;
    write_if_missing_or_forced(
        &root.join("rho/membership.yaml"),
        &format!(
            "version: 1\nmembers:\n  - identity: {}\n    role: owner\n",
            yaml_quote(&owner),
        ),
        force,
    )?;
    write_if_missing_or_forced(
        &root.join("rho/policy/paths.yaml"),
        "version: 1\npaths:\n  repo_root: rho://repo/\n  rho_root: rho://repo/rho/\n  workspace: rho://repo/workspace/\n  requests: rho://repo/rho/requests/\n  control_outbox: rho://repo/rho/control/outbox/\n",
        force,
    )?;
    write_if_missing_or_forced(
        &root.join("rho/policy/permissions.yaml"),
        "version: 1\npermissions: []\n",
        force,
    )?;
    write_if_missing_or_forced(
        &root.join("rho/policy/crypto.yaml"),
        "version: 1\ncrypto:\n  signatures: ssh\n  encryption: x25519-recipient-envelope\n  transparent_git_filters: disabled\n",
        force,
    )?;
    write_if_missing_or_forced(
        &root.join("rho/policy/auth.yaml"),
        "version: 1\nauth:\n  identity_proof: github-profile-fragment-claim\n  trust: local\n",
        force,
    )?;
    write_if_missing_or_forced(
        &root.join("rho/policy/revocations.yaml"),
        "version: 1\nrevocations: []\n",
        force,
    )?;
    write_if_missing_or_forced(
        &root.join("rho/resources.yaml"),
        "version: 1\nresources: []\n",
        force,
    )?;
    write_if_missing_or_forced(
        &root.join(".gitignore"),
        "sandbox/\nrho/private/\nrho/local/\n",
        false,
    )?;
    write_if_missing_or_forced(
        &root.join(".gitattributes"),
        "*.rhoenc binary\n*.rhosig.yaml text\nrho/participants/*.yaml text\nrho/requests/*.yaml text\n",
        false,
    )?;
    write_tool(&root, "run_mock", "run_mock_data", &owner, true, force)?;
    write_tool(&root, "run_real", "run_real_data", &owner, true, force)?;
    write_tool(
        &root,
        "publish_results",
        "release_results",
        &owner,
        true,
        force,
    )?;

    println!("{}", root.join("rho.yaml").display());
    println!("{}", root.join("rho/repo.yaml").display());
    println!("{}", root.join("rho/membership.yaml").display());
    println!("{}", root.join("rho/policy/paths.yaml").display());
    println!("{}", root.join("rho/policy/permissions.yaml").display());
    println!("{}", root.join("rho/policy/crypto.yaml").display());
    println!("{}", root.join("rho/policy/auth.yaml").display());
    println!("{}", root.join("rho/policy/revocations.yaml").display());
    println!("{}", root.join("rho/resources.yaml").display());
    println!("{}", root.join("rho/tools/run_mock.yaml").display());
    println!("{}", root.join("rho/tools/run_real.yaml").display());
    println!("{}", root.join("rho/tools/publish_results.yaml").display());
    Ok(())
}

fn init_existing_repo(args: &[String], root: &Path) -> RhoResult<()> {
    ensure_repo(root)?;
    println!("Rho repo already initialized");
    println!("  root: {}", root.display());
    println!("  repo: {}", root.join("rho/repo.yaml").display());
    if !has_flag(args, "--yes") {
        print!("Install filters and import participants? [Y/n] ");
        io::stdout().flush()?;
        let mut answer = String::new();
        io::stdin().read_line(&mut answer)?;
        let answer = answer.trim().to_ascii_lowercase();
        if answer == "n" || answer == "no" {
            return Err("rho repo init maintenance cancelled".into());
        }
    }
    let root_arg = root.display().to_string();
    install_filters(&["--root".to_string(), root_arg.clone()])?;
    import_users(&["--root".to_string(), root_arg])?;
    Ok(())
}

fn add_user(args: &[String]) -> RhoResult<()> {
    let Some(identity_arg) = args.first() else {
        usage();
    };
    let identity_id = normalize_actor_id(identity_arg)?;
    let handle = github_handle_from_identity_id(&identity_id)?;
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    ensure_repo(&root)?;
    let rho_home = rho_home();
    let local_path = rho_home
        .join("identities")
        .join("github")
        .join(format!("{handle}.yaml"));
    let peer_path = rho_home
        .join("peers")
        .join("github")
        .join(format!("{handle}.yaml"));
    let bundle = if local_path.is_file() {
        let local: LocalIdentityManifest = from_yaml(&fs::read_to_string(&local_path)?)?;
        IdentityBundleManifest {
            version: 1,
            identity: local.local_identity.identity,
        }
    } else if peer_path.is_file() {
        from_yaml::<IdentityBundleManifest>(&fs::read_to_string(&peer_path)?)?
    } else {
        return Err(format!(
            "identity not found in local RHO_HOME: {identity_id}; run `rho id init --github {handle}` or `rho id import <file>` first"
        )
        .into());
    };
    if bundle.identity.id != identity_id {
        return Err(format!(
            "identity mismatch: requested {identity_id}, found {}",
            bundle.identity.id
        )
        .into());
    }
    let out = root
        .join("rho")
        .join("participants")
        .join(format!("{handle}.yaml"));
    ensure_parent(&out)?;
    fs::write(&out, to_yaml(&bundle)?)?;
    println!("{}", out.display());
    add_member_if_missing(&root, &identity_id)?;
    Ok(())
}

fn join(args: &[String]) -> RhoResult<()> {
    let Some(identity_arg) = args.first() else {
        usage();
    };
    if is_github_repo_join_arg(identity_arg) {
        return join_github_repo(args);
    }
    let identity_id = normalize_actor_id(identity_arg)?;
    let handle = github_handle_from_identity_id(&identity_id)?;
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    ensure_repo(&root)?;
    require_local_or_peer_identity(&identity_id)?;

    if !has_flag(args, "--no-branch") {
        switch_branch(&root, &format!("{handle}/join-rho"))?;
    }
    add_user(&[
        identity_id.clone(),
        "--root".to_string(),
        root.display().to_string(),
    ])?;
    println!("join branch: {handle}/join-rho");
    println!("stage: rho/participants/{handle}.yaml rho/membership.yaml");
    println!("next: rho commit -m \"Request Rho access for {handle}\"");
    print_create_pr_next_step(
        &root,
        &format!("Request Rho access for {handle}"),
        &format!("Add {identity_id} as a pending Rho participant."),
    );
    finish_branch_work(
        args,
        &root,
        &["rho/participants", "rho/membership.yaml"],
        &format!("Request Rho access for {handle}"),
        &format!("Request Rho access for {handle}"),
        &format!("Add {identity_id} as a pending Rho participant."),
    )?;
    Ok(())
}

fn join_github_repo(args: &[String]) -> RhoResult<()> {
    let repo_arg = args.first().ok_or("missing GitHub repository")?;
    let identity_id = normalize_actor_id(
        &active_identity().ok_or("rho repo join <owner/repo> requires --profile or --identity")?,
    )?;
    let handle = github_handle_from_identity_id(&identity_id)?;
    let repo_slug = github_repo_slug(repo_arg, &handle)?;
    let repo_name = repo_slug
        .split_once('/')
        .map(|(_, repo)| repo)
        .ok_or("invalid GitHub repo slug")?;
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| default_project_root(&handle, repo_name));
    ensure_join_clone(&root, &repo_slug, &handle, repo_name)?;
    init(&[
        "--root".to_string(),
        root.display().to_string(),
        "--yes".to_string(),
    ])?;
    join(&[
        identity_id,
        "--root".to_string(),
        root.display().to_string(),
    ])?;
    finish_branch_work(
        args,
        &root,
        &["rho/participants", "rho/membership.yaml"],
        &format!("Request Rho access for {handle}"),
        &format!("Request Rho access for {handle}"),
        &format!("Add github/{handle} as a Rho participant."),
    )?;
    Ok(())
}

fn ensure_join_clone(root: &Path, repo_slug: &str, handle: &str, repo_name: &str) -> RhoResult<()> {
    let fork_slug = format!("{handle}/{repo_name}");
    if root.join(".git").is_dir() {
        configure_profile_ssh(root, handle)?;
        ensure_remote(root, "origin", &github_ssh_url(&fork_slug))?;
        ensure_remote(root, "upstream", &github_ssh_url(repo_slug))?;
        return Ok(());
    }
    let fork_status = Command::new("gh")
        .args(["repo", "fork", repo_slug, "--clone=false"])
        .status()?;
    if !fork_status.success() {
        let view_status = Command::new("gh")
            .args(["repo", "view", &fork_slug])
            .status()?;
        if !view_status.success() {
            return Err(format!("gh repo fork failed for {repo_slug}").into());
        }
    }
    if let Some(parent) = root.parent()
        && !parent.as_os_str().is_empty()
    {
        fs::create_dir_all(parent)?;
    }
    let clone_status = Command::new("gh")
        .args(["repo", "clone", &fork_slug])
        .arg(root)
        .status()?;
    if !clone_status.success() {
        return Err(format!("gh repo clone failed for {fork_slug}").into());
    }
    configure_profile_ssh(root, handle)?;
    ensure_remote(root, "origin", &github_ssh_url(&fork_slug))?;
    ensure_remote(root, "upstream", &github_ssh_url(repo_slug))?;
    Ok(())
}

fn gh_repo_clone(repo_slug: &str, root: &Path) -> RhoResult<()> {
    if let Some(parent) = root.parent()
        && !parent.as_os_str().is_empty()
    {
        fs::create_dir_all(parent)?;
    }
    let status = Command::new("gh")
        .args(["repo", "clone", repo_slug])
        .arg(root)
        .status()?;
    if !status.success() {
        return Err(format!("gh repo clone failed for {repo_slug}").into());
    }
    ensure_remote(root, "origin", &github_ssh_url(repo_slug))?;
    Ok(())
}

fn ensure_remote(root: &Path, name: &str, url: &str) -> RhoResult<()> {
    if git_remote_exists(root, name) {
        let status = Command::new("git")
            .arg("-C")
            .arg(root)
            .args(["remote", "set-url", name, url])
            .status()?;
        if !status.success() {
            return Err(format!("git remote set-url failed for {name}").into());
        }
    } else {
        let status = Command::new("git")
            .arg("-C")
            .arg(root)
            .args(["remote", "add", name, url])
            .status()?;
        if !status.success() {
            return Err(format!("git remote add failed for {name}").into());
        }
    }
    Ok(())
}

fn git_remote_exists(root: &Path, name: &str) -> bool {
    Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["remote", "get-url", name])
        .output()
        .is_ok_and(|output| output.status.success())
}

fn configure_profile_ssh(root: &Path, handle: &str) -> RhoResult<()> {
    if default_ssh_authenticates_as(handle)? {
        return Ok(());
    }
    let key = github_ssh_key_for_handle(handle)?.ok_or_else(|| {
        format!(
            "could not find an SSH key that authenticates to GitHub as {handle}; set {}=/path/to/key or configure GitHub SSH access",
            github_ssh_key_env_name(handle)
        )
    })?;
    let command = format!(
        "ssh -i {} -o IdentitiesOnly=yes",
        shell_quote(&key.display().to_string())
    );
    git_config(root, "core.sshCommand", &command)?;
    println!("git ssh identity: {handle} via {}", key.display());
    Ok(())
}

fn default_ssh_authenticates_as(handle: &str) -> RhoResult<bool> {
    let output = Command::new("ssh")
        .args(["-o", "BatchMode=yes", "-T", "git@github.com"])
        .output()?;
    Ok(ssh_auth_output_matches(
        handle,
        &output.stdout,
        &output.stderr,
    ))
}

fn github_ssh_key_for_handle(handle: &str) -> RhoResult<Option<PathBuf>> {
    let env_name = github_ssh_key_env_name(handle);
    if let Ok(value) = env::var(&env_name)
        && !value.trim().is_empty()
    {
        let path = PathBuf::from(value);
        if ssh_key_authenticates_as(&path, handle)? {
            return Ok(Some(path));
        }
        return Err(format!("{} does not authenticate to GitHub as {handle}", env_name).into());
    }
    for path in ssh_key_candidates()? {
        if ssh_key_authenticates_as(&path, handle)? {
            return Ok(Some(path));
        }
    }
    Ok(None)
}

fn ssh_key_candidates() -> RhoResult<Vec<PathBuf>> {
    let ssh_dir = env::var("HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|_| PathBuf::from("."))
        .join(".ssh");
    if !ssh_dir.is_dir() {
        return Ok(Vec::new());
    }
    let mut candidates = Vec::new();
    for entry in fs::read_dir(ssh_dir)? {
        let path = entry?.path();
        if !path.is_file() {
            continue;
        }
        let Some(name) = path.file_name().and_then(|value| value.to_str()) else {
            continue;
        };
        if name.ends_with(".pub")
            || name.starts_with("known_hosts")
            || matches!(name, "config" | "authorized_keys")
        {
            continue;
        }
        if path
            .with_extension(format!(
                "{}pub",
                path.extension()
                    .and_then(|value| value.to_str())
                    .map(|ext| format!("{ext}."))
                    .unwrap_or_default()
            ))
            .is_file()
            || PathBuf::from(format!("{}.pub", path.display())).is_file()
            || name.starts_with("id_")
        {
            candidates.push(path);
        }
    }
    candidates.sort();
    Ok(candidates)
}

fn ssh_key_authenticates_as(path: &Path, handle: &str) -> RhoResult<bool> {
    let output = Command::new("ssh")
        .arg("-i")
        .arg(path)
        .args([
            "-o",
            "IdentitiesOnly=yes",
            "-o",
            "BatchMode=yes",
            "-T",
            "git@github.com",
        ])
        .output()?;
    Ok(ssh_auth_output_matches(
        handle,
        &output.stdout,
        &output.stderr,
    ))
}

fn ssh_auth_output_matches(handle: &str, stdout: &[u8], stderr: &[u8]) -> bool {
    let mut text = String::from_utf8_lossy(stdout).to_string();
    text.push_str(&String::from_utf8_lossy(stderr));
    text.contains(&format!("Hi {handle}!"))
}

fn github_ssh_key_env_name(handle: &str) -> String {
    let suffix = handle
        .chars()
        .map(|ch| {
            if ch.is_ascii_alphanumeric() {
                ch.to_ascii_uppercase()
            } else {
                '_'
            }
        })
        .collect::<String>();
    format!("RHO_GITHUB_SSH_KEY_{suffix}")
}

fn git_fetch(root: &Path, remote: &str, branch: &str) -> RhoResult<()> {
    let status = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["fetch", remote, branch])
        .status()?;
    if !status.success() {
        return Err(format!("git fetch failed for {remote} {branch}").into());
    }
    Ok(())
}

fn git_merge_ff(root: &Path, ref_name: &str) -> RhoResult<()> {
    let status = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["merge", "--ff-only", ref_name])
        .status()?;
    if !status.success() {
        return Err(format!("git merge --ff-only failed for {ref_name}").into());
    }
    Ok(())
}

fn detect_remote_repo(root: &Path, remote_name: &str) -> Option<String> {
    let output = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["remote", "get-url", remote_name])
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }
    let remote = String::from_utf8_lossy(&output.stdout).trim().to_string();
    providers::github::repo_candidate_from_remote(&remote)
}

fn repo_name_from_slug(value: &str) -> Option<&str> {
    value.split_once('/').map(|(_, repo)| repo)
}

fn github_ssh_url(repo_slug: &str) -> String {
    format!("git@github.com:{repo_slug}.git")
}

fn default_project_root(profile_handle: &str, repo_name: &str) -> PathBuf {
    env::var("RHO_PROJECTS_ROOT")
        .map(PathBuf::from)
        .unwrap_or_else(|_| {
            env::var("HOME")
                .map(PathBuf::from)
                .unwrap_or_else(|_| PathBuf::from("."))
                .join("rho")
        })
        .join(profile_handle)
        .join("projects")
        .join(repo_name)
}

fn is_github_repo_join_arg(value: &str) -> bool {
    value.contains('/') && !value.starts_with("github/") && !value.starts_with("rho://")
}

fn admit(args: &[String]) -> RhoResult<()> {
    let Some(identity_arg) = args.first() else {
        usage();
    };
    let identity_id = normalize_actor_id(identity_arg)?;
    let handle = github_handle_from_identity_id(&identity_id)?;
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    let rho_bin = arg_value(args, "--rho-bin").unwrap_or_else(default_rho_bin);
    ensure_repo(&root)?;
    require_local_or_peer_identity(&identity_id)?;

    let branched = !has_flag(args, "--no-branch");
    if branched {
        switch_branch(&root, &format!("review/{handle}/admit-rho"))?;
    }
    add_user(&[
        identity_id.clone(),
        "--root".to_string(),
        root.display().to_string(),
    ])?;
    let inbox_pattern = format!(
        "rho/messages/inbox/{}/**",
        identity_inbox_relative_path(&identity_id)?.display()
    );
    protect_path(&[
        inbox_pattern.clone(),
        "--root".to_string(),
        root.display().to_string(),
        "--rho-bin".to_string(),
        rho_bin,
        "--recipient".to_string(),
        identity_id.clone(),
    ])?;
    sign_governance(&[
        "--root".to_string(),
        root.display().to_string(),
        "--no-stage".to_string(),
    ])?;
    doctor(&["--root".to_string(), root.display().to_string()])?;
    if branched {
        println!("admit branch: review/{handle}/admit-rho");
    } else {
        println!("admit branch: {}", current_branch(&root)?);
    }
    println!("protected inbox: {inbox_pattern}");
    println!("stage: .gitattributes rho");
    if !has_flag(args, "--no-next") {
        println!("next: rho commit -m \"Admit {handle} to Rho project\"");
        print_create_pr_next_step(
            &root,
            &format!("Admit {handle} to Rho project"),
            &format!("Admit {identity_id} and add the encrypted inbox policy."),
        );
    }
    finish_branch_work(
        args,
        &root,
        &[".gitattributes", "rho"],
        &format!("Admit {handle} to Rho project"),
        &format!("Admit {handle} to Rho project"),
        &format!("Admit {identity_id} and add the encrypted inbox policy."),
    )?;
    Ok(())
}

fn admit_pr(args: &[String]) -> RhoResult<()> {
    let Some(pr_number) = args.first() else {
        usage();
    };
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    ensure_repo(&root)?;
    let pr = gh_pr_metadata(&root, pr_number)?;
    gh_pr_checkout(&root, pr_number)?;
    import_users(&["--root".to_string(), root.display().to_string()])?;
    let author = pr.author.login.clone();
    let identity = normalize_actor_id(&author)?;
    verify_github_identity_with_rho(&identity)?;
    let mut admit_args = vec![
        identity.clone(),
        "--root".to_string(),
        root.display().to_string(),
    ];
    if let Some(rho_bin) = arg_value(args, "--rho-bin") {
        admit_args.push("--rho-bin".to_string());
        admit_args.push(rho_bin);
    }
    let new_pr = has_flag(args, "--new-pr");
    if !new_pr || has_flag(args, "--no-branch") {
        admit_args.push("--no-branch".to_string());
    }
    admit_args.push("--no-next".to_string());
    admit(&admit_args)?;
    if new_pr {
        finish_branch_work(
            args,
            &root,
            &[".gitattributes", "rho"],
            &format!("Admit {author} to Rho project"),
            &format!("Admit {author} to Rho project"),
            &format!("Admit {identity} and add the encrypted inbox policy."),
        )?;
    } else {
        finish_pr_branch_work(
            args,
            &root,
            &pr,
            &[".gitattributes", "rho"],
            &format!("Admit {author} to Rho project"),
        )?;
    }
    Ok(())
}

fn verify_github_identity_with_rho(identity: &str) -> RhoResult<()> {
    let exe = env::current_exe()?;
    let status = Command::new(exe)
        .args(["id", "verify-github", "--identity", identity])
        .status()?;
    if !status.success() {
        return Err(format!("github identity verification failed for {identity}").into());
    }
    Ok(())
}

fn create_pr(args: &[String]) -> RhoResult<()> {
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    let title = arg_value(args, "--title").ok_or("missing required argument: --title")?;
    let body = arg_value(args, "--body").unwrap_or_default();
    let open_browser = has_flag(args, "--web") || has_flag(args, "--open");
    ensure_repo(&root)?;
    let output = providers::github::create_pull_request(&root, &title, &body, open_browser)?;
    if output.is_empty() {
        println!("pull request: opened in browser");
    } else {
        println!("pull request: {output}");
    }
    Ok(())
}

fn merge_pr(args: &[String]) -> RhoResult<()> {
    let Some(pr_number) = args.first() else {
        usage();
    };
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    ensure_repo(&root)?;
    let strategy = pr_merge_strategy(args)?;
    let mut command = Command::new("gh");
    command
        .current_dir(&root)
        .args(["pr", "merge", pr_number, strategy]);
    if has_flag(args, "--delete-branch") {
        command.arg("--delete-branch");
    }
    let status = command.status()?;
    if !status.success() {
        return Err(format!("gh pr merge failed for {pr_number}").into());
    }
    println!("merged pull request: {pr_number}");
    Ok(())
}

fn pr_merge_strategy(args: &[String]) -> RhoResult<&'static str> {
    let selected = [
        (has_flag(args, "--merge"), "--merge"),
        (has_flag(args, "--squash"), "--squash"),
        (has_flag(args, "--rebase"), "--rebase"),
    ]
    .iter()
    .filter_map(|(present, value)| present.then_some(*value))
    .collect::<Vec<_>>();
    match selected.as_slice() {
        [value] => Ok(value),
        [] => Ok("--merge"),
        _ => Err("choose only one of --merge, --squash, or --rebase".into()),
    }
}

fn print_create_pr_next_step(root: &Path, title: &str, body: &str) {
    let root_arg = root.display().to_string();
    println!(
        "next: rho repo create-pr --root {} --title {} --body {}",
        shell_quote(&root_arg),
        shell_quote(title),
        shell_quote(body)
    );
}

fn finish_branch_work(
    args: &[String],
    root: &Path,
    stage_paths: &[&str],
    commit_message: &str,
    pr_title: &str,
    pr_body: &str,
) -> RhoResult<()> {
    let want_pr = has_flag(args, "--pr");
    let want_commit = want_pr || has_flag(args, "--commit");
    let want_push = want_pr || has_flag(args, "--push");
    if !want_commit && !want_push && !want_pr {
        return Ok(());
    }
    if want_commit {
        for path in stage_paths {
            git_add_relative(root, path)?;
        }
        if git_has_staged_changes(root)? {
            git_commit(root, commit_message)?;
            println!("committed: {commit_message}");
        } else {
            println!("commit: no staged changes");
        }
    }
    if want_push {
        git_push_current_branch(root)?;
        println!("pushed: {}", current_branch(root)?);
    }
    if want_pr {
        create_pr(&[
            "--root".to_string(),
            root.display().to_string(),
            "--title".to_string(),
            pr_title.to_string(),
            "--body".to_string(),
            pr_body.to_string(),
        ])?;
    }
    Ok(())
}

fn finish_pr_branch_work(
    args: &[String],
    root: &Path,
    pr: &GhPrMetadata,
    stage_paths: &[&str],
    commit_message: &str,
) -> RhoResult<()> {
    let want_pr = has_flag(args, "--pr");
    let want_commit = want_pr || has_flag(args, "--commit");
    let want_push = want_pr || has_flag(args, "--push");
    if !want_commit && !want_push && !want_pr {
        return Ok(());
    }
    if want_commit {
        for path in stage_paths {
            git_add_relative(root, path)?;
        }
        if git_has_staged_changes(root)? {
            git_commit(root, commit_message)?;
            println!("committed: {commit_message}");
        } else {
            println!("commit: no staged changes");
        }
    }
    if want_push {
        let target = github_ssh_url(&format!(
            "{}/{}",
            pr.head_repository_owner.login, pr.head_repository.name
        ));
        git_push_head_to_url(root, &target, &pr.head_ref_name)?;
        println!(
            "pushed: {}:{}",
            pr.head_repository_owner.login, pr.head_ref_name
        );
    }
    if want_pr {
        println!("pull request: {}", pr.url);
    }
    Ok(())
}

fn require_local_or_peer_identity(identity_id: &str) -> RhoResult<()> {
    let handle = github_handle_from_identity_id(identity_id)?;
    let home = rho_home();
    let local_path = home
        .join("identities")
        .join("github")
        .join(format!("{handle}.yaml"));
    let peer_path = home
        .join("peers")
        .join("github")
        .join(format!("{handle}.yaml"));
    if local_path.is_file() || peer_path.is_file() {
        return Ok(());
    }
    Err(format!(
        "identity not found in local RHO_HOME: {identity_id}; run `rho id init --github {handle}` or `rho id import <file>` first"
    )
    .into())
}

fn switch_branch(root: &Path, branch: &str) -> RhoResult<()> {
    let status = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["switch", "-C", branch])
        .status()?;
    if !status.success() {
        return Err(format!("git switch failed for branch {branch}").into());
    }
    Ok(())
}

fn git_init_main(root: &Path) -> RhoResult<()> {
    let status = Command::new("git")
        .arg("-C")
        .arg(root)
        .arg("init")
        .status()?;
    if !status.success() {
        return Err(format!("git init failed in {}", root.display()).into());
    }
    let status = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["switch", "-C", "main"])
        .status()?;
    if !status.success() {
        return Err(format!("git switch main failed in {}", root.display()).into());
    }
    Ok(())
}

fn git_commit(root: &Path, message: &str) -> RhoResult<()> {
    let exe = env::current_exe()?;
    let status = Command::new(exe)
        .current_dir(root)
        .arg("commit")
        .arg("-m")
        .arg(message)
        .status()?;
    if !status.success() {
        return Err(format!("rho commit failed in {}", root.display()).into());
    }
    Ok(())
}

fn git_has_staged_changes(root: &Path) -> RhoResult<bool> {
    let status = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["diff", "--cached", "--quiet"])
        .status()?;
    match status.code() {
        Some(0) => Ok(false),
        Some(1) => Ok(true),
        _ => Err(format!("git diff --cached --quiet failed in {}", root.display()).into()),
    }
}

fn git_push_current_branch(root: &Path) -> RhoResult<()> {
    let branch = current_branch(root)?;
    git_push(root, "origin", &branch)
}

fn git_push(root: &Path, remote: &str, branch: &str) -> RhoResult<()> {
    let status = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["push", "-u", remote, branch])
        .status()?;
    if !status.success() {
        return Err(format!("git push failed for {remote} {branch}").into());
    }
    Ok(())
}

fn git_push_head_to_url(root: &Path, url: &str, branch: &str) -> RhoResult<()> {
    let refspec = format!("HEAD:{branch}");
    let status = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["push", url, &refspec])
        .status()?;
    if !status.success() {
        return Err(format!("git push failed for {url} {refspec}").into());
    }
    Ok(())
}

fn current_branch(root: &Path) -> RhoResult<String> {
    let output = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["branch", "--show-current"])
        .output()?;
    if !output.status.success() {
        return Err(format!("git branch --show-current failed in {}", root.display()).into());
    }
    let branch = String::from_utf8(output.stdout)?.trim().to_string();
    if branch.is_empty() {
        return Err("current Git branch is empty".into());
    }
    Ok(branch)
}

fn gh_repo_create(root: &Path, repo_slug: &str, visibility: &str) -> RhoResult<()> {
    let status = Command::new("gh")
        .args(["repo", "create", repo_slug, visibility, "--source"])
        .arg(root)
        .args(["--remote", "origin"])
        .status()?;
    if !status.success() {
        return Err(format!("gh repo create failed for {repo_slug}").into());
    }
    ensure_remote(root, "origin", &github_ssh_url(repo_slug))?;
    Ok(())
}

fn gh_pr_checkout(root: &Path, pr_number: &str) -> RhoResult<()> {
    let status = Command::new("gh")
        .current_dir(root)
        .args(["pr", "checkout", pr_number])
        .status()?;
    if !status.success() {
        return Err(format!("gh pr checkout failed for {pr_number}").into());
    }
    Ok(())
}

fn gh_pr_metadata(root: &Path, pr_number: &str) -> RhoResult<GhPrMetadata> {
    let output = Command::new("gh")
        .current_dir(root)
        .args([
            "pr",
            "view",
            pr_number,
            "--json",
            "author,headRefName,headRepository,headRepositoryOwner,url",
        ])
        .output()?;
    if !output.status.success() {
        return Err(format!("gh pr view failed for {pr_number}").into());
    }
    from_json(&String::from_utf8(output.stdout)?)
}

fn repo_visibility_arg(args: &[String]) -> RhoResult<&'static str> {
    let values = [
        (has_flag(args, "--public"), "--public"),
        (has_flag(args, "--private"), "--private"),
        (has_flag(args, "--internal"), "--internal"),
    ];
    let selected = values
        .iter()
        .filter_map(|(present, value)| present.then_some(*value))
        .collect::<Vec<_>>();
    match selected.as_slice() {
        [value] => Ok(value),
        [] => {
            if has_flag(args, "--yes") {
                Ok("--public")
            } else {
                Err("choose repository visibility: --public, --private, or --internal".into())
            }
        }
        _ => Err("choose only one of --public, --private, or --internal".into()),
    }
}

fn github_repo_slug(value: &str, default_owner: &str) -> RhoResult<String> {
    let slug = if value.contains('/') {
        value.to_string()
    } else {
        format!("{default_owner}/{value}")
    };
    let Some((owner, repo)) = slug.split_once('/') else {
        return Err(format!("invalid GitHub repository: {value}").into());
    };
    validate_github_repo_part(owner, "owner")?;
    validate_github_repo_part(repo, "repo")?;
    Ok(slug)
}

fn validate_github_repo_part(value: &str, label: &str) -> RhoResult<()> {
    if value.is_empty()
        || !value
            .chars()
            .all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' || ch == '.')
    {
        return Err(format!("invalid GitHub {label}: {value}").into());
    }
    Ok(())
}

fn add_member_if_missing(root: &Path, identity_id: &str) -> RhoResult<()> {
    let membership_path = root.join("rho/membership.yaml");
    let text = fs::read_to_string(&membership_path)?;
    if let Ok(membership) = from_yaml::<MembershipManifest>(&text)
        && membership
            .members
            .iter()
            .any(|member| member.identity == identity_id)
    {
        return Ok(());
    }

    let repo_path = root.join("rho/repo.yaml");
    let repo: RepoManifest = from_yaml(&fs::read_to_string(&repo_path)?)?;
    let role = if repo.repo.owner.as_deref() == Some(identity_id) {
        "owner"
    } else {
        "collaborator"
    };
    let mut updated = text;
    if !updated.ends_with('\n') {
        updated.push('\n');
    }
    updated.push_str(&format!(
        "  - identity: {}\n    role: {role}\n",
        yaml_quote(identity_id)
    ));
    fs::write(&membership_path, updated)?;
    println!("{}", membership_path.display());
    Ok(())
}

#[derive(Debug, Deserialize)]
struct RepoManifest {
    repo: RepoRecord,
}

#[derive(Debug, Deserialize)]
struct RepoRecord {
    id: String,
    owner: Option<String>,
}

#[derive(Debug, Deserialize)]
struct RhoRootManifest {
    project: Option<RhoRootProject>,
    members: Option<RhoFileRef>,
    participants: Option<RhoPathRef>,
    permissions: Option<RhoFileRef>,
    crypto: Option<RhoFileRef>,
    auth: Option<RhoFileRef>,
    tools: Option<RhoPathRef>,
    resources: Option<RhoFileRef>,
}

#[derive(Debug, Deserialize)]
struct RhoRootProject {
    id: Option<String>,
    owner: Option<String>,
}

#[derive(Debug, Deserialize)]
struct RhoFileRef {
    file: Option<String>,
}

#[derive(Debug, Deserialize)]
struct RhoPathRef {
    path: Option<String>,
}

#[derive(Debug, Deserialize)]
struct MembershipManifest {
    #[serde(default)]
    members: Vec<MemberRecord>,
}

#[derive(Debug, Deserialize)]
struct MemberRecord {
    identity: String,
    role: String,
}

#[derive(Debug, Deserialize)]
struct GhPrMetadata {
    author: GhPrAuthor,
    #[serde(rename = "headRefName")]
    head_ref_name: String,
    #[serde(rename = "headRepository")]
    head_repository: GhPrRepository,
    #[serde(rename = "headRepositoryOwner")]
    head_repository_owner: GhPrOwner,
    url: String,
}

#[derive(Debug, Deserialize)]
struct GhPrAuthor {
    login: String,
}

#[derive(Debug, Deserialize)]
struct GhPrRepository {
    name: String,
}

#[derive(Debug, Deserialize)]
struct GhPrOwner {
    login: String,
}

#[derive(Debug, Deserialize)]
struct CryptoPolicyManifest {
    crypto: CryptoPolicy,
}

#[derive(Debug, Deserialize)]
struct CryptoPolicy {
    signatures: Option<String>,
    encryption: Option<String>,
    transparent_git_filters: Option<String>,
    filter: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
struct PermissionsManifest {
    #[serde(default)]
    permissions: Vec<PermissionRule>,
}

#[derive(Debug, Clone, Deserialize)]
struct PermissionRule {
    #[serde(default)]
    id: String,
    pattern: String,
    #[serde(default)]
    read: ReadPermission,
    #[serde(default)]
    write: WritePermission,
}

#[derive(Debug, Clone, Default, Deserialize)]
struct ReadPermission {
    #[serde(default)]
    public: bool,
    #[serde(default)]
    repo_key: bool,
    #[serde(default)]
    encrypted_to: Vec<String>,
}

#[derive(Debug, Clone, Default, Deserialize)]
struct WritePermission {
    #[serde(default)]
    allowed: Vec<String>,
}

#[derive(Debug, Deserialize)]
struct DatasetManifest {
    version: Option<u32>,
    dataset: Option<DatasetRecord>,
    uuid: Option<String>,
    variants: Option<serde_yaml::Value>,
}

#[derive(Debug, Deserialize)]
struct DatasetRecord {
    uuid: Option<String>,
    name: Option<String>,
    owner: Option<String>,
    variants: Option<DatasetVariants>,
}

#[derive(Debug, Deserialize)]
struct DatasetVariants {
    public: Option<DatasetVariant>,
    mock: Option<DatasetVariant>,
    real: Option<DatasetVariant>,
}

#[derive(Debug, Deserialize)]
struct DatasetVariant {
    relative_path: Option<String>,
    share_relative_path: Option<String>,
    sha256: Option<String>,
    source: Option<DatasetSource>,
}

#[derive(Debug, Deserialize)]
struct DatasetSource {
    kind: Option<String>,
    repo: Option<String>,
    url: Option<String>,
    revision: Option<String>,
}

#[derive(Debug, Deserialize)]
struct RunReceiptManifest {
    run_receipt: RunReceiptRecord,
}

#[derive(Debug, Deserialize)]
struct RunReceiptRecord {
    request_id: String,
    run_id: String,
    runner: String,
    status: String,
    tier: String,
    code_path: String,
    code_sha256: String,
    #[serde(default)]
    input_path: Option<String>,
    input_sha256: String,
    output_sha256: String,
    result_path: String,
}

#[derive(Debug, Deserialize)]
struct ResultManifest {
    result: ResultRecord,
}

#[derive(Debug, Deserialize)]
struct ResultRecord {
    request_id: String,
    to: String,
    run_id: String,
}

#[derive(Debug, Deserialize)]
struct ApprovalGrantManifest {
    approval_grant: ApprovalGrantRecord,
}

#[derive(Debug, Deserialize)]
struct ApprovalGrantRecord {
    action_id: String,
    request_id: String,
    tool_id: String,
    action_type: String,
    decision: String,
    granted_by: String,
    action: ApprovalGrantFile,
    local_grant: ApprovalGrantFile,
    code: ApprovalGrantInput,
    private_input: ApprovalGrantPrivateInput,
}

#[derive(Debug, Deserialize)]
struct ApprovalGrantFile {
    path: String,
    sha256: String,
}

#[derive(Debug, Deserialize)]
struct ApprovalGrantInput {
    kind: String,
    path: String,
    sha256: String,
}

#[derive(Debug, Deserialize)]
struct ApprovalGrantPrivateInput {
    dataset_uuid: String,
    tier: String,
    sha256: String,
}

fn doctor(args: &[String]) -> RhoResult<()> {
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    ensure_repo(&root)?;
    if has_flag(args, "--governance") && has_flag(args, "--no-governance") {
        return Err("--governance and --no-governance cannot be used together".into());
    }
    let check_governance = !has_flag(args, "--no-governance");
    let mut failures = Vec::new();
    let mut advisories = Vec::new();

    let root_manifest_path = root.join("rho.yaml");
    let root_manifest: Option<RhoRootManifest> =
        read_yaml_check(&root_manifest_path, &mut failures);

    let repo_path = root.join("rho/repo.yaml");
    let repo: Option<RepoManifest> = read_yaml_check(&repo_path, &mut failures);
    if let Some(repo) = &repo {
        if let Err(error) = validate_repo_id(&repo.repo.id) {
            failures.push(format!(
                "invalid repo id in {}: {error}",
                repo_path.display()
            ));
        }
        match repo.repo.owner.as_deref() {
            Some(owner) => {
                if let Err(error) = validate_actor_id(owner) {
                    failures.push(format!(
                        "invalid repo owner in {}: {error}",
                        repo_path.display()
                    ));
                }
            }
            None => failures.push(format!("missing repo.owner in {}", repo_path.display())),
        }
    }
    if let Some(root_manifest) = &root_manifest {
        check_rho_root_manifest(&root, root_manifest, repo.as_ref(), &mut failures);
    }

    require_file(&root.join("rho/policy/auth.yaml"), &mut failures);
    require_file(&root.join("rho/policy/permissions.yaml"), &mut failures);
    require_file(&root.join("rho/policy/crypto.yaml"), &mut failures);
    require_file(&root.join("rho/resources.yaml"), &mut failures);
    require_file(&root.join("rho/policy/revocations.yaml"), &mut failures);
    require_file(&root.join(".gitattributes"), &mut failures);

    let membership_path = root.join("rho/membership.yaml");
    let membership: Option<MembershipManifest> = read_yaml_check(&membership_path, &mut failures);
    let mut member_ids = Vec::new();
    if let Some(membership) = &membership {
        let mut seen_members = HashSet::new();
        let mut owner_count = 0usize;
        if membership.members.is_empty() {
            failures.push(format!(
                "membership has no members: {}",
                membership_path.display()
            ));
        }
        for member in &membership.members {
            if let Err(error) = validate_actor_id(&member.identity) {
                failures.push(format!(
                    "invalid member identity {}: {error}",
                    member.identity
                ));
            }
            if !seen_members.insert(member.identity.clone()) {
                failures.push(format!("duplicate member identity: {}", member.identity));
            }
            if member.role == "owner" {
                owner_count += 1;
            }
            if !matches!(member.role.as_str(), "owner" | "admin" | "collaborator") {
                failures.push(format!(
                    "unsupported role for {}: {}",
                    member.identity, member.role
                ));
            }
            member_ids.push(member.identity.clone());
            check_participant(&root, &member.identity, &mut failures);
        }
        if owner_count == 0 {
            failures.push("membership has no owner role".to_string());
        }
        if let Some(owner) = repo.as_ref().and_then(|repo| repo.repo.owner.as_ref())
            && !membership
                .members
                .iter()
                .any(|member| member.identity == *owner && member.role == "owner")
        {
            failures.push(format!(
                "repo owner is not listed as owner in membership: {owner}"
            ));
        }
        check_orphan_participants(&root, &seen_members, &mut failures)?;
    }
    check_legacy_inbox_paths(&root, &member_ids, &mut advisories)?;

    let crypto_path = root.join("rho/policy/crypto.yaml");
    let crypto: Option<CryptoPolicyManifest> = read_yaml_check(&crypto_path, &mut failures);
    if let Some(crypto) = &crypto {
        check_crypto_policy(&crypto.crypto, &root, &mut failures);
    }

    let permissions_path = root.join("rho/policy/permissions.yaml");
    let permissions: Option<PermissionsManifest> =
        read_yaml_check(&permissions_path, &mut failures);
    if let Some(permissions) = permissions {
        check_permissions_policy(&root, &permissions, &member_ids, &mut failures)?;
    }
    check_dataset_manifests(&root, &member_ids, &mut failures)?;
    let mut flow_report = None;
    if has_flag(args, "--flow") {
        if let Some(repo) = repo.as_ref() {
            if let Some(owner) = repo.repo.owner.as_deref() {
                flow_report = Some(check_flow_receipts(
                    &root,
                    &repo.repo.id,
                    owner,
                    &mut failures,
                )?);
            } else {
                failures.push("flow audit requires repo.owner".to_string());
            }
        } else {
            failures.push("flow audit requires repo manifest".to_string());
        }
    }
    if check_governance {
        if let Some(owner) = repo.as_ref().and_then(|repo| repo.repo.owner.as_deref()) {
            check_governance_signatures(&root, owner, &mut failures)?;
        } else {
            failures.push("governance signatures require repo.owner".to_string());
        }
    }

    if failures.is_empty() {
        println!("rho repo doctor passed");
        println!("{}", root.join("rho/repo.yaml").display());
        println!("{}", root.join("rho/membership.yaml").display());
        println!("{}", root.join("rho/policy/permissions.yaml").display());
        println!("{}", root.join("rho/policy/crypto.yaml").display());
        if has_flag(args, "--flow") {
            println!("flow: passed");
            if let Some(report) = flow_report {
                for next_step in report.next_steps {
                    println!("flow next: {next_step}");
                }
            }
        }
        if check_governance {
            println!("governance: passed");
        }
        for advisory in advisories {
            println!("migration: {advisory}");
        }
        return Ok(());
    }

    eprintln!("rho repo doctor failed");
    for failure in &failures {
        eprintln!("- {failure}");
    }
    Err(format!("{} repo health check(s) failed", failures.len()).into())
}

fn read_yaml_check<T: for<'de> Deserialize<'de>>(
    path: &Path,
    failures: &mut Vec<String>,
) -> Option<T> {
    match fs::read_to_string(path) {
        Ok(text) => match from_yaml(&text) {
            Ok(value) => Some(value),
            Err(error) => {
                failures.push(format!("invalid YAML in {}: {error}", path.display()));
                None
            }
        },
        Err(_) => {
            failures.push(format!("missing required file: {}", path.display()));
            None
        }
    }
}

fn require_file(path: &Path, failures: &mut Vec<String>) {
    if !path.is_file() {
        failures.push(format!("missing required file: {}", path.display()));
    }
}

fn check_rho_root_manifest(
    root: &Path,
    manifest: &RhoRootManifest,
    repo: Option<&RepoManifest>,
    failures: &mut Vec<String>,
) {
    match manifest
        .project
        .as_ref()
        .and_then(|project| project.id.as_deref())
    {
        Some(id) => {
            if let Some(repo) = repo
                && id != repo.repo.id
            {
                failures.push(format!(
                    "rho.yaml project.id mismatch: expected {}, got {id}",
                    repo.repo.id
                ));
            }
        }
        None => failures.push("rho.yaml missing project.id".to_string()),
    }
    match manifest
        .project
        .as_ref()
        .and_then(|project| project.owner.as_deref())
    {
        Some(owner) => {
            if let Some(repo_owner) = repo.and_then(|repo| repo.repo.owner.as_deref())
                && owner != repo_owner
            {
                failures.push(format!(
                    "rho.yaml project.owner mismatch: expected {repo_owner}, got {owner}"
                ));
            }
        }
        None => failures.push("rho.yaml missing project.owner".to_string()),
    }

    check_rho_file_ref(
        root,
        "members.file",
        manifest
            .members
            .as_ref()
            .and_then(|value| value.file.as_deref()),
        "rho/membership.yaml",
        failures,
    );
    check_rho_path_ref(
        root,
        "participants.path",
        manifest
            .participants
            .as_ref()
            .and_then(|value| value.path.as_deref()),
        "rho/participants/",
        failures,
    );
    check_rho_file_ref(
        root,
        "permissions.file",
        manifest
            .permissions
            .as_ref()
            .and_then(|value| value.file.as_deref()),
        "rho/policy/permissions.yaml",
        failures,
    );
    check_rho_file_ref(
        root,
        "crypto.file",
        manifest
            .crypto
            .as_ref()
            .and_then(|value| value.file.as_deref()),
        "rho/policy/crypto.yaml",
        failures,
    );
    check_rho_file_ref(
        root,
        "auth.file",
        manifest
            .auth
            .as_ref()
            .and_then(|value| value.file.as_deref()),
        "rho/policy/auth.yaml",
        failures,
    );
    check_rho_path_ref(
        root,
        "tools.path",
        manifest
            .tools
            .as_ref()
            .and_then(|value| value.path.as_deref()),
        "rho/tools/",
        failures,
    );
    check_rho_file_ref(
        root,
        "resources.file",
        manifest
            .resources
            .as_ref()
            .and_then(|value| value.file.as_deref()),
        "rho/resources.yaml",
        failures,
    );
}

fn check_rho_file_ref(
    root: &Path,
    label: &str,
    actual: Option<&str>,
    expected: &str,
    failures: &mut Vec<String>,
) {
    let Some(actual) = non_empty(actual) else {
        failures.push(format!("rho.yaml missing {label}"));
        return;
    };
    if actual != expected {
        failures.push(format!("rho.yaml {label} must be {expected}, got {actual}"));
    }
    let Some(path) = safe_repo_path(actual) else {
        failures.push(format!(
            "rho.yaml {label} is not a safe repo path: {actual}"
        ));
        return;
    };
    if !root.join(path).is_file() {
        failures.push(format!(
            "rho.yaml {label} references missing file: {actual}"
        ));
    }
}

fn check_rho_path_ref(
    root: &Path,
    label: &str,
    actual: Option<&str>,
    expected: &str,
    failures: &mut Vec<String>,
) {
    let Some(actual) = non_empty(actual) else {
        failures.push(format!("rho.yaml missing {label}"));
        return;
    };
    if actual != expected {
        failures.push(format!("rho.yaml {label} must be {expected}, got {actual}"));
    }
    let Some(path) = safe_repo_path(actual.trim_end_matches('/')) else {
        failures.push(format!(
            "rho.yaml {label} is not a safe repo path: {actual}"
        ));
        return;
    };
    if !root.join(path).is_dir() {
        failures.push(format!(
            "rho.yaml {label} references missing directory: {actual}"
        ));
    }
}

fn check_participant(root: &Path, identity_id: &str, failures: &mut Vec<String>) {
    let Ok(handle) = github_handle_from_identity_id(identity_id) else {
        return;
    };
    let path = root.join("rho/participants").join(format!("{handle}.yaml"));
    let Some(bundle): Option<IdentityBundleManifest> = read_yaml_check(&path, failures) else {
        return;
    };
    if bundle.identity.id != identity_id {
        failures.push(format!(
            "participant id mismatch in {}: expected {}, got {}",
            path.display(),
            identity_id,
            bundle.identity.id
        ));
    }
    if !bundle
        .identity
        .public_keys
        .iter()
        .any(|key| key.kind == "ssh-signing")
    {
        failures.push(format!(
            "participant has no signing key: {}",
            path.display()
        ));
    }
    if !bundle
        .identity
        .public_keys
        .iter()
        .any(|key| key.kind == "x25519-encryption" && key.algorithm == "x25519")
    {
        failures.push(format!(
            "participant has no X25519 encryption key: {}",
            path.display()
        ));
    }
}

fn check_orphan_participants(
    root: &Path,
    member_ids: &HashSet<String>,
    failures: &mut Vec<String>,
) -> RhoResult<()> {
    let participants_dir = root.join("rho/participants");
    if !participants_dir.is_dir() {
        return Ok(());
    }
    for entry in fs::read_dir(participants_dir)? {
        let path = entry?.path();
        let Some(file_name) = path.file_name().and_then(|value| value.to_str()) else {
            continue;
        };
        if !file_name.ends_with(".yaml") || file_name.ends_with(".rhosig.yaml") {
            continue;
        }
        let Some(bundle): Option<IdentityBundleManifest> = read_yaml_check(&path, failures) else {
            continue;
        };
        if !member_ids.contains(&bundle.identity.id) {
            failures.push(format!(
                "participant is not admitted in membership: rho/participants/{file_name} -> {}",
                bundle.identity.id
            ));
        }
    }
    Ok(())
}

fn check_legacy_inbox_paths(
    root: &Path,
    member_ids: &[String],
    advisories: &mut Vec<String>,
) -> RhoResult<()> {
    for migration in legacy_inbox_migrations(root, member_ids)? {
        advisories.push(format!(
            "legacy inbox path {}/ should move to {}/",
            migration.legacy_relative, migration.canonical_relative
        ));
    }
    advisories.sort();
    advisories.dedup();
    Ok(())
}

#[derive(Debug)]
struct InboxPathMigration {
    handle: String,
    legacy_relative: String,
    canonical_relative: String,
}

fn legacy_inbox_migrations(
    root: &Path,
    member_ids: &[String],
) -> RhoResult<Vec<InboxPathMigration>> {
    let inbox = root.join("rho/messages/inbox");
    if !inbox.is_dir() {
        return Ok(Vec::new());
    }
    let member_handles = member_ids
        .iter()
        .filter_map(|identity| github_handle_from_identity_id(identity).ok())
        .collect::<HashSet<_>>();
    let mut migrations = Vec::new();
    for entry in fs::read_dir(inbox)? {
        let path = entry?.path();
        if !path.is_dir() {
            continue;
        }
        let Some(name) = path.file_name().and_then(|value| value.to_str()) else {
            continue;
        };
        if name == "id" || !member_handles.contains(name) {
            continue;
        }
        migrations.push(InboxPathMigration {
            handle: name.to_string(),
            legacy_relative: format!("rho/messages/inbox/{name}"),
            canonical_relative: format!("rho/messages/inbox/id/github/{name}"),
        });
    }
    migrations.sort_by(|left, right| left.handle.cmp(&right.handle));
    Ok(migrations)
}

fn migrate_inbox_paths(args: &[String]) -> RhoResult<()> {
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    let dry_run = has_flag(args, "--dry-run");
    ensure_repo(&root)?;
    let membership_path = root.join("rho/membership.yaml");
    let membership: MembershipManifest = from_yaml(&fs::read_to_string(&membership_path)?)?;
    let member_ids = membership
        .members
        .iter()
        .map(|member| member.identity.clone())
        .collect::<Vec<_>>();
    let migrations = legacy_inbox_migrations(&root, &member_ids)?;
    if migrations.is_empty() {
        println!("legacy inbox paths: none");
        return Ok(());
    }
    for migration in &migrations {
        let legacy_path = root.join(&migration.legacy_relative);
        let canonical_path = root.join(&migration.canonical_relative);
        if canonical_path.exists() {
            return Err(format!(
                "cannot migrate {}; target already exists: {}",
                migration.legacy_relative, migration.canonical_relative
            )
            .into());
        }
        if dry_run {
            println!(
                "dry-run: move {} -> {}",
                migration.legacy_relative, migration.canonical_relative
            );
            continue;
        }
        ensure_parent(&canonical_path)?;
        fs::rename(&legacy_path, &canonical_path)?;
        println!(
            "moved {} -> {}",
            migration.legacy_relative, migration.canonical_relative
        );
    }
    if !dry_run {
        let rewritten = rewrite_legacy_inbox_policy_patterns(&root, &migrations)?;
        if rewritten > 0 {
            let permissions_path = root.join("rho/policy/permissions.yaml");
            let permissions: PermissionsManifest =
                from_yaml(&fs::read_to_string(&permissions_path)?)?;
            write_generated_gitattributes_block(&root, &permissions)?;
            println!("updated inbox policy patterns: {rewritten}");
        }
        println!("next: git add rho/messages/inbox rho/policy/permissions.yaml .gitattributes");
    }
    println!("legacy inbox paths migrated: {}", migrations.len());
    Ok(())
}

fn rewrite_legacy_inbox_policy_patterns(
    root: &Path,
    migrations: &[InboxPathMigration],
) -> RhoResult<usize> {
    let path = root.join("rho/policy/permissions.yaml");
    let mut text = fs::read_to_string(&path)?;
    let original = text.clone();
    let mut rewritten = 0usize;
    for migration in migrations {
        let legacy_pattern = format!("{}/**", migration.legacy_relative);
        let canonical_pattern = format!("{}/**", migration.canonical_relative);
        if text.contains(&legacy_pattern) {
            text = text.replace(&legacy_pattern, &canonical_pattern);
            rewritten += 1;
        }
    }
    if text != original {
        fs::write(path, text)?;
    }
    Ok(rewritten)
}

fn member_set(member_ids: &[String]) -> HashSet<&str> {
    member_ids.iter().map(String::as_str).collect()
}

fn check_crypto_policy(crypto: &CryptoPolicy, root: &Path, failures: &mut Vec<String>) {
    if crypto.signatures.as_deref() != Some("ssh") {
        failures.push("crypto.signatures must be ssh".to_string());
    }
    if crypto.encryption.as_deref() != Some("x25519-recipient-envelope") {
        failures.push("crypto.encryption must be x25519-recipient-envelope".to_string());
    }
    let attributes = fs::read_to_string(root.join(".gitattributes")).unwrap_or_default();
    let has_rho_filter = attributes
        .lines()
        .any(|line| line.contains("filter=rho-crypt"));
    if has_rho_filter && crypto.transparent_git_filters.as_deref() == Some("disabled") {
        failures.push(
            "crypto.transparent_git_filters is disabled but .gitattributes uses filter=rho-crypt"
                .to_string(),
        );
    }
    if has_rho_filter && crypto.filter.as_deref() != Some("rho-crypt") {
        failures
            .push("crypto.filter must be rho-crypt when protected filters are used".to_string());
    }
}

fn check_permissions_policy(
    root: &Path,
    permissions: &PermissionsManifest,
    member_ids: &[String],
    failures: &mut Vec<String>,
) -> RhoResult<()> {
    let attributes = fs::read_to_string(root.join(".gitattributes")).unwrap_or_default();
    for rule in &permissions.permissions {
        if rule.read.public && (rule.read.repo_key || !rule.read.encrypted_to.is_empty()) {
            failures.push(format!(
                "permission cannot be both public and encrypted: {}",
                rule.pattern
            ));
        }
        for recipient in &rule.read.encrypted_to {
            if !member_ids.is_empty() && !member_ids.iter().any(|member| member == recipient) {
                failures.push(format!(
                    "permission encrypted reader is not a member: {} -> {}",
                    rule.pattern, recipient
                ));
            }
            if let Err(error) = validate_actor_id(recipient) {
                failures.push(format!(
                    "invalid permission encrypted reader {recipient}: {error}"
                ));
            }
        }
        if rule.read.repo_key || !rule.read.encrypted_to.is_empty() {
            let expected_attr = format!("{} filter=rho-crypt", rule.pattern);
            if !attributes
                .lines()
                .any(|line| line.starts_with(&expected_attr))
            {
                failures.push(format!(
                    ".gitattributes is missing rho-crypt filter for {}",
                    rule.pattern
                ));
            }
        }
    }

    let files = tracked_and_untracked_files(root)?;
    for file in files {
        for rule in &permissions.permissions {
            if path_matches_pattern(&file, &rule.pattern) {
                check_permission_file(root, &file, rule, failures);
            }
        }
    }
    Ok(())
}

fn check_dataset_manifests(
    root: &Path,
    member_ids: &[String],
    failures: &mut Vec<String>,
) -> RhoResult<()> {
    let member_ids = member_set(member_ids);
    for file in tracked_and_untracked_files(root)? {
        if file == "dataset.yaml" || file.ends_with("/dataset.yaml") {
            check_dataset_manifest(root, &file, &member_ids, failures);
        }
    }
    Ok(())
}

fn check_dataset_manifest(
    root: &Path,
    relative_path: &str,
    member_ids: &HashSet<&str>,
    failures: &mut Vec<String>,
) {
    let path = root.join(relative_path);
    let text = match fs::read_to_string(&path) {
        Ok(text) => text,
        Err(error) => {
            failures.push(format!(
                "cannot read dataset manifest {relative_path}: {error}"
            ));
            return;
        }
    };
    let manifest: DatasetManifest = match from_yaml(&text) {
        Ok(manifest) => manifest,
        Err(error) => {
            failures.push(format!("invalid dataset manifest {relative_path}: {error}"));
            return;
        }
    };
    if manifest.version != Some(1) {
        failures.push(format!(
            "dataset manifest version must be 1: {relative_path}"
        ));
    }
    if manifest.uuid.is_some() || manifest.variants.is_some() {
        failures.push(format!(
            "dataset manifest uses deprecated top-level fields; expected dataset.uuid and dataset.variants: {relative_path}"
        ));
    }
    let Some(dataset) = manifest.dataset else {
        failures.push(format!("missing dataset section in {relative_path}"));
        return;
    };
    let Some(uuid) = non_empty(dataset.uuid.as_deref()) else {
        failures.push(format!("missing dataset.uuid in {relative_path}"));
        return;
    };
    let dataset_name = non_empty(dataset.name.as_deref());
    if dataset_name.is_none() {
        failures.push(format!("missing dataset.name in {relative_path}"));
    }
    if let Some(owner) = non_empty(dataset.owner.as_deref()) {
        if let Err(error) = validate_actor_id(owner) {
            failures.push(format!(
                "invalid dataset.owner in {relative_path}: {owner}: {error}"
            ));
        }
        if !member_ids.is_empty() && !member_ids.contains(owner) {
            failures.push(format!(
                "dataset owner is not a member in {relative_path}: {owner}"
            ));
        }
    } else {
        failures.push(format!("missing dataset.owner in {relative_path}"));
    }
    let Some(variants) = dataset.variants else {
        failures.push(format!("missing dataset.variants in {relative_path}"));
        return;
    };
    if variants.public.is_none() && variants.mock.is_none() && variants.real.is_none() {
        failures.push(format!(
            "dataset.variants must define at least one of public, mock, or real in {relative_path}"
        ));
    }

    if let Some(public) = variants.public {
        check_dataset_variant(root, relative_path, "public", &public, failures);
    }
    if let Some(mock) = variants.mock {
        check_dataset_variant(root, relative_path, "mock", &mock, failures);
    }
    if let Some(real) = variants.real
        && let Some(real_path) = non_empty(real.relative_path.as_deref())
    {
        check_dataset_relative_path(root, relative_path, real_path, failures);
        check_variant_sha(relative_path, "real", &real, failures);
    }
    if !relative_path.contains(uuid)
        && dataset_name.is_none_or(|name| !relative_path.contains(name))
    {
        failures.push(format!(
            "dataset path should include dataset.uuid {uuid} or dataset.name in {relative_path}"
        ));
    }
}

fn check_dataset_variant(
    root: &Path,
    manifest_path: &str,
    variant_name: &str,
    variant: &DatasetVariant,
    failures: &mut Vec<String>,
) {
    if let Some(path) = non_empty(
        variant
            .relative_path
            .as_deref()
            .or(variant.share_relative_path.as_deref()),
    ) {
        check_dataset_relative_path(root, manifest_path, path, failures);
        check_variant_sha(manifest_path, variant_name, variant, failures);
        return;
    }
    let Some(source) = &variant.source else {
        failures.push(format!(
            "missing dataset.variants.{variant_name}.relative_path or source in {manifest_path}"
        ));
        return;
    };
    if non_empty(source.kind.as_deref()).is_none() {
        failures.push(format!(
            "missing dataset.variants.{variant_name}.source.kind in {manifest_path}"
        ));
    }
    if non_empty(source.repo.as_deref()).is_none() && non_empty(source.url.as_deref()).is_none() {
        failures.push(format!(
            "dataset.variants.{variant_name}.source must include repo or url in {manifest_path}"
        ));
    }
    let _ = non_empty(source.revision.as_deref());
}

fn check_variant_sha(
    manifest_path: &str,
    variant_name: &str,
    variant: &DatasetVariant,
    failures: &mut Vec<String>,
) {
    if non_empty(variant.sha256.as_deref()).is_none() {
        failures.push(format!(
            "missing dataset.variants.{variant_name}.sha256 in {manifest_path}"
        ));
    }
}

fn check_dataset_relative_path(
    root: &Path,
    manifest_path: &str,
    relative_payload: &str,
    failures: &mut Vec<String>,
) {
    let payload = Path::new(relative_payload);
    if payload.is_absolute()
        || payload.components().any(|component| {
            matches!(
                component,
                Component::ParentDir | Component::RootDir | Component::Prefix(_)
            )
        })
    {
        failures.push(format!(
            "unsafe dataset payload path in {manifest_path}: {relative_payload}"
        ));
        return;
    }
    let manifest_dir = root
        .join(manifest_path)
        .parent()
        .map(Path::to_path_buf)
        .unwrap_or_else(|| root.to_path_buf());
    if !manifest_dir.join(payload).is_file() {
        failures.push(format!(
            "dataset payload missing for {manifest_path}: {relative_payload}"
        ));
    }
}

fn non_empty(value: Option<&str>) -> Option<&str> {
    value.map(str::trim).filter(|value| !value.is_empty())
}

#[derive(Default)]
struct FlowReport {
    next_steps: Vec<String>,
}

fn check_flow_receipts(
    root: &Path,
    repo_id: &str,
    owner: &str,
    failures: &mut Vec<String>,
) -> RhoResult<FlowReport> {
    let mut report = FlowReport::default();
    let receipt_files: Vec<String> = tracked_and_untracked_files(root)?
        .into_iter()
        .filter(|file| {
            file.starts_with("rho/run-receipts/")
                && file.ends_with(".yaml")
                && !file.ends_with(".rhosig.yaml")
        })
        .collect();
    if receipt_files.is_empty() {
        let request_ids = repo_request_ids(root)?;
        if request_ids.is_empty() {
            report
                .next_steps
                .push("submit a signed request".to_string());
        } else {
            for request_id in request_ids {
                report.next_steps.push(format!(
                    "rho run approve-request {request_id} --private-root <private-datasets-root>"
                ));
            }
        }
        return Ok(report);
    }
    for receipt_file in receipt_files {
        check_flow_receipt(root, repo_id, owner, &receipt_file, failures, &mut report);
    }
    Ok(report)
}

fn check_flow_receipt(
    root: &Path,
    repo_id: &str,
    owner: &str,
    receipt_file: &str,
    failures: &mut Vec<String>,
    report: &mut FlowReport,
) {
    let receipt_path = root.join(receipt_file);
    let receipt: RunReceiptManifest = match read_yaml_check(&receipt_path, failures) {
        Some(receipt) => receipt,
        None => return,
    };
    let record = receipt.run_receipt;
    if let Err(error) = validate_request_id(&record.request_id) {
        failures.push(format!(
            "invalid run receipt request_id in {receipt_file}: {error}"
        ));
    }
    for (label, value) in [
        ("run_id", record.run_id.as_str()),
        ("runner", record.runner.as_str()),
        ("status", record.status.as_str()),
        ("tier", record.tier.as_str()),
        ("code_path", record.code_path.as_str()),
        ("code_sha256", record.code_sha256.as_str()),
        ("output_sha256", record.output_sha256.as_str()),
        ("result_path", record.result_path.as_str()),
    ] {
        if value.trim().is_empty() {
            failures.push(format!("missing run receipt {label} in {receipt_file}"));
        }
    }
    if record.tier == "real" && record.input_sha256.trim().is_empty() {
        failures.push(format!(
            "missing real input commitment in run receipt {receipt_file}"
        ));
    }
    if let Some(input_path) = non_empty(record.input_path.as_deref())
        && Path::new(input_path).is_absolute()
    {
        failures.push(format!(
            "run receipt leaks absolute input_path in {receipt_file}: {input_path}"
        ));
    }
    if !repo_request_exists(root, &record.request_id) {
        failures.push(format!(
            "run receipt references missing request {}: {receipt_file}",
            record.request_id
        ));
    }
    let Some(result_path) = safe_repo_path(&record.result_path) else {
        failures.push(format!(
            "unsafe run receipt result_path in {receipt_file}: {}",
            record.result_path
        ));
        return;
    };
    let result_path = root.join(result_path);
    let expected_result_recipient = flow_recipient_from_result_path(&record.result_path);
    let result_exists = result_path.is_file();
    if !result_exists {
        report.next_steps.push(format!(
            "rho result release {} --run-id {} --to {}",
            record.request_id,
            record.run_id,
            expected_result_recipient
                .as_deref()
                .unwrap_or("<result-recipient>")
        ));
    }
    let result = if result_path.is_file() {
        read_result_for_flow(&result_path, &record, failures)
    } else {
        None
    };
    check_flow_approval_grant(
        root,
        repo_id,
        owner,
        &record,
        result
            .as_ref()
            .map(|result| result.to.as_str())
            .or(expected_result_recipient.as_deref()),
        receipt_file,
        failures,
    );
    let result_signature = result_path.with_file_name("result.rhosig.yaml");
    if result_exists && !result_signature.is_file() {
        failures.push(format!(
            "run receipt result signature missing for {}",
            record.result_path
        ));
    }
    let receipt_signature = receipt_path.with_file_name(format!("{}.rhosig.yaml", record.run_id));
    if !receipt_signature.is_file() {
        failures.push(format!("run receipt signature missing for {receipt_file}"));
    }
    if receipt_signature.is_file() {
        if let Some(recipient) = result
            .as_ref()
            .map(|result| result.to.as_str())
            .or(expected_result_recipient.as_deref())
        {
            let context =
                flow_signature_context(repo_id, &record.request_id, recipient, "rho.run_receipt");
            verify_repo_signature_with_context(
                root,
                &receipt_path,
                &receipt_signature,
                Some(&context),
                failures,
            );
        } else {
            verify_repo_signature(root, &receipt_path, &receipt_signature, failures);
        }
    }
    if result_path.is_file() && result_signature.is_file() {
        if let Some(result) = &result {
            let context =
                flow_signature_context(repo_id, &record.request_id, &result.to, "rho.result");
            verify_repo_signature_with_context(
                root,
                &result_path,
                &result_signature,
                Some(&context),
                failures,
            );
        } else {
            verify_repo_signature(root, &result_path, &result_signature, failures);
        }
    }
}

fn read_result_for_flow(
    result_path: &Path,
    receipt: &RunReceiptRecord,
    failures: &mut Vec<String>,
) -> Option<ResultRecord> {
    let result: ResultManifest = read_yaml_check(result_path, failures)?;
    let result = result.result;
    if result.request_id != receipt.request_id {
        failures.push(format!(
            "result request_id mismatch for {}: expected {}, got {}",
            result_path.display(),
            receipt.request_id,
            result.request_id
        ));
    }
    if result.run_id != receipt.run_id {
        failures.push(format!(
            "result run_id mismatch for {}: expected {}, got {}",
            result_path.display(),
            receipt.run_id,
            result.run_id
        ));
    }
    if let Err(error) = validate_actor_id(&result.to) {
        failures.push(format!(
            "invalid result recipient in {}: {error}",
            result_path.display()
        ));
    }
    Some(result)
}

fn flow_signature_context(
    repo_id: &str,
    request_id: &str,
    recipient_id: &str,
    purpose: &str,
) -> SignatureContext {
    SignatureContext {
        repo_id: Some(repo_id.to_string()),
        request_id: Some(request_id.to_string()),
        message_id: None,
        recipient_id: Some(recipient_id.to_string()),
        purpose: Some(purpose.to_string()),
    }
}

fn check_flow_approval_grant(
    root: &Path,
    repo_id: &str,
    owner: &str,
    receipt: &RunReceiptRecord,
    result_recipient: Option<&str>,
    receipt_file: &str,
    failures: &mut Vec<String>,
) {
    let Some(action_id) = action_id_for_run_id(&receipt.run_id) else {
        failures.push(format!(
            "run receipt has non-conventional run_id with no approval grant mapping in {receipt_file}: {}",
            receipt.run_id
        ));
        return;
    };
    let relative_grant = format!(
        "rho/approval-grants/{}/{}.yaml",
        receipt.request_id, action_id
    );
    let grant_path = root.join(&relative_grant);
    if !grant_path.is_file() {
        failures.push(format!(
            "approval grant missing for run receipt {receipt_file}: {relative_grant}"
        ));
        return;
    }
    let signature_path = grant_path.with_file_name(format!("{action_id}.rhosig.yaml"));
    if !signature_path.is_file() {
        failures.push(format!(
            "approval grant signature missing for {relative_grant}"
        ));
    } else {
        match signature_signer(&signature_path) {
            Ok(signer) if signer == owner => {}
            Ok(signer) => failures.push(format!(
                "approval grant signer mismatch for {relative_grant}: expected {owner}, got {signer}"
            )),
            Err(error) => failures.push(format!(
                "invalid approval grant signature {}: {error}",
                signature_path.display()
            )),
        }
        if let Some(result_recipient) = result_recipient {
            let context = flow_signature_context(
                repo_id,
                &receipt.request_id,
                result_recipient,
                "rho.approval_grant",
            );
            verify_repo_signature_with_context(
                root,
                &grant_path,
                &signature_path,
                Some(&context),
                failures,
            );
        } else {
            verify_repo_signature(root, &grant_path, &signature_path, failures);
        }
    }

    let grant: ApprovalGrantManifest = match read_yaml_check(&grant_path, failures) {
        Some(grant) => grant,
        None => return,
    };
    let grant = grant.approval_grant;
    if grant.action_id != action_id {
        failures.push(format!(
            "approval grant action_id mismatch for {relative_grant}: expected {action_id}, got {}",
            grant.action_id
        ));
    }
    if grant.request_id != receipt.request_id {
        failures.push(format!(
            "approval grant request_id mismatch for {relative_grant}: expected {}, got {}",
            receipt.request_id, grant.request_id
        ));
    }
    if grant.decision != "approved" {
        failures.push(format!(
            "approval grant decision is not approved for {relative_grant}: {}",
            grant.decision
        ));
    }
    if grant.granted_by != owner {
        failures.push(format!(
            "approval grant granted_by mismatch for {relative_grant}: expected {owner}, got {}",
            grant.granted_by
        ));
    }
    for (label, value) in [
        ("tool_id", grant.tool_id.as_str()),
        ("action_type", grant.action_type.as_str()),
        ("action.sha256", grant.action.sha256.as_str()),
        ("local_grant.sha256", grant.local_grant.sha256.as_str()),
        ("code.path", grant.code.path.as_str()),
        (
            "private_input.dataset_uuid",
            grant.private_input.dataset_uuid.as_str(),
        ),
    ] {
        if value.trim().is_empty() {
            failures.push(format!(
                "missing approval grant {label} in {relative_grant}"
            ));
        }
    }
    let expected_local_grant = format!(".rho/action-grants/{action_id}.yaml");
    if grant.local_grant.path != expected_local_grant {
        failures.push(format!(
            "approval grant local_grant path mismatch for {relative_grant}: expected {expected_local_grant}, got {}",
            grant.local_grant.path
        ));
    }
    if grant.code.kind != "code" {
        failures.push(format!(
            "approval grant code input kind mismatch for {relative_grant}: {}",
            grant.code.kind
        ));
    }
    if grant.code.sha256 != receipt.code_sha256 {
        failures.push(format!(
            "approval grant code digest mismatch for {relative_grant}: expected {}, got {}",
            receipt.code_sha256, grant.code.sha256
        ));
    }
    if grant.private_input.tier != receipt.tier {
        failures.push(format!(
            "approval grant private input tier mismatch for {relative_grant}: expected {}, got {}",
            receipt.tier, grant.private_input.tier
        ));
    }
    if grant.private_input.sha256 != receipt.input_sha256 {
        failures.push(format!(
            "approval grant private input digest mismatch for {relative_grant}: expected {}, got {}",
            receipt.input_sha256, grant.private_input.sha256
        ));
    }
    if let Some(action_path) = safe_repo_path(&grant.action.path)
        && root.join(&action_path).is_file()
        && let Ok(digest) = file_digest(&root.join(action_path))
        && digest != grant.action.sha256
    {
        failures.push(format!(
            "approval grant action digest mismatch for {relative_grant}: expected {}, got {digest}",
            grant.action.sha256
        ));
    }
}

fn action_id_for_run_id(run_id: &str) -> Option<String> {
    run_id
        .strip_prefix("run-")
        .map(|suffix| format!("act-{suffix}"))
}

fn verify_repo_signature(
    root: &Path,
    signed_path: &Path,
    signature_path: &Path,
    failures: &mut Vec<String>,
) {
    verify_repo_signature_with_context(root, signed_path, signature_path, None, failures);
}

fn verify_repo_signature_with_context(
    root: &Path,
    signed_path: &Path,
    signature_path: &Path,
    context: Option<&SignatureContext>,
    failures: &mut Vec<String>,
) {
    let text = match fs::read_to_string(signature_path) {
        Ok(text) => text,
        Err(error) => {
            failures.push(format!(
                "cannot read signature {}: {error}",
                signature_path.display()
            ));
            return;
        }
    };
    let manifest: SignatureManifest = match from_yaml(&text) {
        Ok(manifest) => manifest,
        Err(error) => {
            failures.push(format!(
                "invalid signature manifest {}: {error}",
                signature_path.display()
            ));
            return;
        }
    };
    let mut rho_crypto = rho_crypto_command();
    rho_crypto
        .arg("verify")
        .arg(signed_path)
        .arg("--signature")
        .arg(signature_path)
        .arg("--identity")
        .arg(&manifest.signature.signer)
        .arg("--repo-root")
        .arg(root);
    if let Some(context) = context {
        rho_crypto
            .arg("--repo-id")
            .arg(context.repo_id.as_deref().unwrap_or(""))
            .arg("--request-id")
            .arg(context.request_id.as_deref().unwrap_or(""))
            .arg("--recipient")
            .arg(context.recipient_id.as_deref().unwrap_or(""))
            .arg("--purpose")
            .arg(context.purpose.as_deref().unwrap_or(""));
    }
    let output = rho_crypto.output();
    match output {
        Ok(output) if output.status.success() => {}
        Ok(output) => {
            let stderr = String::from_utf8_lossy(&output.stderr);
            failures.push(format!(
                "signature verification failed for {}: {}",
                signed_path.display(),
                stderr.trim()
            ));
        }
        Err(error) => failures.push(format!(
            "failed to run rho crypto verify for {}: {error}",
            signed_path.display()
        )),
    }
}

fn check_governance_signatures(
    root: &Path,
    owner: &str,
    failures: &mut Vec<String>,
) -> RhoResult<()> {
    let files = governance_manifest_files(root)?;
    if files.is_empty() {
        failures.push("governance has no manifest files to verify".to_string());
        return Ok(());
    }
    for file in files {
        let signed_path = root.join(&file);
        let signature_path = governance_signature_path(root, &file)?;
        if !signature_path.is_file() {
            failures.push(format!(
                "governance signature missing for {file}: {}",
                repo_relative_display(root, &signature_path)
            ));
            continue;
        }
        match signature_signer(&signature_path) {
            Ok(signer) if signer == owner => {}
            Ok(signer) => failures.push(format!(
                "governance signature signer mismatch for {file}: expected {owner}, got {signer}"
            )),
            Err(error) => failures.push(format!(
                "invalid governance signature {}: {error}",
                signature_path.display()
            )),
        }
        verify_repo_signature(root, &signed_path, &signature_path, failures);
    }
    Ok(())
}

fn governance_manifest_files(root: &Path) -> RhoResult<Vec<String>> {
    let mut files: Vec<String> = tracked_and_untracked_files(root)?
        .into_iter()
        .filter(|file| is_governance_manifest(file))
        .collect();
    files.sort();
    files.dedup();
    Ok(files)
}

fn is_governance_manifest(file: &str) -> bool {
    if !file.ends_with(".yaml") || file.ends_with(".rhosig.yaml") {
        return false;
    }
    matches!(
        file,
        "rho.yaml" | "rho/repo.yaml" | "rho/membership.yaml" | "rho/resources.yaml"
    ) || (file.starts_with("rho/policy/") && !file["rho/policy/".len()..].contains('/'))
        || (file.starts_with("rho/participants/")
            && !file["rho/participants/".len()..].contains('/'))
        || (file.starts_with("rho/tools/") && !file["rho/tools/".len()..].contains('/'))
        || file == "dataset.yaml"
        || file.ends_with("/dataset.yaml")
}

fn governance_signature_path(root: &Path, relative_file: &str) -> RhoResult<PathBuf> {
    let path = root.join(relative_file);
    let Some(file_name) = path.file_name().and_then(|value| value.to_str()) else {
        return Err(format!("invalid governance path: {relative_file}").into());
    };
    let signature_name = if let Some(stem) = file_name.strip_suffix(".yaml") {
        format!("{stem}.rhosig.yaml")
    } else {
        format!("{file_name}.rhosig.yaml")
    };
    Ok(path.with_file_name(signature_name))
}

fn signature_signer(signature_path: &Path) -> RhoResult<String> {
    let text = fs::read_to_string(signature_path)?;
    let manifest: SignatureManifest = from_yaml(&text)?;
    Ok(manifest.signature.signer)
}

fn sign_file(signed_path: &Path, signature_path: &Path, identity: &str) -> RhoResult<()> {
    let output = rho_crypto_command()
        .arg("sign")
        .arg(signed_path)
        .arg("--identity")
        .arg(identity)
        .arg("--out")
        .arg(signature_path)
        .output()?;
    if output.status.success() {
        return Ok(());
    }
    let stderr = String::from_utf8_lossy(&output.stderr);
    Err(format!(
        "failed to sign {}: {}",
        signed_path.display(),
        stderr.trim()
    )
    .into())
}

fn repo_relative_display(root: &Path, path: &Path) -> String {
    path.strip_prefix(root)
        .map(|path| path.display().to_string())
        .unwrap_or_else(|_| path.display().to_string())
}

fn rho_crypto_command() -> Command {
    let rho = env::current_exe()
        .ok()
        .map(|path| path.with_file_name("rho"))
        .unwrap_or_else(|| PathBuf::from("rho"));
    let mut command = if rho.is_file() {
        Command::new(rho)
    } else {
        let mut command = Command::new("cargo");
        command.args(["run", "--quiet", "--bin", "rho", "--"]);
        command
    };
    command.arg("crypto");
    command
}

fn repo_request_exists(root: &Path, request_id: &str) -> bool {
    let inbox = root.join("rho/messages/inbox");
    request_exists_under(&inbox, request_id)
}

fn repo_request_ids(root: &Path) -> RhoResult<Vec<String>> {
    let inbox = root.join("rho/messages/inbox");
    let mut ids = Vec::new();
    collect_request_ids_under(&inbox, &mut ids)?;
    ids.sort();
    ids.dedup();
    Ok(ids)
}

fn collect_request_ids_under(dir: &Path, ids: &mut Vec<String>) -> RhoResult<()> {
    let Ok(entries) = fs::read_dir(dir) else {
        return Ok(());
    };
    for entry in entries {
        let path = entry?.path();
        if !path.is_dir() {
            continue;
        }
        if path.join("request.yaml").is_file()
            && let Some(request_id) = path.file_name().and_then(|value| value.to_str())
            && validate_request_id(request_id).is_ok()
        {
            ids.push(request_id.to_string());
        }
        collect_request_ids_under(&path, ids)?;
    }
    Ok(())
}

fn request_exists_under(dir: &Path, request_id: &str) -> bool {
    let Ok(entries) = fs::read_dir(dir) else {
        return false;
    };
    for entry in entries.flatten() {
        let path = entry.path();
        if path.is_dir() {
            if path.file_name().and_then(|value| value.to_str()) == Some(request_id)
                && path.join("request.yaml").is_file()
            {
                return true;
            }
            if request_exists_under(&path, request_id) {
                return true;
            }
        }
    }
    false
}

fn flow_recipient_from_result_path(path: &str) -> Option<String> {
    let parts = path.split('/').collect::<Vec<_>>();
    match parts.as_slice() {
        [
            "rho",
            "messages",
            "inbox",
            "id",
            "github",
            handle,
            _request_id,
            "result.yaml",
        ] if !handle.is_empty() && !handle.contains('/') => {
            Some(format!("rho://id/github/{handle}"))
        }
        [
            "rho",
            "messages",
            "inbox",
            handle,
            _request_id,
            "result.yaml",
        ] if !handle.is_empty() && !handle.contains('/') => {
            Some(format!("rho://id/github/{handle}"))
        }
        _ => None,
    }
}

fn identity_inbox_relative_path(identity_id: &str) -> RhoResult<PathBuf> {
    let handle = github_handle_from_identity_id(identity_id)?;
    Ok(PathBuf::from("id").join("github").join(handle))
}

fn safe_repo_path(path: &str) -> Option<PathBuf> {
    let path = Path::new(path);
    if path.is_absolute() {
        return None;
    }
    let mut has_component = false;
    for component in path.components() {
        match component {
            Component::Normal(_) => has_component = true,
            Component::CurDir => {}
            Component::ParentDir | Component::RootDir | Component::Prefix(_) => return None,
        }
    }
    has_component.then(|| path.to_path_buf())
}

fn tracked_and_untracked_files(root: &Path) -> RhoResult<Vec<String>> {
    let output = Command::new("git")
        .arg("-C")
        .arg(root)
        .args([
            "ls-files",
            "--cached",
            "--others",
            "--exclude-standard",
            "-z",
        ])
        .output()?;
    if !output.status.success() {
        return Err("git ls-files failed".into());
    }
    Ok(output
        .stdout
        .split(|byte| *byte == 0)
        .filter(|entry| !entry.is_empty())
        .filter_map(|entry| String::from_utf8(entry.to_vec()).ok())
        .collect())
}

fn check_permission_file(
    root: &Path,
    relative_path: &str,
    rule: &PermissionRule,
    failures: &mut Vec<String>,
) {
    if rule.read.public {
        return;
    }
    let recipients = &rule.read.encrypted_to;
    let content = index_blob(root, relative_path)
        .or_else(|| fs::read_to_string(root.join(relative_path)).ok())
        .unwrap_or_default();
    let kind = yaml_top_level_kind(&content);
    if !matches!(
        kind.as_deref(),
        Some("rho_recipient_envelope" | "rho_transparent_file")
    ) {
        failures.push(format!(
            "private-read file is not encrypted: {relative_path}"
        ));
        return;
    }
    if !recipients.is_empty() && kind.as_deref() == Some("rho_transparent_file") {
        failures.push(format!(
            "protected file uses repo-key encryption but policy requires recipients: {relative_path}"
        ));
        return;
    }
    if kind.as_deref() == Some("rho_recipient_envelope") {
        for recipient in recipients {
            if !content.contains(&format!("identity_id: {recipient}"))
                && !content.contains(&format!("identity_id: \"{recipient}\""))
            {
                failures.push(format!(
                    "protected file is not encrypted to expected recipient {recipient}: {relative_path}"
                ));
            }
        }
    }
}

fn missing_recipients(content: &str, recipients: &[String]) -> Vec<String> {
    recipients
        .iter()
        .filter(|recipient| {
            !content.contains(&format!("identity_id: {recipient}"))
                && !content.contains(&format!("identity_id: \"{recipient}\""))
        })
        .cloned()
        .collect()
}

fn index_blob(root: &Path, relative_path: &str) -> Option<String> {
    let output = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["show", &format!(":{relative_path}")])
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }
    String::from_utf8(output.stdout).ok()
}

fn head_blob(root: &Path, relative_path: &str) -> Option<String> {
    let output = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["show", &format!("HEAD:{relative_path}")])
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }
    String::from_utf8(output.stdout).ok()
}

fn github_handle_from_identity_id(identity_id: &str) -> RhoResult<String> {
    let Some(handle) = identity_id.strip_prefix("rho://id/github/") else {
        return Err(format!("unsupported identity id: {identity_id}").into());
    };
    if handle.is_empty() || handle.contains('/') {
        return Err(format!("unsupported identity id: {identity_id}").into());
    }
    Ok(handle.to_string())
}

fn active_identity() -> Option<String> {
    env::var("RHO_IDENTITY")
        .ok()
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
}

fn detect_github_remote(root: &Path) -> Option<String> {
    let output = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["remote", "get-url", "origin"])
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }
    let remote = String::from_utf8_lossy(&output.stdout).trim().to_string();
    if remote.is_empty() {
        None
    } else {
        Some(providers::github::repo_candidate_from_remote(&remote).unwrap_or(remote))
    }
}

fn protect_path(args: &[String]) -> RhoResult<()> {
    let Some(pattern) = args.first().map(String::as_str) else {
        usage();
    };
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    let key_file = arg_value(args, "--key-file")
        .map(PathBuf::from)
        .unwrap_or_else(|| default_repo_key_file(&root));
    let rho_bin = arg_value(args, "--rho-bin").unwrap_or_else(default_rho_bin);
    let recipients = repeated_arg(args, "--recipient")
        .into_iter()
        .map(|recipient| normalize_actor_id(&recipient))
        .collect::<RhoResult<Vec<_>>>()?;

    ensure_repo(&root)?;
    if recipients.is_empty() {
        ensure_repo_key(&key_file)?;
    } else {
        for recipient in &recipients {
            validate_actor_id(recipient)?;
        }
    }
    if recipients.is_empty() {
        configure_git_filter(&root, &rho_bin, &key_file)?;
        write_filter_crypto_policy(&root)?;
        write_permissions_policy_rule(&root, pattern, &[], true)?;
        sync_gitattributes_from_permissions(&root)?;
    } else {
        configure_git_filter_for_policy(&root, &rho_bin)?;
        write_filter_crypto_policy(&root)?;
        write_permissions_policy_rule(&root, pattern, &recipients, false)?;
        sync_gitattributes_from_permissions(&root)?;
    }
    install_pre_commit_hook(&root)?;

    if recipients.is_empty() {
        println!("{}", key_file.display());
    }
    println!("{}", root.join(".gitattributes").display());
    println!("{}", root.join("rho/policy/permissions.yaml").display());
    println!("{}", root.join(".git/hooks/pre-commit").display());
    Ok(())
}

fn install_filters(args: &[String]) -> RhoResult<()> {
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    let rho_bin = arg_value(args, "--rho-bin").unwrap_or_else(default_rho_bin);

    ensure_repo(&root)?;
    let permissions_path = root.join("rho/policy/permissions.yaml");
    let permissions: PermissionsManifest = from_yaml(&fs::read_to_string(&permissions_path)?)?;
    let installed = protected_permission_rules(&permissions).count();
    write_generated_gitattributes_block(&root, &permissions)?;
    configure_git_filter_for_policy(&root, &rho_bin)?;
    write_filter_crypto_policy(&root)?;
    install_pre_commit_hook(&root)?;

    println!("{}", root.join(".gitattributes").display());
    println!("{}", root.join(".git/hooks/pre-commit").display());
    println!("installed encrypted path filters: {installed}");
    Ok(())
}

fn refresh_protected(args: &[String]) -> RhoResult<()> {
    let root = arg_value(args, "--root")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    let rho_bin = arg_value(args, "--rho-bin").unwrap_or_else(default_rho_bin);

    ensure_repo(&root)?;
    let root_arg = root.display().to_string();
    install_filters(&[
        "--root".to_string(),
        root_arg,
        "--rho-bin".to_string(),
        rho_bin.clone(),
    ])?;

    let permissions_path = root.join("rho/policy/permissions.yaml");
    let permissions: PermissionsManifest = from_yaml(&fs::read_to_string(&permissions_path)?)?;
    let tracked = tracked_files(&root)?;
    let mut refreshed = 0usize;
    let mut skipped_dirty = 0usize;
    let mut unchanged = 0usize;

    for file in tracked {
        let Some(_rule) = permissions.permissions.iter().find(|rule| {
            !rule.read.public
                && !rule.read.encrypted_to.is_empty()
                && path_matches_pattern(&file, &rule.pattern)
        }) else {
            continue;
        };
        let path = root.join(&file);
        let Ok(text) = fs::read_to_string(&path) else {
            continue;
        };
        if !is_rho_encrypted_text(&text) {
            continue;
        }
        if path_has_worktree_changes(&root, &file)? {
            skipped_dirty += 1;
            println!("skipped dirty protected file: {file}");
            continue;
        }
        let plaintext = smudge_file_text(&root, &rho_bin, &file, &text)?;
        if plaintext == text || is_rho_encrypted_text(&plaintext) {
            unchanged += 1;
            continue;
        }
        fs::write(&path, plaintext)?;
        refreshed += 1;
        println!("refreshed protected file: {file}");
    }

    println!("refreshed protected files: {refreshed}");
    if skipped_dirty > 0 {
        println!("skipped dirty protected files: {skipped_dirty}");
    }
    if unchanged > 0 {
        println!("unchanged protected envelopes: {unchanged}");
    }
    Ok(())
}

fn tracked_files(root: &Path) -> RhoResult<Vec<String>> {
    let output = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["ls-files", "-z"])
        .output()?;
    if !output.status.success() {
        return Err("git ls-files failed".into());
    }
    Ok(output
        .stdout
        .split(|byte| *byte == 0)
        .filter(|entry| !entry.is_empty())
        .filter_map(|entry| String::from_utf8(entry.to_vec()).ok())
        .collect())
}

fn path_has_worktree_changes(root: &Path, relative_path: &str) -> RhoResult<bool> {
    let output = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["status", "--porcelain", "--"])
        .arg(relative_path)
        .output()?;
    if !output.status.success() {
        return Err(format!("git status failed for {relative_path}").into());
    }
    Ok(!output.stdout.is_empty())
}

fn smudge_file_text(
    root: &Path,
    rho_bin: &str,
    relative_path: &str,
    text: &str,
) -> RhoResult<String> {
    let mut child = Command::new(rho_bin)
        .args(["crypto", "smudge", "--repo-root"])
        .arg(root)
        .args(["--path", relative_path])
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()?;
    {
        let Some(stdin) = child.stdin.as_mut() else {
            return Err("failed to open rho crypto smudge stdin".into());
        };
        stdin.write_all(text.as_bytes())?;
    }
    let output = child.wait_with_output()?;
    if !output.status.success() {
        return Err(format!("rho crypto smudge failed for {relative_path}").into());
    }
    Ok(String::from_utf8(output.stdout)?)
}

fn ensure_repo(root: &Path) -> RhoResult<()> {
    if !root.join(".git").exists() {
        return Err(format!("not a Git working tree root: {}", root.display()).into());
    }
    if !root.join("rho/repo.yaml").is_file() {
        return Err(format!(
            "rho repo not initialized: {}",
            root.join("rho/repo.yaml").display()
        )
        .into());
    }
    Ok(())
}

fn default_repo_key_file(root: &Path) -> PathBuf {
    let repo_slug = fs::read_to_string(root.join("rho/repo.yaml"))
        .ok()
        .and_then(|text| from_yaml::<RepoManifest>(&text).ok())
        .map(|manifest| manifest.repo.id)
        .unwrap_or_else(|| "rho-repo".to_string())
        .trim_matches('"')
        .replace("rho://repo/", "")
        .replace(['/', ':'], "-");
    rho_home()
        .join("repo-keys")
        .join(format!("{repo_slug}.key"))
}

fn default_rho_bin() -> String {
    if let Ok(exe) = env::current_exe()
        && let Some(parent) = exe.parent()
    {
        let sibling = parent.join("rho");
        if sibling.is_file() {
            return sibling.display().to_string();
        }
    }
    "rho".to_string()
}

fn rho_home() -> PathBuf {
    if let Ok(value) = env::var("RHO_HOME")
        && !value.is_empty()
    {
        return PathBuf::from(value);
    }
    env::var("HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|_| PathBuf::from("."))
        .join(".rho")
}

fn ensure_repo_key(path: &Path) -> RhoResult<()> {
    if path.is_file() {
        return Ok(());
    }
    ensure_parent(path)?;
    let mut key = [0u8; 32];
    getrandom::getrandom(&mut key).map_err(|error| error.to_string())?;
    fs::write(path, format!("{}\n", BASE64.encode(key)))?;
    Ok(())
}

fn configure_git_filter(root: &Path, rho_bin: &str, key_file: &Path) -> RhoResult<()> {
    let clean = format!(
        "{} crypto clean --key-file {}",
        shell_quote(rho_bin),
        shell_quote(&key_file.display().to_string())
    );
    let smudge = format!(
        "{} crypto smudge --key-file {}",
        shell_quote(rho_bin),
        shell_quote(&key_file.display().to_string())
    );
    git_config(root, "filter.rho-crypt.clean", &clean)?;
    git_config(root, "filter.rho-crypt.smudge", &smudge)?;
    git_config(root, "filter.rho-crypt.required", "true")?;
    git_config(root, "diff.rho-crypt.textconv", &smudge)?;
    Ok(())
}

fn configure_git_filter_for_policy(root: &Path, rho_bin: &str) -> RhoResult<()> {
    let env_prefix = env::var("RHO_HOME")
        .ok()
        .filter(|value| !value.is_empty())
        .map(|value| format!("RHO_HOME={} ", shell_quote(&value)))
        .unwrap_or_default();
    let clean = format!(
        "{}{} crypto clean --repo-root {} --path %f",
        env_prefix,
        shell_quote(rho_bin),
        shell_quote(&root.display().to_string())
    );
    let smudge = format!(
        "{}{} crypto smudge --repo-root {} --path %f",
        env_prefix,
        shell_quote(rho_bin),
        shell_quote(&root.display().to_string())
    );
    git_config(root, "filter.rho-crypt.clean", &clean)?;
    git_config(root, "filter.rho-crypt.smudge", &smudge)?;
    git_config(root, "filter.rho-crypt.required", "true")?;
    git_config(root, "diff.rho-crypt.textconv", &smudge)?;
    Ok(())
}

fn write_filter_crypto_policy(root: &Path) -> RhoResult<()> {
    write_if_missing_or_forced(
        &root.join("rho/policy/crypto.yaml"),
        "version: 1\ncrypto:\n  signatures: ssh\n  encryption: x25519-recipient-envelope\n  encrypted_envelopes: explicit\n  transparent_git_filters: enabled\n  filter: rho-crypt\n",
        true,
    )
}

const GENERATED_ATTRIBUTES_START: &str = "# >>> rho generated encrypted path filters >>>";
const GENERATED_ATTRIBUTES_END: &str = "# <<< rho generated encrypted path filters <<<";

fn protected_permission_rules(
    permissions: &PermissionsManifest,
) -> impl Iterator<Item = &PermissionRule> {
    permissions
        .permissions
        .iter()
        .filter(|rule| rule.read.repo_key || !rule.read.encrypted_to.is_empty())
}

fn generated_gitattributes_block(permissions: &PermissionsManifest) -> String {
    let mut rules = protected_permission_rules(permissions).collect::<Vec<_>>();
    rules.sort_by(|left, right| left.pattern.cmp(&right.pattern));
    rules.dedup_by(|left, right| left.pattern == right.pattern);
    if rules.is_empty() {
        return String::new();
    }

    let mut text = String::new();
    text.push_str(GENERATED_ATTRIBUTES_START);
    text.push('\n');
    text.push_str("# Generated from rho/policy/permissions.yaml; run `rho repo install-filters` after policy edits.\n");
    for rule in rules {
        text.push_str(&format!(
            "{} filter=rho-crypt diff=rho-crypt\n",
            rule.pattern
        ));
    }
    text.push_str(GENERATED_ATTRIBUTES_END);
    text.push('\n');
    text
}

fn is_legacy_generated_attribute(line: &str, patterns: &[&str]) -> bool {
    if !line.contains("filter=rho-crypt") {
        return false;
    }
    let Some(pattern) = line.split_whitespace().next() else {
        return false;
    };
    patterns.contains(&pattern)
}

fn write_generated_gitattributes_block(
    root: &Path,
    permissions: &PermissionsManifest,
) -> RhoResult<()> {
    let attributes_path = root.join(".gitattributes");
    let existing = fs::read_to_string(&attributes_path).unwrap_or_default();
    let patterns = protected_permission_rules(permissions)
        .map(|rule| rule.pattern.as_str())
        .collect::<Vec<_>>();
    let mut lines = Vec::new();
    let mut in_generated_block = false;
    for line in existing.lines() {
        let trimmed = line.trim();
        if trimmed == GENERATED_ATTRIBUTES_START {
            in_generated_block = true;
            continue;
        }
        if in_generated_block {
            if trimmed == GENERATED_ATTRIBUTES_END {
                in_generated_block = false;
            }
            continue;
        }
        if is_legacy_generated_attribute(line, &patterns) {
            continue;
        }
        lines.push(line.to_string());
    }
    while lines.last().is_some_and(|line| line.trim().is_empty()) {
        lines.pop();
    }

    let mut updated = lines.join("\n");
    if !updated.is_empty() {
        updated.push('\n');
    }
    let block = generated_gitattributes_block(permissions);
    if !block.is_empty() {
        if !updated.is_empty() {
            updated.push('\n');
        }
        updated.push_str(&block);
    }
    fs::write(attributes_path, updated)?;
    Ok(())
}

fn sync_gitattributes_from_permissions(root: &Path) -> RhoResult<()> {
    let permissions_path = root.join("rho/policy/permissions.yaml");
    let permissions: PermissionsManifest = from_yaml(&fs::read_to_string(&permissions_path)?)?;
    write_generated_gitattributes_block(root, &permissions)
}

fn write_permissions_policy_rule(
    root: &Path,
    pattern: &str,
    recipients: &[String],
    repo_key: bool,
) -> RhoResult<()> {
    let permissions_path = root.join("rho/policy/permissions.yaml");
    let existing: Option<PermissionsManifest> = read_yaml_check(&permissions_path, &mut Vec::new());
    let mut rules = existing
        .map(|manifest| manifest.permissions)
        .unwrap_or_default();
    if let Some(base) = pattern.strip_suffix("/**") {
        rules.retain(|rule| rule.pattern != base);
    }
    if let Some(rule) = rules.iter_mut().find(|rule| rule.pattern == pattern) {
        rule.read.public = false;
        rule.read.repo_key = repo_key;
        rule.read.encrypted_to = recipients.to_vec();
        if rule.write.allowed.is_empty() {
            rule.write.allowed = vec!["members".to_string()];
        }
    } else {
        let id = unique_permission_id(pattern, &rules);
        rules.push(PermissionRule {
            id,
            pattern: pattern.to_string(),
            read: ReadPermission {
                public: false,
                repo_key,
                encrypted_to: recipients.to_vec(),
            },
            write: WritePermission {
                allowed: vec!["members".to_string()],
            },
        });
    }
    let mut text = String::from("version: 1\npermissions:\n");
    for rule in rules {
        text.push_str(&format!("  - id: {}\n", yaml_quote(&rule.id)));
        text.push_str(&format!("    pattern: {}\n", yaml_quote(&rule.pattern)));
        if !rule.write.allowed.is_empty() {
            text.push_str("    write:\n      allowed:\n");
            for allowed in rule.write.allowed {
                text.push_str(&format!("        - {}\n", yaml_quote(&allowed)));
            }
        }
        text.push_str("    read:\n");
        if rule.read.public {
            text.push_str("      public: true\n");
        }
        if rule.read.repo_key {
            text.push_str("      repo_key: true\n");
        }
        if !rule.read.encrypted_to.is_empty() {
            text.push_str("      encrypted_to:\n");
            for recipient in rule.read.encrypted_to {
                text.push_str(&format!("        - {}\n", yaml_quote(&recipient)));
            }
        }
    }
    write_if_missing_or_forced(&permissions_path, &text, true)
}

fn unique_permission_id(pattern: &str, rules: &[PermissionRule]) -> String {
    let base = permission_id_from_pattern(pattern);
    if !rules.iter().any(|rule| rule.id == base) {
        return base;
    }
    let mut index = 2usize;
    loop {
        let candidate = format!("{base}_{index}");
        if !rules.iter().any(|rule| rule.id == candidate) {
            return candidate;
        }
        index += 1;
    }
}

fn permission_id_from_pattern(pattern: &str) -> String {
    let mut id = String::from("perm");
    for ch in pattern.chars() {
        if ch.is_ascii_alphanumeric() {
            id.push(ch.to_ascii_lowercase());
        } else if !id.ends_with('_') {
            id.push('_');
        }
    }
    id.trim_end_matches('_').to_string()
}

fn git_config(root: &Path, key: &str, value: &str) -> RhoResult<()> {
    let status = Command::new("git")
        .arg("-C")
        .arg(root)
        .args(["config", "--local", key, value])
        .status()?;
    if !status.success() {
        return Err(format!("git config failed for {key}").into());
    }
    Ok(())
}

fn git_add_relative(root: &Path, relative_path: &str) -> RhoResult<()> {
    let status = Command::new("git")
        .arg("-C")
        .arg(root)
        .arg("add")
        .arg("--")
        .arg(relative_path)
        .status()?;
    if !status.success() {
        return Err(format!("git add failed for {relative_path}").into());
    }
    Ok(())
}

fn git_add_path(root: &Path, path: &Path) -> RhoResult<()> {
    let relative = path
        .strip_prefix(root)
        .map(|path| path.display().to_string())
        .unwrap_or_else(|_| path.display().to_string());
    git_add_relative(root, &relative)
}

fn install_pre_commit_hook(root: &Path) -> RhoResult<()> {
    let hook = root.join(".git/hooks/pre-commit");
    ensure_parent(&hook)?;
    let content = r#"#!/usr/bin/env bash
set -euo pipefail

failed=0
while IFS= read -r -d '' path; do
  attr="$(git check-attr filter -- "$path" | sed 's/^.*: filter: //')"
  if [[ "$attr" == "rho-crypt" ]]; then
    if ! git show ":$path" | grep -Eq "kind: rho_(transparent_file|recipient_envelope)"; then
      echo "rho: protected path is staged without rho encryption: $path" >&2
      failed=1
    fi
  fi
done < <(git diff --cached --name-only -z)

if [[ "$failed" -ne 0 ]]; then
  echo "rho: run git add again after configuring rho repo protect-path, or use rho crypto encrypt for explicit envelopes." >&2
  exit 1
fi
"#;
    fs::write(&hook, content)?;
    let status = Command::new("chmod").arg("+x").arg(&hook).status()?;
    if !status.success() {
        return Err(format!("failed to chmod hook: {}", hook.display()).into());
    }
    Ok(())
}

fn shell_quote(value: &str) -> String {
    format!("'{}'", value.replace('\'', "'\\''"))
}

fn repeated_arg(args: &[String], flag: &str) -> Vec<String> {
    args.windows(2)
        .filter(|window| window[0] == flag)
        .map(|window| window[1].clone())
        .collect()
}

fn write_tool(
    root: &Path,
    id: &str,
    action_type: &str,
    owner: &str,
    approval_required: bool,
    force: bool,
) -> RhoResult<()> {
    let path = root.join("rho/tools").join(format!("{id}.yaml"));
    let manifest = ToolManifest {
        version: 1,
        tool: Tool {
            id: id.to_string(),
            action_type: action_type.to_string(),
            owner: owner.to_string(),
            approval_required,
            command_template: default_tool_command_template(action_type),
        },
    };
    write_if_missing_or_forced(&path, &to_yaml(&manifest)?, force)
}

fn default_tool_command_template(action_type: &str) -> Vec<String> {
    match action_type {
        "run_mock_data" | "run_real_data" => vec![
            "python3".to_string(),
            "CODE_PATH".to_string(),
            "DATASET_CSV".to_string(),
        ],
        _ => Vec::new(),
    }
}

fn write_if_missing_or_forced(path: &Path, content: &str, force: bool) -> RhoResult<()> {
    if path.exists() && !force {
        return Ok(());
    }
    ensure_parent(path)?;
    fs::write(path, content)?;
    Ok(())
}

fn validate_repo_id(value: &str) -> RhoResult<()> {
    if !value.starts_with("rho://repo/") {
        return Err(format!("repo id must start with rho://repo/: {value}").into());
    }
    if value.contains('\0') || value.contains("..") {
        return Err(format!("invalid repo id: {value}").into());
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn validates_repo_ids() {
        assert!(validate_repo_id("rho://repo/local/example").is_ok());
        assert!(validate_repo_id("rho://id/github/user").is_err());
        assert!(validate_repo_id("rho://repo/../bad").is_err());
    }

    #[test]
    fn shell_quotes_filter_commands() {
        assert_eq!(shell_quote("/tmp/rho"), "'/tmp/rho'");
        assert_eq!(shell_quote("/tmp/rho's bin"), "'/tmp/rho'\\''s bin'");
    }

    #[test]
    fn generated_gitattributes_patterns_match_policy_matcher() {
        let permissions = PermissionsManifest {
            permissions: vec![
                PermissionRule {
                    id: "owner-inbox".to_string(),
                    pattern: "rho/messages/inbox/id/github/*/req-*/request.yaml".to_string(),
                    read: ReadPermission {
                        public: false,
                        repo_key: false,
                        encrypted_to: vec!["rho://id/github/rho-owner".to_string()],
                    },
                    write: WritePermission::default(),
                },
                PermissionRule {
                    id: "nested-results".to_string(),
                    pattern: "rho/messages/**/result.*".to_string(),
                    read: ReadPermission {
                        public: false,
                        repo_key: false,
                        encrypted_to: vec!["rho://id/github/rho-collab".to_string()],
                    },
                    write: WritePermission::default(),
                },
                PermissionRule {
                    id: "repo-key-secret".to_string(),
                    pattern: "secrets/**".to_string(),
                    read: ReadPermission {
                        public: false,
                        repo_key: true,
                        encrypted_to: Vec::new(),
                    },
                    write: WritePermission::default(),
                },
                PermissionRule {
                    id: "public-docs".to_string(),
                    pattern: "docs/**".to_string(),
                    read: ReadPermission {
                        public: true,
                        repo_key: false,
                        encrypted_to: Vec::new(),
                    },
                    write: WritePermission::default(),
                },
            ],
        };
        let generated = generated_gitattributes_block(&permissions);
        let generated_patterns = generated
            .lines()
            .filter(|line| line.contains("filter=rho-crypt"))
            .map(|line| line.split_whitespace().next().unwrap())
            .collect::<Vec<_>>();

        assert_eq!(
            generated_patterns,
            vec![
                "rho/messages/**/result.*",
                "rho/messages/inbox/id/github/*/req-*/request.yaml",
                "secrets/**"
            ]
        );
        assert!(generated_patterns.iter().any(|pattern| {
            path_matches_pattern(
                "rho/messages/inbox/id/github/rho-owner/req-123/request.yaml",
                pattern,
            )
        }));
        assert!(generated_patterns.iter().any(|pattern| {
            path_matches_pattern(
                "rho/messages/inbox/id/github/rho-owner/req-123/result.yaml",
                pattern,
            )
        }));
        assert!(!generated_patterns.iter().any(|pattern| {
            path_matches_pattern(
                "rho/messages/inbox/id/github/rho-owner/req-123/notes/request.yaml",
                pattern,
            )
        }));
        assert!(
            !generated_patterns
                .iter()
                .any(|pattern| path_matches_pattern("docs/private.md", pattern))
        );
        assert!(
            generated_patterns
                .iter()
                .any(|pattern| path_matches_pattern("secrets/note.txt", pattern))
        );
    }
}
