feat(P01): add ideation engine + ciagent ideate command — IDEATE-01,02,03,17 + MULTI-01

---ci---
phase: 1
milestone: v0.10
status: execute
decisions:
  - id: D-080
    decision: Three-tier ideation (mechanical, backend-enriched, cross-project)
    rationale: Mechanical tier always produces output without backend
    confidence: 0.92
  - id: D-089
    decision: No separate codebase map command
    rationale: Git-native + .ciagent/ covers mapping; avoids tree-sitter dep
    confidence: 0.88
requirements:
  covered:
    - IDEATE-01
    - IDEATE-02
    - IDEATE-03
    - IDEATE-17
    - MULTI-01
---/ci---

Add IdeationEngine core module with 15 signal collectors:
- Uncovered/partial requirements from REQUIREMENTS.md
- Coverage gaps (documented but unimplemented agents)
- Repeated lessons from git history
- Low-confidence decisions from ---ci--- blocks
- Escalation patterns from git history
- Compound solution patterns
- Architecture drift (ARCHITECTURE.md vs src/)
- Verification inversion (missing test files)
- Improvement patterns (cross-referencing lessons + requirements)
- Spec ambiguity (should/could/might patterns)
- Spec missing (common requirement categories)
- Cascade impact (--affected from git diff)
- External signals (npm audit, dependency staleness)
- Cross-project lesson mining

Add ciagent ideate CLI command with flags:
--category, --affected, --spec, --external, --cross-project, --output

Add active_projects to CIAgentConfig (backwards compatible with active_project).
Add IDEATE pipeline stage between RESEARCH and PLAN.
Update IdeationAgent to delegate to IdeationEngine.

533 tests passing.
This commit is contained in:
Jon Chery
2026-05-30 20:13:43 +00:00
parent da528cc493
commit 8e50049ba5
11 changed files with 1317 additions and 231 deletions
+142
View File
@@ -1,5 +1,6 @@
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";
@@ -941,4 +942,145 @@ function getPreviousTag(projectPath: string, currentTag: string): string | null
} 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);
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;
}
for (let i = 0; i < allIdeas.length; i++) {
const idea = allIdeas[i];
console.log(`═══ 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("");
}
const byCategory: Record<string, number> = {};
for (const idea of allIdeas) {
byCategory[idea.category] = (byCategory[idea.category] || 0) + 1;
}
console.log("─── Summary ───\n");
console.log(`Total ideas: ${allIdeas.length}`);
for (const [cat, count] of Object.entries(byCategory)) {
console.log(` ${cat}: ${count}`);
}
});
}