Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9927558d5 | |||
| 895d9f95a1 | |||
| 30352a3603 | |||
| d58fd0bdde |
@@ -47,6 +47,7 @@ export class OrchestratorAgent extends BaseAgent {
|
||||
|
||||
private static readonly STAGE_AGENT_MAP: Partial<Record<PipelineStage, AgentName[]>> = {
|
||||
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...");
|
||||
|
||||
|
||||
@@ -169,6 +169,7 @@ export function createRunCommand(): Command {
|
||||
.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();
|
||||
|
||||
@@ -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<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);
|
||||
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user