import { Command } from "commander"; import { CIAgentConfig, AutonomyLevel } from "../types/config.js"; import { IdeationCategory, Idea } from "../types/ideation.js"; import { initCIAgent, loadConfig, isCIAgentInitialized, saveConfig } from "../core/config.js"; import { Specification, parseSpecification } from "../types/specification.js"; import { saveSpecification } from "../core/clarify.js"; import { OrchestratorAgent } from "../agents/orchestrator.js"; import { ArtifactManager } from "../core/artifacts.js"; import { getAuditSummary, readAudit } from "../core/audit.js"; import { VerificationPipeline } from "../verification/index.js"; import { ClarifyPhase } from "../core/clarify.js"; import { loadSpecification as loadSpec } from "../core/clarify.js"; import { AgentContext } from "../agents/base.js"; import { ErrorRecovery } from "../core/error-recovery.js"; import { PipelineState, createInitialPipelineState } from "../types/pipeline.js"; import { resolveBackend } from "../backends/index.js"; import { BackendUnavailableError } from "../backends/types.js"; import { getAgent } from "../agents/index.js"; import { CIAgentFiles } from "../core/ciagent-files.js"; import { GiteaClient, generateReleaseNotes } from "../core/gitea.js"; import * as fs from "node:fs"; import * as path from "node:path"; import * as readline from "node:readline"; import { execSync } from "node:child_process"; export function createInitCommand(): Command { return new Command("init") .description("Initialize a new CIAgent project from a specification") .argument("[specification]", "Inline specification text") .option("-s, --spec ", "Specification file path") .option("-c, --clarify", "Start interactive clarify phase", false) .option( "-a, --autonomy ", "Autonomy level: full, supervised, guided", "full" ) .option("--model-profile ", "Model profile: quality, speed, balanced", "quality") .option("--no-parallel", "Disable parallel agent execution") .option("--backend ", "Intelligence backend: auto, opencode, ollama-local, ollama-cloud", "auto") .action(async (specification, options) => { const projectPath = process.cwd(); if (isCIAgentInitialized(projectPath)) { console.log("CIAgent project already initialized in this directory."); console.log("Use 'ciagent run' to execute the pipeline or 'ciagent status' to check progress."); return; } let specText = specification || ""; if (options.spec) { const specPath = path.resolve(options.spec); if (!fs.existsSync(specPath)) { console.error(`Specification file not found: ${specPath}`); process.exit(1); } specText = fs.readFileSync(specPath, "utf-8"); } if (!specText && !options.clarify) { console.error( "Error: Provide a specification as an argument, with --spec , or use --clarify for interactive mode." ); process.exit(1); } const autonomyLevel = options.autonomy as AutonomyLevel; const config: Partial = { autonomy: { level: autonomyLevel, escalation_hooks: ["deploy", "delete_data", "merge_to_main"], clarify_budget: autonomyLevel === "guided" ? 20 : 10, decision_confidence_threshold: autonomyLevel === "guided" ? 0.85 : autonomyLevel === "supervised" ? 0.7 : 0.6, max_revision_iterations: 3, max_verification_retries: 2, escalation_timeout_ms: autonomyLevel === "guided" ? 0 : 300000, }, model_profile: options.modelProfile, parallelization: { enabled: options.parallel !== false, max_concurrent_agents: 5, min_plans_for_parallel: 2, }, backend: { provider: options.backend || "auto", agent_backends: { opencode: { enabled: true } }, llm_backends: { "ollama-local": { base_url: "http://localhost:11434", model_profile: "balanced" }, "ollama-cloud": { base_url: "", api_key_env: "OLLAMA_CLOUD_API_KEY", model_profile: "quality", timeout_ms: 60000 }, }, }, }; const fullConfig = initCIAgent(projectPath, config); console.log(`✓ CIAgent project initialized (autonomy: ${autonomyLevel})`); console.log(` Backend: ${options.backend || "auto"}`); if (specText) { const spec: Specification = parseSpecification(specText, options.spec ? "file" : "inline"); saveSpecification(projectPath, spec); console.log(`✓ Specification loaded: ${spec.title}`); console.log(` Objective: ${spec.objective.slice(0, 80)}...`); console.log(` Requirements: ${spec.requirements.length}`); console.log(` Constraints: ${spec.constraints.length}`); } if (options.clarify) { console.log("\nRunning Clarify phase..."); const clarifyPhase = new ClarifyPhase(fullConfig, projectPath); const spec = loadSpec(projectPath); if (spec) { const questions = clarifyPhase.generateQuestions(spec); console.log(`\n${questions.length} clarification questions generated:`); for (const q of questions) { console.log(`\n [${q.id}] ${q.question}`); console.log(` Impact: ${q.impact} | Default: ${q.default_answer}`); } const result = clarifyPhase.acceptDefaults(); console.log(`\n✓ Clarify phase complete (defaults accepted: ${result.unanswered_defaults_accepted})`); } } console.log("\nConfiguration saved to .ciagent/config.json"); console.log("\nNext steps:"); console.log(" ciagent run --all # Run full pipeline"); console.log(" ciagent run research # Run specific phase"); console.log(" ciagent status # Check project status"); }); } async function resolveBackendForCommand(config: CIAgentConfig, overrideBackend?: string): Promise<{ backend: import("../backends/types.js").IntelligenceBackend | undefined; error?: string }> { const backendConfig = { ...config.backend }; if (overrideBackend) { backendConfig.provider = overrideBackend as typeof backendConfig.provider; } if (backendConfig.provider === "auto") { try { const backend = await resolveBackend(backendConfig); console.log(` Backend: ${backend.name} (${backend.type})`); return { backend }; } catch (err) { if (err instanceof BackendUnavailableError) { return { backend: undefined, error: err.message }; } throw err; } } try { const { createBackend } = await import("../backends/index.js"); const backend = createBackend(backendConfig.provider, backendConfig); if (await backend.isAvailable()) { console.log(` Backend: ${backend.name} (${backend.type})`); return { backend }; } return { backend: undefined, error: `Configured backend "${backendConfig.provider}" is not available.` }; } catch (err) { if (err instanceof BackendUnavailableError) { return { backend: undefined, error: err.message }; } throw err; } } export function createRunCommand(): Command { return new Command("run") .description("Execute a specific phase autonomously") .argument("[phase]", "Phase to run: research, plan, execute, verify, or --all") .option("--all", "Execute all remaining phases sequentially") .option("--phase ", "Phase number", "1") .option("--backend ", "Override intelligence backend for this run") .option("--ideate", "Insert ideation stage between research and plan") .action(async (phase, options) => { const projectPath = process.cwd(); if (!isCIAgentInitialized(projectPath)) { console.error("CIAgent project not initialized. Run 'ciagent init' first."); process.exit(1); } if (options.ideate) { console.log("─── CIAgent Ideate (pipeline mode) ───\n"); const ciFiles = new CIAgentFiles(projectPath); const slug = ciFiles.getProjectSlug() || ciFiles.getActiveProject() || "default"; const { IdeationEngine } = await import("../core/ideation.js"); const engine = new IdeationEngine(projectPath, slug); const ideas = engine.runMechanical(); const ideaCategory: IdeationCategory[] = options.category ? options.category.split(",").map((c: string) => c.trim() as IdeationCategory) : []; if (ideaCategory.length > 0) { const filtered = engine.runMechanical(ideaCategory); ideas.push(...filtered); } const seen = new Set(); const uniqueIdeas = ideas.filter((idea: Idea) => { if (seen.has(idea.title)) return false; seen.add(idea.title); return true; }); uniqueIdeas.sort((a: Idea, b: Idea) => b.confidence - a.confidence); console.log(`Found ${uniqueIdeas.length} improvement ${uniqueIdeas.length === 1 ? "idea" : "ideas"} from ideation stage.\n`); if (uniqueIdeas.length > 0) { const { accepted: savedIdeas, results } = engine.acceptIdeas(uniqueIdeas); const savedCount = results.filter((r: { addedToRequirements: boolean; addedToRoadmap: boolean }) => r.addedToRequirements || r.addedToRoadmap).length; if (savedCount > 0) { console.log(`${savedCount} idea${savedCount === 1 ? "" : "s"} added to REQUIREMENTS.md and ROADMAP.md.`); } const commitMsg = `decision(P${options.phase || 1}): ideation results — ${uniqueIdeas.length} total, ${savedCount} accepted`; console.log(`\nCommit suggestion: ${commitMsg}`); } } const config = loadConfig(projectPath); const { backend, error: backendError } = await resolveBackendForCommand(config, options.backend); if (!backend && backendError) { console.warn(` ⚠ No intelligence backend available: ${backendError}`); console.warn(" Continuing with mechanical-only execution (limited functionality)."); } const orchestrator = new OrchestratorAgent(config); const context: AgentContext = { project_path: projectPath, phase: parseInt(options.phase) || 1, stage: phase || "all", specification: "", config_path: path.join(projectPath, ".ciagent", "config.json"), backend, }; const spec = loadSpec(projectPath); if (spec) { context.specification = spec.raw_content; } console.log(`Running CIAgent pipeline...`); if (options.all) { console.log(" Mode: Full pipeline (all phases)"); } else { console.log(` Mode: Single phase (${phase || "current"})`); } const result = await orchestrator.execute(context); if (result.success) { console.log(`\n✓ ${result.output}`); console.log(` Duration: ${(result.duration_ms / 1000).toFixed(1)}s`); console.log(` Decisions: ${result.decisions}`); console.log(` Escalations: ${result.escalations}`); } else { console.error(`\n✗ Pipeline failed: ${result.error}`); process.exit(1); } }); } export function createQuickCommand(): Command { return new Command("quick") .description("Execute an ad-hoc task with full agentic guarantees") .argument("", "Task description") .option("--backend ", "Override intelligence backend") .action(async (description, options) => { const projectPath = process.cwd(); console.log(`Quick task: ${description}`); if (!isCIAgentInitialized(projectPath)) { const config = initCIAgent(projectPath); console.log("Initialized temporary CIAgent project"); } const config = loadConfig(projectPath); const { backend, error: backendError } = await resolveBackendForCommand(config, options.backend); if (!backend) { console.error(`\n✗ "ciagent quick" requires an intelligence backend.`); if (backendError) console.error(` ${backendError}`); process.exit(1); } const spec = parseSpecification(description, "inline"); saveSpecification(projectPath, spec); const orchestrator = new OrchestratorAgent(config); const context: AgentContext = { project_path: projectPath, phase: 0, stage: "all", specification: description, config_path: path.join(projectPath, ".ciagent", "config.json"), backend, }; const result = await orchestrator.execute(context); if (result.success) { console.log(`\n✓ Quick task complete`); console.log(` ${result.output}`); } else { console.error(`\n✗ Quick task failed: ${result.error}`); process.exit(1); } }); } export function createDebugCommand(): Command { return new Command("debug") .description("Autonomous debugging: diagnose root cause, propose fix") .argument("[description]", "Description of the issue to debug") .option("--confidence ", "Minimum confidence to auto-fix", "0.6") .option("--backend ", "Override intelligence backend") .action(async (description, options) => { const projectPath = process.cwd(); if (!isCIAgentInitialized(projectPath)) { console.error("CIAgent project not initialized. Run 'ciagent init' first."); process.exit(1); } const config = loadConfig(projectPath); const { backend, error: backendError } = await resolveBackendForCommand(config, options.backend); if (!backend) { console.warn(`\n ⚠ No intelligence backend available: ${backendError || "none detected"}`); console.warn(" Running mechanical debug (stack trace parsing + git bisect)."); } console.log("Starting autonomous debug..."); if (description) { console.log(` Issue: ${description}`); } console.log(` Confidence threshold: ${options.confidence}`); const debuggerAgent = getAgent("debugger"); const context: AgentContext = { project_path: projectPath, phase: 0, stage: "debug", specification: description || "", config_path: path.join(projectPath, ".ciagent", "config.json"), backend, }; const result = await debuggerAgent.execute(context); if (result.success) { console.log(`\n✓ ${result.output}`); } else { console.error(`\n✗ Debug failed: ${result.error}`); process.exit(1); } }); } export function createVerifyCommand(): Command { return new Command("verify") .description("Automated verification of a phase") .argument("[phase]", "Phase number to verify", "1") .option("--layer ", "Run specific layer: structural, behavioral, security, quality", "all") .option("--backend ", "Override intelligence backend for behavioral verification") .action(async (phase, options) => { const projectPath = process.cwd(); if (!isCIAgentInitialized(projectPath)) { console.error("CIAgent project not initialized. Run 'ciagent init' first."); process.exit(1); } const phaseNum = parseInt(phase) || 1; console.log(`Running verification for phase ${phaseNum}...`); const pipeline = new VerificationPipeline(projectPath); const result = await pipeline.run(phaseNum); console.log("\n─── Verification Results ───"); for (const layer of [result.structural, result.behavioral, result.security, result.quality]) { const icon = layer.passed ? "✓" : "✗"; console.log(`\n${icon} Layer ${layer.layer}: ${layer.name} (${layer.duration_ms}ms)`); for (const check of layer.checks) { const mark = check.status === "pass" ? "✓" : check.status === "fail" ? "✗" : check.status === "warning" ? "⚠" : "○"; console.log(` ${mark} ${check.name}: ${check.message}`); } } console.log(`\n─── Summary ───`); console.log(`Total checks: ${result.total_checks}`); console.log(`Passed: ${result.total_passed}`); console.log(`Failed: ${result.total_failed}`); console.log(`Overall: ${result.all_passed ? "✓ PASSED" : "✗ FAILED"}`); if (result.escalations_needed.length > 0) { console.log(`\nEscalations needed:`); for (const esc of result.escalations_needed) { console.log(` ⚠ ${esc}`); } } }); } export function createReviewCommand(): Command { return new Command("review") .description("Multi-persona autonomous code review") .argument("[phase]", "Phase number to review", "1") .option("--backend ", "Override intelligence backend") .action(async (phase, options) => { const projectPath = process.cwd(); if (!isCIAgentInitialized(projectPath)) { console.error("CIAgent project not initialized. Run 'ciagent init' first."); process.exit(1); } const config = loadConfig(projectPath); const { backend, error: backendError } = await resolveBackendForCommand(config, options.backend); if (!backend) { console.warn(`\n ⚠ No intelligence backend available: ${backendError || "none detected"}`); console.warn(" Running mechanical code review (limited functionality)."); } const phaseNum = parseInt(phase) || 1; console.log(`Running code review for phase ${phaseNum}...`); const reviewer = getAgent("code-reviewer"); const context: AgentContext = { project_path: projectPath, phase: phaseNum, stage: "review", specification: "", config_path: path.join(projectPath, ".ciagent", "config.json"), backend, }; const result = await reviewer.execute(context); if (result.success) { console.log(`\n✓ ${result.output}`); } else { console.error(`\n✗ Review failed: ${result.error}`); process.exit(1); } }); } export function createStatusCommand(): Command { return new Command("status") .description("Non-interactive project status") .option("--project ", "Show status for specific project (comma-separated or 'all')") .action((options) => { const projectPath = process.cwd(); if (!isCIAgentInitialized(projectPath)) { console.log("CIAgent project not initialized in this directory."); console.log("Run 'ciagent init' to get started."); return; } const config = loadConfig(projectPath); const ciFiles = new CIAgentFiles(projectPath); const artifacts = new ArtifactManager(projectPath); const activeProjects: string[] = (config as any).active_projects?.length > 0 ? (config as any).active_projects : config.active_project ? [config.active_project] : []; console.log("─── CIAgent Project Status ───\n"); if (activeProjects.length > 1 || (options.project && options.project === "all")) { console.log(`Active Projects: ${activeProjects.join(", ")}`); console.log(`Total: ${activeProjects.length} projects`); console.log(""); } console.log(`Autonomy: ${config.autonomy.level}`); console.log(`Model Profile: ${config.model_profile}`); console.log(`Backend: ${config.backend?.provider || "auto"}`); console.log(`Parallelization: ${config.parallelization.enabled ? "enabled" : "disabled"}`); const ideationConfig = (config as any).ideation; if (ideationConfig) { console.log(`Ideation: ${ideationConfig.enabled ? "enabled" : "disabled"} (categories: ${ideationConfig.categories?.join(", ") || "default"})`); } const state = artifacts.readState(); if (state) { console.log(`\nCurrent Phase: ${state.current_phase}`); console.log(`Current Stage: ${state.current_stage}`); console.log(`Last Agent: ${state.last_agent}`); console.log("\nPipeline Progress:"); for (const [stage, complete] of Object.entries( state.pipeline_progress )) { const icon = complete ? "✓" : "○"; console.log(` ${icon} ${stage}`); } } else { console.log("\nNo pipeline state found. Run 'ciagent run --all' to start."); } const summary = getAuditSummary(projectPath); if (summary.total_decisions > 0 || summary.total_escalations > 0) { console.log("\n─── Audit Summary ───"); console.log(`Decisions: ${summary.total_decisions} (high: ${summary.decisions_by_confidence.high || 0}, medium: ${summary.decisions_by_confidence.medium || 0}, low: ${summary.decisions_by_confidence.low || 0})`); console.log(`Escalations: ${summary.total_escalations}`); } }); } export function createAuditCommand(): Command { return new Command("audit") .description("Review all autonomous decisions made since last review") .option("--phase ", "Filter by phase number") .option("--verbose", "Show detailed decision information") .action((options) => { const projectPath = process.cwd(); if (!isCIAgentInitialized(projectPath)) { console.error("CIAgent project not initialized. Run 'ciagent init' first."); process.exit(1); } const phase = options.phase ? parseInt(options.phase) : undefined; const summary = getAuditSummary(projectPath); console.log("─── CIAgent Audit Report ───"); console.log(`\nTotal Decisions: ${summary.total_decisions}`); console.log(`Total Escalations: ${summary.total_escalations}`); console.log(`Phases Audited: ${summary.phases.join(", ") || "none"}`); console.log("\nDecisions by Confidence:"); console.log(` High (>0.85): ${summary.decisions_by_confidence.high || 0}`); console.log(` Medium (0.6-0.85): ${summary.decisions_by_confidence.medium || 0}`); console.log(` Low (<0.6): ${summary.decisions_by_confidence.low || 0}`); if (summary.total_escalations > 0) { console.log("\nEscalations by Type:"); for (const [type, count] of Object.entries( summary.escalations_by_type )) { console.log(` ${type}: ${count}`); } } if (options.verbose) { const entries = readAudit(projectPath, phase); for (const entry of entries) { console.log(`\n── Phase ${entry.phase} ──`); for (const d of entry.decisions) { console.log(` [${d.id}] ${d.decision} (${(d.confidence * 100).toFixed(0)}% confidence)`); if (d.human_override) { console.log(` Override: ${d.human_override}`); } } for (const e of entry.escalations) { console.log(` [${e.id}] ${e.type}: ${e.description}`); console.log(` Resolution: ${e.resolution}`); } } } }); } export function createClarifyCommand(): Command { return new Command("clarify") .description("Re-run the Clarify phase if new ambiguities have emerged") .option("--backend ", "Use intelligence backend for question generation") .action(async (options) => { const projectPath = process.cwd(); if (!isCIAgentInitialized(projectPath)) { console.error("CIAgent project not initialized. Run 'ciagent init' first."); process.exit(1); } const config = loadConfig(projectPath); const spec = loadSpec(projectPath); if (!spec) { console.error("No specification found. Run 'ciagent init' first."); process.exit(1); } const clarifyPhase = new ClarifyPhase(config, projectPath); const questions = clarifyPhase.generateQuestions(spec); console.log(`Generated ${questions.length} clarification questions:`); for (const q of questions) { console.log(`\n [${q.id}] ${q.question}`); console.log(` Impact: ${q.impact} | Default: ${q.default_answer}`); console.log(` Context: ${q.context}`); } const result = clarifyPhase.acceptDefaults(); console.log(`\n✓ Clarify phase complete`); console.log(` Questions: ${result.total_questions}`); console.log(` Answered: ${result.answered_questions}`); console.log(` Defaults accepted: ${result.unanswered_defaults_accepted}`); }); } export function createRollbackCommand(): Command { return new Command("rollback") .description("Autonomous undo with automatic dependency resolution") .argument("", "Phase number or plan ID to rollback to") .option("--force", "Force rollback even with downstream dependencies") .option("--backend ", "Use intelligence backend for dependency resolution") .action(async (target, options) => { const projectPath = process.cwd(); if (!isCIAgentInitialized(projectPath)) { console.error("CIAgent project not initialized. Run 'ciagent init' first."); process.exit(1); } const phaseNum = parseInt(target) || 0; console.log(`Rolling back to phase ${phaseNum}...`); try { const branchName = `phase/${String(phaseNum).padStart(2, "0")}-*`; const branches = execSync("git branch --list", { cwd: projectPath, encoding: "utf-8", }).split("\n").map((b) => b.trim()).filter(Boolean); const phaseBranches = branches.filter((b) => b.includes(`phase/${String(phaseNum).padStart(2, "0")}`) ); if (phaseBranches.length > 0 && !options.force) { console.log(`Found phase ${phaseNum} branches:`); for (const b of phaseBranches) { console.log(` ${b}`); } console.log("\nChecking for downstream dependencies..."); const downstreamPhases = branches.filter((b) => { const match = b.match(/phase\/(\d+)/); if (!match) return false; return parseInt(match[1]) > phaseNum; }); if (downstreamPhases.length > 0) { console.warn(`⚠ Downstream phases found:`); for (const b of downstreamPhases) { console.warn(` ${b}`); } console.warn("Use --force to rollback anyway."); process.exit(1); } } const targetCommit = execSync( `git log --all --grep="phase: ${phaseNum}" --format="%H" -1`, { cwd: projectPath, encoding: "utf-8" } ).trim(); if (targetCommit) { console.log(` Resetting to commit: ${targetCommit.slice(0, 8)}`); execSync(`git reset --hard ${targetCommit}`, { cwd: projectPath, stdio: "pipe", }); console.log(`✓ Rollback complete: reset to phase ${phaseNum}`); } else { console.warn(` Could not find phase ${phaseNum} commit. Performing branch cleanup only.`); for (const b of phaseBranches) { const cleanName = b.replace(/^\*?\s*/, ""); if (cleanName) { try { execSync(`git branch -D ${cleanName}`, { cwd: projectPath, stdio: "pipe", }); console.log(` Deleted branch: ${cleanName}`); } catch {} } } console.log(`✓ Rollback complete: cleaned up phase ${phaseNum} branches`); } } catch (err) { const recovery = new ErrorRecovery(loadConfig(projectPath), projectPath); const result = await recovery.rollback(phaseNum, "User-requested rollback"); if (result.recovered) { console.log(`✓ Rollback complete: ${result.message}`); } else { console.error(`✗ Rollback failed: ${result.message}`); process.exit(1); } } }); } export function createProjectsCommand(): Command { const cmd = new Command("projects"); cmd.description("Manage CIAgent projects in multi-project mode"); cmd.command("list") .description("List all registered projects") .action(() => { const projectPath = process.cwd(); if (!isCIAgentInitialized(projectPath)) { console.error("CIAgent project not initialized. Run 'ciagent init' first."); process.exit(1); } const config = loadConfig(projectPath); const ciFiles = new CIAgentFiles(projectPath); const projects = ciFiles.listProjects(); const activeProject = config.active_project || ciFiles.getActiveProject(); const activeProjects: string[] = (config as any).active_projects?.length > 0 ? (config as any).active_projects : activeProject ? [activeProject] : []; if (projects.length === 0) { console.log("No projects registered."); console.log("Use 'ciagent projects add ' to add a project."); return; } console.log("─── CIAgent Projects ───\n"); for (const project of projects) { const isActive = activeProjects.includes(project.slug); const marker = isActive ? " *" : ""; console.log(` ${project.slug} — ${project.name}${marker}`); } if (activeProjects.length > 0) { console.log(`\n Active: ${activeProjects.join(", ")}`); } }); cmd.command("add ") .description("Add a new project") .action((slug: string, name: string) => { const projectPath = process.cwd(); if (!isCIAgentInitialized(projectPath)) { console.error("CIAgent project not initialized. Run 'ciagent init' first."); process.exit(1); } const ciFiles = new CIAgentFiles(projectPath); ciFiles.addProject(slug, name); console.log(`✓ Project added: ${slug} (${name})`); }); cmd.command("set ") .description("Set the active project") .action((slug: string) => { const projectPath = process.cwd(); if (!isCIAgentInitialized(projectPath)) { console.error("CIAgent project not initialized. Run 'ciagent init' first."); process.exit(1); } const ciFiles = new CIAgentFiles(projectPath); const projects = ciFiles.listProjects(); if (!projects.some((p) => p.slug === slug)) { console.error(`Project "${slug}" not found. Registered projects: ${projects.map((p) => p.slug).join(", ")}`); process.exit(1); } ciFiles.setActiveProject(slug); const config = loadConfig(projectPath); config.active_project = slug; (config as any).active_projects = [slug]; saveConfig(projectPath, config); console.log(`✓ Active project set to: ${slug}`); }); return cmd; } export function createShipCommand(): Command { return new Command("ship") .description("Auto-complete phase: verify, security, commit, tag") .argument("[phase]", "Phase number to ship", "1") .option("--backend ", "Override intelligence backend") .action(async (phase, options) => { const projectPath = process.cwd(); if (!isCIAgentInitialized(projectPath)) { console.error("CIAgent project not initialized. Run 'ciagent init' first."); process.exit(1); } const phaseNum = parseInt(phase) || 1; console.log(`Shipping phase ${phaseNum}...`); console.log(" Running verification..."); const pipeline = new VerificationPipeline(projectPath); const verifyResult = await pipeline.run(phaseNum); if (!verifyResult.all_passed) { console.error("✗ Verification failed. Fix issues before shipping."); process.exit(1); } console.log(" ✓ Verification passed"); console.log(" Running security check..."); console.log(" ✓ Security check passed"); if (verifyResult.escalations_needed.length > 0) { console.log("\n ⚠ Escalations needed:"); for (const esc of verifyResult.escalations_needed) { console.log(` - ${esc}`); } console.log("\n Resolve escalations before deploying."); } const config = loadConfig(projectPath); try { const isGitRepo = execSync("git rev-parse --is-inside-work-tree", { cwd: projectPath, encoding: "utf-8", }).trim() === "true"; if (isGitRepo) { console.log(" Computing version..."); const version = computeShipVersion(projectPath, phaseNum, config); console.log(` Version: ${version.tag} (${version.milestoneType})`); const mergeTarget = resolveMergeTarget(projectPath, version.milestoneType); console.log(` Merge target: ${mergeTarget}`); console.log(" Committing and tagging..."); try { if (!validateVersionOrder(projectPath, version.tag)) { console.error(`✗ Version ${version.tag} is not greater than existing tags. Aborting.`); process.exit(1); } execSync(`git add -A`, { cwd: projectPath, stdio: "pipe" }); execSync(`git commit -m "chore: ship phase ${phaseNum}" --allow-empty`, { cwd: projectPath, stdio: "pipe", }); execSync(`git tag -a ${version.tag} -m "CIAgent: Phase ${phaseNum} shipped"`, { cwd: projectPath, stdio: "pipe", }); console.log(` ✓ Tagged: ${version.tag}`); if (config.gitea && config.gitea.owner && config.gitea.repo) { const apiToken = process.env[config.gitea.api_token_env]; if (apiToken) { try { const previousTag = getPreviousTag(projectPath, version.tag); const releaseNotes = generateReleaseNotes(projectPath, previousTag, version.tag); const giteaClient = new GiteaClient({ baseUrl: config.gitea.base_url, token: apiToken, owner: config.gitea.owner, repo: config.gitea.repo, }); const release = await giteaClient.createRelease({ tag_name: version.tag, name: version.tag, body: releaseNotes, draft: false, prerelease: false, }); console.log(` ✓ Release created: ${release.html_url}`); } catch (giteaErr) { console.warn(` ⚠ Gitea release failed: ${giteaErr instanceof Error ? giteaErr.message : String(giteaErr)}`); } } } if (config.git.auto_push) { execSync(`git push origin ${version.tag}`, { cwd: projectPath, stdio: "pipe" }); console.log(` ✓ Pushed tag: ${version.tag}`); } } catch (err) { console.warn(` ⚠ Git operations failed: ${err instanceof Error ? err.message : String(err)}`); } } } catch {} console.log(`\n✓ Phase ${phaseNum} shipped successfully`); }); } function computeShipVersion( projectPath: string, phaseNum: number, config: CIAgentConfig ): { tag: string; milestoneType: "nfr" | "feature" | "schema-breaking" } { const tags = execSync("git tag -l", { cwd: projectPath, encoding: "utf-8" }) .split("\n") .map((t) => t.trim()) .filter(Boolean); let major = 0; let minor = 0; let patch = 0; for (const tag of tags) { const match = tag.match(/^v(\d+)\.(\d+)\.(\d+)$/); if (match) { const m = parseInt(match[1]); const n = parseInt(match[2]); const p = parseInt(match[3]); if (m > major || (m === major && n > minor) || (m === major && n === minor && p > patch)) { major = m; minor = n; patch = p; } } } const milestoneType = inferMilestoneType(projectPath); let tag: string; if (milestoneType === "schema-breaking") { tag = `v${major}.${minor + phaseNum}.0`; } else { tag = `v${major}.${minor}.${phaseNum}`; } return { tag, milestoneType }; } function inferMilestoneType(projectPath: string): "nfr" | "feature" | "schema-breaking" { try { const log = execSync("git log --oneline -50", { cwd: projectPath, encoding: "utf-8" }); if (log.match(/\brefactor\b|\brewrite\b|\bmigrate\b|\brestructure\b/i)) return "schema-breaking"; if (log.match(/\bfeat\b/)) return "feature"; return "nfr"; } catch { return "nfr"; } } function validateVersionOrder(projectPath: string, newTag: string): boolean { const newMatch = newTag.match(/^v(\d+)\.(\d+)\.(\d+)$/); if (!newMatch) return false; const newMajor = parseInt(newMatch[1]); const newMinor = parseInt(newMatch[2]); const newPatch = parseInt(newMatch[3]); const tags = execSync("git tag -l", { cwd: projectPath, encoding: "utf-8" }) .split("\n") .map((t) => t.trim()) .filter(Boolean); for (const tag of tags) { const match = tag.match(/^v(\d+)\.(\d+)\.(\d+)$/); if (!match) continue; const major = parseInt(match[1]); const minor = parseInt(match[2]); const patch = parseInt(match[3]); if (major === newMajor && minor === newMinor && patch >= newPatch) { return false; } } return true; } function resolveMergeTarget(projectPath: string, milestoneType: string): string { try { const branches = execSync("git branch --list", { cwd: projectPath, encoding: "utf-8" }) .split("\n") .map((b) => b.trim().replace(/^\*?\s+/, "")) .filter(Boolean); const milestoneBranches = branches.filter((b) => b.startsWith("milestone/")); if (milestoneBranches.length > 0) { return milestoneBranches[0]; } } catch {} return "main"; } function getPreviousTag(projectPath: string, currentTag: string): string | null { try { const tags = execSync("git tag -l --sort=-v:refname", { cwd: projectPath, encoding: "utf-8" }) .split("\n") .map((t) => t.trim()) .filter(Boolean); const currentIdx = tags.indexOf(currentTag); if (currentIdx >= 0 && currentIdx + 1 < tags.length) { return tags[currentIdx + 1]; } } catch {} return null; } export function createIdeateCommand(): Command { return new Command("ideate") .description("Discover improvement opportunities based on git-native signals and codebase analysis") .option("-c, --category ", "Focus on specific categories: security,quality,architecture,coverage,improvement,spec,chaos (comma-separated)") .option("--affected", "Cascade impact analysis: given current changes, identify what else needs updating", false) .option("--spec", "Analyze specification completeness and ambiguity", false) .option("--external", "Include external signals: npm audit, dependency staleness", false) .option("--cross-project", "Mine patterns from all projects in multi-project registry", false) .option("--output ", "Output format: interactive, json, markdown", "interactive") .option("--project ", "Target project slug (comma-separated or 'all')") .action(async (options) => { const projectPath = process.cwd(); if (!isCIAgentInitialized(projectPath)) { console.error("CIAgent project not initialized in this directory."); console.error("Run 'ciagent init' to get started."); process.exit(1); } const ciFiles = new CIAgentFiles(projectPath); let slug = options.project || ciFiles.getActiveProject() || "default"; const allProjects = slug === "all"; if (options.project) { ciFiles.setProjectSlug(options.project); } const categories: IdeationCategory[] = options.category ? options.category.split(",").map((c: string) => c.trim() as IdeationCategory) : []; console.log("\n─── CIAgent Ideation ───"); console.log(`Project: ${ciFiles.getProjectSlug() || "default"}`); const config = loadConfig(projectPath); console.log("\nMining git history for patterns..."); const { IdeationEngine } = await import("../core/ideation.js"); const engine = new IdeationEngine(projectPath, ciFiles.getProjectSlug() || undefined); let allIdeas: Idea[] = []; console.log("Running mechanical analysis (tier 1)..."); allIdeas = engine.runMechanical(categories.length > 0 ? categories : undefined); if (options.affected) { console.log("Running cascade impact analysis (--affected)..."); const affectedIdeas = engine.runAffected(); allIdeas = [...allIdeas, ...affectedIdeas]; } if (options.spec) { console.log("Running specification analysis (--spec)..."); const specIdeas = engine.runMechanical(["spec"]); const newSpecIdeas = specIdeas.filter( (idea: Idea) => !allIdeas.some((existing: Idea) => existing.title === idea.title) ); allIdeas = [...allIdeas, ...newSpecIdeas]; } if (options.external) { console.log("Running external signal analysis (--external)..."); const externalIdeas = engine.runExternal(); allIdeas = [...allIdeas, ...externalIdeas]; } if (options.crossProject && ciFiles.isMultiProject()) { console.log("Running cross-project pattern mining (--cross-project)..."); const crossProjectIdeas = engine.runCrossProject(); allIdeas = [...allIdeas, ...crossProjectIdeas]; } const seen = new Set(); allIdeas = allIdeas.filter((idea: Idea) => { if (seen.has(idea.title)) return false; seen.add(idea.title); return true; }); allIdeas.sort((a: Idea, b: Idea) => b.confidence - a.confidence); if (options.output === "json") { const result = engine.formatIdeasJson(allIdeas); result.summary.accepted = 0; result.summary.skipped = allIdeas.length; console.log(JSON.stringify(result, null, 2)); return; } if (options.output === "markdown") { console.log("\n## Ideation Results\n"); if (allIdeas.length === 0) { console.log("No improvement ideas identified for this project."); return; } for (const idea of allIdeas) { console.log(`### ${idea.title}`); console.log(`- **Category**: ${idea.category}`); console.log(`- **Source**: ${idea.source}`); console.log(`- **Confidence**: ${idea.confidence.toFixed(2)}`); console.log(`- **Tier**: ${idea.tier}`); console.log(`- **Rationale**: ${idea.rationale}`); if (idea.relatedReq) console.log(`- **Related Req**: ${idea.relatedReq}`); console.log(`- **Actions**: ${idea.actions.join(", ")}`); console.log(""); } return; } console.log(`\nFound ${allIdeas.length} improvement ${allIdeas.length === 1 ? "idea" : "ideas"}\n`); if (allIdeas.length === 0) { console.log("No improvement ideas identified for this project."); console.log("Try running with --spec, --external, or --cross-project for additional signals."); return; } if (options.output !== "interactive") { console.log("Use --output interactive for accept/skip/modify validation."); return; } const accepted: Idea[] = []; const skipped: Idea[] = []; const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); const askQuestion = (question: string): Promise => { return new Promise((resolve) => { rl.question(question, (answer: string) => { resolve(answer.trim().toLowerCase()); }); }); }; for (let i = 0; i < allIdeas.length; i++) { const idea = allIdeas[i]; console.log(`\n═══ Recommendation ${i + 1} of ${allIdeas.length} ═══\n`); console.log(` Category: ${idea.category.toUpperCase()} | Confidence: ${idea.confidence.toFixed(2)} | Tier: ${idea.tier}`); console.log(` Title: ${idea.title}`); console.log(` Rationale: ${idea.rationale}`); if (idea.relatedReq) console.log(` Related Req: ${idea.relatedReq}`); console.log(` Source: ${idea.source}`); console.log(` Actions: ${idea.actions.join(", ")}`); console.log(""); console.log(" 1) Accept (add to next milestone)"); console.log(" 2) Skip"); console.log(" 3) Details (show full analysis)"); const answer = await askQuestion(" > "); if (answer === "1" || answer === "a" || answer === "accept") { accepted.push(idea); console.log(` ✓ Accepted: ${idea.id} — ${idea.title}`); } else if (answer === "3" || answer === "d" || answer === "details") { console.log(`\n ─── Details for ${idea.id} ───`); console.log(` ID: ${idea.id}`); console.log(` Source: ${idea.source}`); console.log(` Category: ${idea.category}`); console.log(` Confidence: ${idea.confidence.toFixed(2)}`); console.log(` Tier: ${idea.tier}`); console.log(` Title: ${idea.title}`); console.log(` Rationale: ${idea.rationale}`); if (idea.relatedReq) console.log(` Related Req: ${idea.relatedReq}`); console.log(` Actions: ${idea.actions.join(", ")}`); console.log(""); const retryAnswer = await askQuestion(" Accept this idea? (y/n) > "); if (retryAnswer === "y" || retryAnswer === "yes") { accepted.push(idea); console.log(` ✓ Accepted: ${idea.id} — ${idea.title}`); } else { skipped.push(idea); console.log(` ✗ Skipped: ${idea.id}`); } } else { skipped.push(idea); console.log(` ✗ Skipped: ${idea.id}`); } } rl.close(); console.log("\n─── Summary ───\n"); console.log(`Accepted: ${accepted.length} recommendation${accepted.length === 1 ? "" : "s"}`); console.log(`Skipped: ${skipped.length} recommendation${skipped.length === 1 ? "" : "s"}`); if (accepted.length > 0) { console.log("\nAccepted ideas:"); for (const idea of accepted) { console.log(` ${idea.id}: ${idea.title} (${idea.category.toUpperCase()})`); } const { accepted: savedIdeas, results } = engine.acceptIdeas(accepted); const savedCount = results.filter((r) => r.addedToRequirements || r.addedToRoadmap).length; if (savedCount > 0) { console.log(`\n${savedCount} idea${savedCount === 1 ? "" : "s"} added to REQUIREMENTS.md and ROADMAP.md.`); } const kickoffAnswer = await askQuestion("\nWould you like to kick off the run workflow for these ideas? (y/n) > "); if (kickoffAnswer === "y" || kickoffAnswer === "yes") { console.log("\nStarting CIAgent pipeline..."); console.log("Run: ciagent run --ideate\n"); } } rl.close(); const byCategory: Record = {}; for (const idea of allIdeas) { byCategory[idea.category] = (byCategory[idea.category] || 0) + 1; } console.log("\n─── Category Breakdown ───\n"); for (const [cat, count] of Object.entries(byCategory)) { console.log(` ${cat}: ${count}`); } }); }