feat(P03): External/cascade tests + --ideate flag on run (#6)
CI / build-and-test (push) Has been cancelled
Publish to npm / publish (push) Has been cancelled

This commit is contained in:
Jon Chery
2026-05-30 20:59:40 +00:00
2 changed files with 100 additions and 0 deletions
+44
View File
@@ -169,6 +169,7 @@ export function createRunCommand(): Command {
.option("--all", "Execute all remaining phases sequentially") .option("--all", "Execute all remaining phases sequentially")
.option("--phase <number>", "Phase number", "1") .option("--phase <number>", "Phase number", "1")
.option("--backend <provider>", "Override intelligence backend for this run") .option("--backend <provider>", "Override intelligence backend for this run")
.option("--ideate", "Insert ideation stage between research and plan")
.action(async (phase, options) => { .action(async (phase, options) => {
const projectPath = process.cwd(); const projectPath = process.cwd();
@@ -177,6 +178,49 @@ export function createRunCommand(): Command {
process.exit(1); 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 config = loadConfig(projectPath);
const { backend, error: backendError } = await resolveBackendForCommand(config, options.backend); const { backend, error: backendError } = await resolveBackendForCommand(config, options.backend);
+56
View File
@@ -327,4 +327,60 @@ describe("IdeationEngine", () => {
expect(titles.some((t) => t.includes("coverage"))).toBe(true); 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([]);
});
});
}); });