diff --git a/src/agents/orchestrator.ts b/src/agents/orchestrator.ts index e7d9cb5..94df99e 100644 --- a/src/agents/orchestrator.ts +++ b/src/agents/orchestrator.ts @@ -47,6 +47,7 @@ export class OrchestratorAgent extends BaseAgent { private static readonly STAGE_AGENT_MAP: Partial> = { research: ["researcher"], + ideate: ["ideation-agent"], plan: ["planner"], execute: ["executor", "code-reviewer", "security-auditor"], test: ["tester"], @@ -571,6 +572,64 @@ export class OrchestratorAgent extends BaseAgent { break; } + case "ideate": { + this.log("Running ideation stage..."); + const { IdeationEngine } = await import("../core/ideation.js"); + const ideationEngine = new IdeationEngine(context.project_path); + const ideas = ideationEngine.runMechanical(); + + const ideationConfig = this.config.ideation; + if (ideationConfig?.categories && ideationConfig.categories.length > 0) { + const categoryIdeas = ideationEngine.runMechanical(ideationConfig.categories); + const seenTitles = new Set(ideas.map((i) => i.title)); + for (const idea of categoryIdeas) { + if (!seenTitles.has(idea.title)) { + ideas.push(idea); + seenTitles.add(idea.title); + } + } + } + + ideas.sort((a, b) => b.confidence - a.confidence); + + const maxIdeas = ideationConfig?.max_ideas || 20; + const trimmedIdeas = ideas.slice(0, maxIdeas); + + if (this.config.git.auto_commit && this.gitContext!.isGitRepo()) { + const { accepted: savedIdeas, results } = ideationEngine.acceptIdeas(trimmedIdeas); + const savedCount = results.filter((r) => r.addedToRequirements || r.addedToRoadmap).length; + + const ideationCommit = CommitBuilder.buildDecisionCommit({ + phase: this.pipelineState!.current_phase, + milestone: this.currentMilestone, + subject: `ideation results — ${trimmedIdeas.length} total, ${savedCount} accepted`, + decisions: savedIdeas.map((idea) => ({ + id: idea.id, + decision: idea.title, + rationale: idea.rationale, + confidence: idea.confidence, + alternatives: idea.actions, + })), + }); + + try { + execSync(`git add -A && git commit -m "${ideationCommit.replace(/"/g, '\\"')}" --allow-empty`, { + cwd: context.project_path, + stdio: "pipe", + }); + } catch (err) { + this.warn(`Ideation commit failed: ${err instanceof Error ? err.message : String(err)}`); + } + + artifactsCreated.push(".ciagent/REQUIREMENTS.md", ".ciagent/ROADMAP.md"); + decisionsMade += savedCount; + } + + this.pipelineState!.ideate_completed = true; + this.log(`Ideation stage complete: ${trimmedIdeas.length} ideas generated`); + break; + } + case "plan": this.log("Planning phase execution..."); diff --git a/src/types/pipeline.test.ts b/src/types/pipeline.test.ts index d2449a0..18adb15 100644 --- a/src/types/pipeline.test.ts +++ b/src/types/pipeline.test.ts @@ -62,4 +62,14 @@ describe("createInitialPipelineState", () => { expect(state.started_at).toBeTruthy(); expect(state.last_updated).toBeTruthy(); }); +}); + +describe("STAGE_ORDER ideate position", () => { + it("places ideate between research and plan", () => { + const ideateIdx = STAGE_ORDER.indexOf("ideate"); + const researchIdx = STAGE_ORDER.indexOf("research"); + const planIdx = STAGE_ORDER.indexOf("plan"); + expect(ideateIdx).toBeGreaterThan(researchIdx); + expect(ideateIdx).toBeLessThan(planIdx); + }); }); \ No newline at end of file