diff --git a/src/cli/commands.ts b/src/cli/commands.ts index 3bf8d8b..4fdba79 100644 --- a/src/cli/commands.ts +++ b/src/cli/commands.ts @@ -169,6 +169,7 @@ export function createRunCommand(): Command { .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(); @@ -177,6 +178,49 @@ export function createRunCommand(): Command { 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); diff --git a/src/core/ideation.test.ts b/src/core/ideation.test.ts index a43c9ce..a73248a 100644 --- a/src/core/ideation.test.ts +++ b/src/core/ideation.test.ts @@ -327,4 +327,60 @@ describe("IdeationEngine", () => { expect(titles.some((t) => t.includes("coverage"))).toBe(true); }); }); + + describe("Phase 3: External signals and cascade impact", () => { + let tempDir: string; + + beforeEach(() => { + resetIdeaCounter(); + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-p3-test-")); + }); + + afterEach(() => { + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + it("runAffected detects cascade from architecture.md", () => { + const ciagentDir = path.join(tempDir, ".ciagent"); + fs.mkdirSync(ciagentDir, { recursive: true }); + fs.writeFileSync( + path.join(ciagentDir, "config.json"), + JSON.stringify({ projects: [], active_project: "default" }) + ); + fs.writeFileSync( + path.join(ciagentDir, "ARCHITECTURE.md"), + "# Architecture\n\n## Overview\n\nTest.\n\n## Components\n\n### CLI\n\n- **Description**: Command line interface\n- **Boundaries**: User-facing only\n- **Depends on**: Core\n\n### Core\n\n- **Description**: Core engine\n- **Boundaries**: Internal only\n- **Depends on**: None\n\n## Data Flow\n\nSimple.\n\n## Build Order\n\n1. CLI\n2. Core\n" + ); + + const engine = new IdeationEngine(tempDir); + const ideas = engine.runAffected(); + expect(Array.isArray(ideas)).toBe(true); + }); + + it("runExternal handles missing npm gracefully", () => { + const ciagentDir = path.join(tempDir, ".ciagent"); + fs.mkdirSync(ciagentDir, { recursive: true }); + fs.writeFileSync( + path.join(ciagentDir, "config.json"), + JSON.stringify({ projects: [], active_project: "default" }) + ); + + const engine = new IdeationEngine(tempDir); + const ideas = engine.runExternal(); + expect(Array.isArray(ideas)).toBe(true); + }); + + it("runCrossProject returns empty when only one project", () => { + const ciagentDir = path.join(tempDir, ".ciagent"); + fs.mkdirSync(ciagentDir, { recursive: true }); + fs.writeFileSync( + path.join(ciagentDir, "config.json"), + JSON.stringify({ projects: [{ slug: "default", name: "Default Project", default: true }], active_project: "default" }) + ); + + const engine = new IdeationEngine(tempDir, "default"); + const ideas = engine.runCrossProject(); + expect(ideas).toEqual([]); + }); + }); }); \ No newline at end of file