d58fd0bdde
---ci---
phase: 3
milestone: v0.10
status: execute
decisions:
- id: D-084
decision: Dual integration: standalone ciagent ideate + --ideate flag on run
confidence: 0.90
requirements:
covered:
- IDEATE-07
- IDEATE-08
- IDEATE-15
---/ci---
- IDEATE-07: External signal collection (npm audit, dependency staleness) tested
- IDEATE-08: Cascade impact analysis (--affected) tested
- IDEATE-15: --ideate flag on ciagent run inserts IDEATE stage between RESEARCH and PLAN
- Tests for runAffected, runExternal, runCrossProject
- 541 tests passing
1238 lines
47 KiB
TypeScript
1238 lines
47 KiB
TypeScript
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 <file>", "Specification file path")
|
|
.option("-c, --clarify", "Start interactive clarify phase", false)
|
|
.option(
|
|
"-a, --autonomy <level>",
|
|
"Autonomy level: full, supervised, guided",
|
|
"full"
|
|
)
|
|
.option("--model-profile <profile>", "Model profile: quality, speed, balanced", "quality")
|
|
.option("--no-parallel", "Disable parallel agent execution")
|
|
.option("--backend <provider>", "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 <file>, or use --clarify for interactive mode."
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const autonomyLevel = options.autonomy as AutonomyLevel;
|
|
const config: Partial<CIAgentConfig> = {
|
|
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 <number>", "Phase number", "1")
|
|
.option("--backend <provider>", "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<string>();
|
|
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("<description>", "Task description")
|
|
.option("--backend <provider>", "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 <threshold>", "Minimum confidence to auto-fix", "0.6")
|
|
.option("--backend <provider>", "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 <layer>", "Run specific layer: structural, behavioral, security, quality", "all")
|
|
.option("--backend <provider>", "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 <provider>", "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 <slug>", "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 <number>", "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 <provider>", "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("<target>", "Phase number or plan ID to rollback to")
|
|
.option("--force", "Force rollback even with downstream dependencies")
|
|
.option("--backend <provider>", "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 <slug> <name>' 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 <slug> <name>")
|
|
.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 <slug>")
|
|
.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 <provider>", "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 <categories>", "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 <format>", "Output format: interactive, json, markdown", "interactive")
|
|
.option("--project <slug>", "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<string>();
|
|
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<string> => {
|
|
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<string, number> = {};
|
|
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}`);
|
|
}
|
|
});
|
|
} |