forge-orchestrator 1.8.0

Orchestration for Claude Code on shared repos, with Codex CLI and Antigravity CLI (agy) as task runners — single Rust binary, zero deps.
Documentation
use std::path::Path;

use crate::core::state::StateManager;

/// Execute `forge config` subcommands
pub fn execute(project_root: &Path, key: &str, value: &str) -> anyhow::Result<()> {
    let forge_dir = project_root.join(".forge");
    if !forge_dir.exists() {
        anyhow::bail!("Forge is not initialized. Run `forge init` first.");
    }

    let state_mgr = StateManager::new(&forge_dir);
    let mut state = state_mgr.load()?;

    match key {
        "brain" => {
            match value {
                "rule-based" | "openai" => {
                    state.brain.provider = value.to_string();
                    state.updated_at = chrono::Utc::now();
                    state_mgr.save(&state)?;
                    println!("✓ Brain provider set to: {value}");

                    if value == "openai" {
                        // Check for API key
                        if std::env::var("OPENAI_API_KEY").is_err() {
                            println!("  ⚠ OPENAI_API_KEY not found in environment.");
                            println!("  → Add it to .env or export it in your shell.");
                        } else {
                            println!("  ✓ OPENAI_API_KEY found in environment.");
                        }
                    }
                }
                _ => {
                    anyhow::bail!("Unknown brain provider: {value}. Choose: rule-based, openai");
                }
            }
        }
        "brain.model" => {
            state.brain.model = Some(value.to_string());
            state.updated_at = chrono::Utc::now();
            state_mgr.save(&state)?;
            println!("✓ Brain model set to: {value}");
        }
        "claude.auth" | "codex.auth" | "gemini.auth" => {
            let agent = key.split('.').next().unwrap();
            match value {
                "subscription" | "api" => {
                    state
                        .agent_auth
                        .insert(agent.to_string(), value.to_string());
                    state.updated_at = chrono::Utc::now();
                    state_mgr.save(&state)?;
                    println!("✓ {agent} auth mode set to: {value}");
                    match value {
                        "subscription" => {
                            println!(
                                "  \u{2192} Will use CLI subscription (API keys stripped from subprocess)"
                            );

                            // DX-038: Risk warning based on RESEARCH-001
                            if agent == "claude" {
                                println!();
                                println!(
                                    "  \u{26A0}\u{26A0}\u{26A0}  WARNING: SUBSCRIPTION RISK  \u{26A0}\u{26A0}\u{26A0}"
                                );
                                println!(
                                    "  Anthropic ACTIVELY BLOCKS subscription-based CLI orchestration."
                                );
                                println!(
                                    "  In January 2026, accounts were banned for using third-party"
                                );
                                println!(
                                    "  tools with subscription OAuth tokens (ToS Section D.4)."
                                );
                                println!(
                                    "  \u{2192} STRONGLY RECOMMENDED: Use `forge config claude.auth api` instead."
                                );
                                println!(
                                    "  \u{2192} See: docs/research/cli-subscription-gating-analysis.md"
                                );
                                println!();
                                println!(
                                    "  To use subscription mode in dashboard/start, you must pass:"
                                );
                                println!("  `forge dashboard --i-accept-subscription-risk`");
                            } else if agent == "codex" {
                                println!();
                                println!(
                                    "  \u{26A0} CAUTION: Codex subscription has 10-60 cloud task/5h limits."
                                );
                                println!("  Consider API key mode for heavier workloads.");
                            }
                        }
                        "api" => {
                            println!("  \u{2192} Will pass API keys to subprocess");
                        }
                        _ => {}
                    }
                }
                _ => {
                    anyhow::bail!(
                        "Unknown auth mode: {value}. Choose: subscription, api\n\n  subscription — Use CLI subscription (Pro/Max). Strips API keys from subprocess\n  api          — Pass API keys through to subprocess"
                    );
                }
            }
        }
        "claude.permissions" | "codex.permissions" | "gemini.permissions" => {
            let agent = key.split('.').next().unwrap();
            match value {
                "safe" | "yolo" => {
                    state
                        .agent_permissions
                        .insert(agent.to_string(), value.to_string());
                    state.updated_at = chrono::Utc::now();
                    state_mgr.save(&state)?;
                    println!("✓ {agent} permissions set to: {value}");
                    match value {
                        "safe" => {
                            println!(
                                "  → Read-only mode (agent cannot write files or run commands)"
                            );
                        }
                        "yolo" => {
                            println!(
                                "  → Full autonomy (agent can read, write, edit, and execute)"
                            );
                            println!("  ⚡ YOLO MODE ACTIVATED for {agent}");
                        }
                        _ => {}
                    }
                }
                _ => {
                    anyhow::bail!(
                        "Unknown permission mode: {value}. Choose: safe, yolo\n\n  safe — Read-only (agent can analyze but not modify)\n  yolo — Full autonomy (agent can read, write, edit, execute)"
                    );
                }
            }
        }
        "scheduler.rotation" => match value.to_lowercase().as_str() {
            "true" | "on" | "yes" | "enabled" => {
                state.scheduler.rotation = true;
                state.updated_at = chrono::Utc::now();
                state_mgr.save(&state)?;
                println!("✓ scheduler.rotation = enabled");
                println!("  → Rate-limited tasks will be reassigned to available providers");
            }
            "false" | "off" | "no" | "disabled" => {
                state.scheduler.rotation = false;
                state.updated_at = chrono::Utc::now();
                state_mgr.save(&state)?;
                println!("✓ scheduler.rotation = disabled");
                println!("  → Tasks stay with their assigned provider (default)");
            }
            _ => {
                anyhow::bail!("Invalid value: {value}. Use: enabled / disabled");
            }
        },
        "scheduler.pacing" => {
            // Format: "min-max" e.g. "64-179"
            let parts: Vec<&str> = value.split('-').collect();
            if parts.len() != 2 {
                anyhow::bail!("Invalid format: {value}. Use: MIN-MAX (e.g. 64-179)");
            }
            let min: u64 = parts[0]
                .parse()
                .map_err(|_| anyhow::anyhow!("Invalid min: {}", parts[0]))?;
            let max: u64 = parts[1]
                .parse()
                .map_err(|_| anyhow::anyhow!("Invalid max: {}", parts[1]))?;
            if min >= max || min < 10 {
                anyhow::bail!("Min must be >= 10 and less than max. Got: {min}-{max}");
            }
            state.scheduler.pacing_min_secs = min;
            state.scheduler.pacing_max_secs = max;
            state.updated_at = chrono::Utc::now();
            state_mgr.save(&state)?;
            println!("✓ Subscription pacing set to: {min}-{max}s");
            println!("  → Random delay of {min}-{max}s between tasks in subscription mode");
        }
        "dashboard.mode" => match value {
            "pty" | "piped" => {
                state.dashboard_mode = value.to_string();
                state.updated_at = chrono::Utc::now();
                state_mgr.save(&state)?;
                println!("✓ dashboard.mode = {value}");
                match value {
                    "pty" => {
                        println!("  → Stargate PTY mode: agents render with full terminal colors");
                    }
                    "piped" => {
                        println!("  → Legacy piped mode: plain text agent output (default)");
                    }
                    _ => {}
                }
            }
            _ => {
                anyhow::bail!(
                    "Unknown dashboard mode: {value}. Choose: pty, piped\n\n  pty    — Stargate PTY mode with colors and interactivity\n  piped  — Legacy piped mode with plain text (default)"
                );
            }
        },
        "git.auto_commit" => match value.to_lowercase().as_str() {
            "true" | "on" | "yes" => {
                state.git.auto_commit = true;
                state.updated_at = chrono::Utc::now();
                state_mgr.save(&state)?;
                println!("✓ git.auto_commit = true");
                println!("  → Agents will commit after each task completion");
            }
            "false" | "off" | "no" => {
                state.git.auto_commit = false;
                state.updated_at = chrono::Utc::now();
                state_mgr.save(&state)?;
                println!("✓ git.auto_commit = false");
                println!("  → No automatic commits — you manage git manually");
            }
            _ => {
                anyhow::bail!("Invalid value: {value}. Use: true / false");
            }
        },
        _ => {
            anyhow::bail!(
                "Unknown config key: {key}\n\nAvailable keys:\n  brain                — Brain provider (rule-based, openai)\n  brain.model          — Model name (gpt-4o, gpt-4.1, gpt-5, gpt-5-mini)\n  claude.auth          — Claude auth mode (subscription, api)\n  codex.auth           — Codex auth mode (subscription, api)\n  gemini.auth          — Antigravity CLI (agy) auth mode (subscription, api)\n  claude.permissions   — Claude permission mode (safe, yolo)\n  codex.permissions    — Codex permission mode (safe, yolo)\n  gemini.permissions   — Antigravity CLI (agy) permission mode (safe, yolo)\n  scheduler.rotation   — Rotate tasks on rate limit (enabled, disabled)\n  scheduler.pacing     — Subscription delay range (e.g. 64-179)\n  git.auto_commit      — Auto-commit after task completion (true, false)\n  dashboard.mode       — Dashboard rendering mode (pty, piped)"
            );
        }
    }

    Ok(())
}

/// Show current config
pub fn show(project_root: &Path) -> anyhow::Result<()> {
    let forge_dir = project_root.join(".forge");
    if !forge_dir.exists() {
        anyhow::bail!("Forge is not initialized. Run `forge init` first.");
    }

    let state_mgr = StateManager::new(&forge_dir);
    let state = state_mgr.load()?;

    println!("FORGE CONFIGURATION");
    println!("========================================");
    println!();
    println!("  brain          = {}", state.brain.provider);
    println!(
        "  brain.model    = {}",
        state.brain.model.as_deref().unwrap_or("(default)")
    );
    println!("  project        = {}", state.project_name);
    println!(
        "  tools          = {}",
        state
            .tools
            .iter()
            .map(|t| t.name.clone())
            .collect::<Vec<_>>()
            .join(", ")
    );
    println!();

    // Check API key status
    if state.brain.provider == "openai" {
        if std::env::var("OPENAI_API_KEY").is_ok() {
            println!("  ✓ OPENAI_API_KEY: set");
        } else {
            println!("  ⚠ OPENAI_API_KEY: NOT set — brain will fall back to rule-based");
        }
        println!();
    }

    // Agent config
    println!("  Agent Configuration:");
    for agent in ["claude", "codex", "gemini"] {
        let auth = state
            .agent_auth
            .get(agent)
            .cloned()
            .unwrap_or_else(|| "subscription".to_string());
        let perms = state
            .agent_permissions
            .get(agent)
            .cloned()
            .unwrap_or_else(|| "safe".to_string());
        let perms_icon = if perms == "yolo" { "⚡" } else { "🔒" };
        println!("    {agent:8} auth={auth:13} permissions={perms} {perms_icon}",);
    }
    println!();

    // Scheduler config
    println!("  Scheduler:");
    let rotation_icon = if state.scheduler.rotation {
        "✓"
    } else {
        "✗"
    };
    println!(
        "    rotation     = {} {rotation_icon}",
        state.scheduler.rotation
    );
    println!(
        "    pacing       = {}-{}s (subscription mode)",
        state.scheduler.pacing_min_secs, state.scheduler.pacing_max_secs
    );
    println!();

    // Git config
    println!("  Git:");
    let auto_icon = if state.git.auto_commit { "✓" } else { "✗" };
    println!("    auto_commit  = {} {auto_icon}", state.git.auto_commit);
    println!("    strategy     = {}", state.git.strategy);
    println!();

    // Dashboard config
    let mode_icon = if state.dashboard_mode == "pty" {
        "⚡"
    } else {
        "📄"
    };
    println!("  Dashboard:");
    println!("    mode         = {} {mode_icon}", state.dashboard_mode);
    println!();

    // Usage hints (DX-006)
    println!("To change settings:");
    println!("  forge config brain openai          # Switch to OpenAI brain");
    println!("  forge config brain rule-based       # Switch to rule-based brain");
    println!("  forge config brain.model gpt-4.1    # Set model (gpt-4.1, gpt-5, gpt-5-mini)");
    println!("  forge config claude.auth api        # Use API key for Claude");
    println!("  forge config claude.auth subscription  # Use Pro/Max subscription");
    println!("  forge config claude.permissions yolo   # Full autonomy for Claude");
    println!("  forge config claude.permissions safe   # Read-only (default)");
    println!("  forge config git.auto_commit true      # Commit after each task");
    println!("  forge config git.auto_commit false     # Disable auto-commit");

    Ok(())
}