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:
@@ -4,74 +4,24 @@ import * as os from "node:os";
|
|||||||
import { IdeationAgent } from "../agents/ideation-agent.js";
|
import { IdeationAgent } from "../agents/ideation-agent.js";
|
||||||
|
|
||||||
describe("IdeationAgent", () => {
|
describe("IdeationAgent", () => {
|
||||||
let tempDir: string;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-ideation-test-"));
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("generates ideas from uncovered requirements", () => {
|
|
||||||
const ciagentDir = path.join(tempDir, ".ciagent");
|
|
||||||
fs.mkdirSync(ciagentDir, { recursive: true });
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(ciagentDir, "REQUIREMENTS.md"),
|
|
||||||
"REQ-1: First requirement\nREQ-2: Second requirement"
|
|
||||||
);
|
|
||||||
|
|
||||||
const agent = new IdeationAgent();
|
|
||||||
const ideas = agent.mechanicalIdeate(tempDir);
|
|
||||||
|
|
||||||
const reqIdeas = ideas.filter((i) => i.source === "uncovered_requirement");
|
|
||||||
expect(reqIdeas.length).toBeGreaterThanOrEqual(2);
|
|
||||||
expect(reqIdeas.some((i) => i.relatedReq === "REQ-1")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("identifies coverage gaps from PROJECT.md", () => {
|
|
||||||
const ciagentDir = path.join(tempDir, ".ciagent");
|
|
||||||
fs.mkdirSync(ciagentDir, { recursive: true });
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(ciagentDir, "PROJECT.md"),
|
|
||||||
"We use agent: magic-agent and agent: super-agent for tasks."
|
|
||||||
);
|
|
||||||
|
|
||||||
const srcDir = path.join(tempDir, "src", "agents");
|
|
||||||
fs.mkdirSync(srcDir, { recursive: true });
|
|
||||||
fs.writeFileSync(path.join(srcDir, "base.ts"), "");
|
|
||||||
fs.writeFileSync(path.join(srcDir, "index.ts"), "");
|
|
||||||
|
|
||||||
const agent = new IdeationAgent();
|
|
||||||
const gaps = agent.identifyCoverageGaps(tempDir);
|
|
||||||
|
|
||||||
expect(gaps).toContain("magic-agent");
|
|
||||||
expect(gaps).toContain("super-agent");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("finds repeated patterns from lessons list", () => {
|
|
||||||
const agent = new IdeationAgent();
|
|
||||||
const lessons = [
|
|
||||||
{ topic: "testing", detail: "testing: tests are flaky" },
|
|
||||||
{ topic: "testing", detail: "testing: more test failures" },
|
|
||||||
{ topic: "build", detail: "build: CI broken" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const repeated = agent.findRepeatedPatterns(lessons);
|
|
||||||
expect(repeated).toContain("testing");
|
|
||||||
expect(repeated).not.toContain("build");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns empty ideas when no project files exist", () => {
|
|
||||||
const agent = new IdeationAgent();
|
|
||||||
const ideas = agent.mechanicalIdeate(tempDir);
|
|
||||||
|
|
||||||
expect(ideas).toEqual(expect.arrayContaining([]));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("agent name is ideation-agent", () => {
|
it("agent name is ideation-agent", () => {
|
||||||
const agent = new IdeationAgent();
|
const agent = new IdeationAgent();
|
||||||
expect(agent.name).toBe("ideation-agent");
|
expect(agent.name).toBe("ideation-agent");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("workflow is research", () => {
|
||||||
|
const agent = new IdeationAgent();
|
||||||
|
expect(agent.workflow).toBe("research");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delegates mechanicalIdeate to IdeationEngine", () => {
|
||||||
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-agent-test-"));
|
||||||
|
try {
|
||||||
|
const agent = new IdeationAgent();
|
||||||
|
const ideas = agent.mechanicalIdeate(tempDir);
|
||||||
|
expect(Array.isArray(ideas)).toBe(true);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -1,18 +1,9 @@
|
|||||||
import * as fs from "node:fs";
|
|
||||||
import * as path from "node:path";
|
|
||||||
import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||||
|
import { IdeationEngine } from "../core/ideation.js";
|
||||||
interface Idea {
|
|
||||||
source: "uncovered_requirement" | "repeated_lesson" | "gap_in_coverage" | "improvement_pattern";
|
|
||||||
title: string;
|
|
||||||
rationale: string;
|
|
||||||
confidence: number;
|
|
||||||
relatedReq?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class IdeationAgent extends BaseAgent {
|
export class IdeationAgent extends BaseAgent {
|
||||||
readonly name = "ideation-agent";
|
readonly name = "ideation-agent";
|
||||||
readonly description = "Generates improvement ideas. Output feeds directly into planning pipeline.";
|
readonly description = "Generates improvement ideas using git-native pattern mining, coverage gap analysis, and architectural drift detection. Output feeds directly into planning pipeline.";
|
||||||
readonly workflow = "research";
|
readonly workflow = "research";
|
||||||
|
|
||||||
async execute(context: AgentContext): Promise<AgentResult> {
|
async execute(context: AgentContext): Promise<AgentResult> {
|
||||||
@@ -27,8 +18,9 @@ export class IdeationAgent extends BaseAgent {
|
|||||||
return { ...result, duration_ms: Date.now() - start };
|
return { ...result, duration_ms: Date.now() - start };
|
||||||
}
|
}
|
||||||
|
|
||||||
const ideas = this.mechanicalIdeate(context.project_path);
|
const engine = new IdeationEngine(context.project_path);
|
||||||
const output = this.formatIdeas(ideas);
|
const ideas = engine.runMechanical();
|
||||||
|
const output = engine.formatIdeas(ideas);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -40,153 +32,8 @@ export class IdeationAgent extends BaseAgent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mechanicalIdeate(projectPath: string): Idea[] {
|
mechanicalIdeate(projectPath: string) {
|
||||||
const ideas: Idea[] = [];
|
const engine = new IdeationEngine(projectPath);
|
||||||
const uncoveredReqs = this.readUncoveredRequirements(projectPath);
|
return engine.runMechanical();
|
||||||
const lessons = this.readRecentLessons(projectPath);
|
|
||||||
const repeated = this.findRepeatedPatterns(lessons);
|
|
||||||
const coverageGaps = this.identifyCoverageGaps(projectPath);
|
|
||||||
|
|
||||||
for (const req of uncoveredReqs) {
|
|
||||||
ideas.push({
|
|
||||||
source: "uncovered_requirement",
|
|
||||||
title: `Address uncovered requirement: ${req}`,
|
|
||||||
rationale: `Requirement ${req} has no corresponding implementation task.`,
|
|
||||||
confidence: 0.8,
|
|
||||||
relatedReq: req,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const topic of repeated) {
|
|
||||||
ideas.push({
|
|
||||||
source: "repeated_lesson",
|
|
||||||
title: `Investigate repeated lesson: ${topic}`,
|
|
||||||
rationale: `Topic "${topic}" appears in multiple commit lessons, indicating a systemic issue.`,
|
|
||||||
confidence: 0.7,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const gap of coverageGaps) {
|
|
||||||
ideas.push({
|
|
||||||
source: "gap_in_coverage",
|
|
||||||
title: `Fill coverage gap: ${gap}`,
|
|
||||||
rationale: `Agent "${gap}" is claimed in PROJECT.md but not found in the agent registry.`,
|
|
||||||
confidence: 0.75,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.generateIdeas(uncoveredReqs, repeated, ideas);
|
|
||||||
|
|
||||||
return ideas;
|
|
||||||
}
|
|
||||||
|
|
||||||
readUncoveredRequirements(projectPath: string): string[] {
|
|
||||||
const reqPath = path.join(projectPath, ".ciagent", "REQUIREMENTS.md");
|
|
||||||
if (!fs.existsSync(reqPath)) return [];
|
|
||||||
|
|
||||||
const content = fs.readFileSync(reqPath, "utf-8");
|
|
||||||
const reqIds: string[] = [];
|
|
||||||
const reqIdRegex = /REQ-(\d+)/g;
|
|
||||||
let match;
|
|
||||||
while ((match = reqIdRegex.exec(content)) !== null) {
|
|
||||||
reqIds.push(`REQ-${match[1]}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const planPath = path.join(projectPath, ".ciagent", "PLAN.md");
|
|
||||||
if (!fs.existsSync(planPath)) return reqIds;
|
|
||||||
|
|
||||||
const planContent = fs.readFileSync(planPath, "utf-8");
|
|
||||||
const coveredReqIds = new Set<string>();
|
|
||||||
const planRegex = /REQ-(\d+)/g;
|
|
||||||
let planMatch;
|
|
||||||
while ((planMatch = planRegex.exec(planContent)) !== null) {
|
|
||||||
coveredReqIds.add(`REQ-${planMatch[1]}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return reqIds.filter((id) => !coveredReqIds.has(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
readRecentLessons(projectPath: string): Array<{ topic: string; detail: string }> {
|
|
||||||
const lessons: Array<{ topic: string; detail: string }> = [];
|
|
||||||
try {
|
|
||||||
const { execSync } = require("node:child_process");
|
|
||||||
const log = execSync('git log --grep="lessons:" --format="%B" -50', {
|
|
||||||
cwd: projectPath,
|
|
||||||
encoding: "utf-8",
|
|
||||||
timeout: 5000,
|
|
||||||
});
|
|
||||||
const lessonsRegex = /lessons:\s*\n((?:\s+-\s+.+\n?)+)/g;
|
|
||||||
let match;
|
|
||||||
while ((match = lessonsRegex.exec(log)) !== null) {
|
|
||||||
const items = match[1].split("\n").filter((l: string) => l.trim().startsWith("-"));
|
|
||||||
for (const item of items) {
|
|
||||||
const detail = item.replace(/^\s*-\s*/, "").trim();
|
|
||||||
const topic = detail.split(":")[0].trim().toLowerCase();
|
|
||||||
lessons.push({ topic, detail });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
return lessons;
|
|
||||||
}
|
|
||||||
|
|
||||||
findRepeatedPatterns(lessons: Array<{ topic: string; detail: string }>): string[] {
|
|
||||||
const counts: Record<string, number> = {};
|
|
||||||
for (const lesson of lessons) {
|
|
||||||
counts[lesson.topic] = (counts[lesson.topic] || 0) + 1;
|
|
||||||
}
|
|
||||||
return Object.entries(counts)
|
|
||||||
.filter(([, count]) => count > 1)
|
|
||||||
.map(([topic]) => topic);
|
|
||||||
}
|
|
||||||
|
|
||||||
generateIdeas(uncoveredReqs: string[], repeated: string[], ideas: Idea[]): void {
|
|
||||||
const repeatedSet = new Set(repeated.map((r) => r.toLowerCase()));
|
|
||||||
for (const req of uncoveredReqs) {
|
|
||||||
for (const topic of repeated) {
|
|
||||||
if (req.toLowerCase().includes(topic) || topic.includes(req.toLowerCase())) {
|
|
||||||
ideas.push({
|
|
||||||
source: "improvement_pattern",
|
|
||||||
title: `Cross-reference: ${req} ↔ ${topic}`,
|
|
||||||
rationale: `Repeated lesson "${topic}" directly relates to uncovered requirement ${req}.`,
|
|
||||||
confidence: 0.85,
|
|
||||||
relatedReq: req,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
identifyCoverageGaps(projectPath: string): string[] {
|
|
||||||
const projectMdPath = path.join(projectPath, ".ciagent", "PROJECT.md");
|
|
||||||
if (!fs.existsSync(projectMdPath)) return [];
|
|
||||||
|
|
||||||
const content = fs.readFileSync(projectMdPath, "utf-8");
|
|
||||||
const agentMentionRegex = /(?:agent|Agent):\s*(\S+)/g;
|
|
||||||
const mentionedAgents: string[] = [];
|
|
||||||
let match;
|
|
||||||
while ((match = agentMentionRegex.exec(content)) !== null) {
|
|
||||||
mentionedAgents.push(match[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const agentsDir = path.join(projectPath, "src", "agents");
|
|
||||||
if (!fs.existsSync(agentsDir)) return mentionedAgents;
|
|
||||||
|
|
||||||
const existingAgents = new Set(
|
|
||||||
fs.readdirSync(agentsDir)
|
|
||||||
.filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".d.ts") && f !== "index.ts" && f !== "base.ts")
|
|
||||||
.map((f) => f.replace(".ts", ""))
|
|
||||||
);
|
|
||||||
|
|
||||||
return mentionedAgents.filter((a) => !existingAgents.has(a) && !existingAgents.has(a.replace(/-agent$/, "")));
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatIdeas(ideas: Idea[]): string {
|
|
||||||
if (ideas.length === 0) return "No improvement ideas generated.";
|
|
||||||
const lines: string[] = ["Improvement Ideas:", ""];
|
|
||||||
for (const idea of ideas) {
|
|
||||||
lines.push(`[${idea.source}|${idea.confidence.toFixed(2)}] ${idea.title} — ${idea.rationale}${idea.relatedReq ? ` (req: ${idea.relatedReq})` : ""}`);
|
|
||||||
}
|
|
||||||
return lines.join("\n");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
import { CIAgentConfig, AutonomyLevel } from "../types/config.js";
|
import { CIAgentConfig, AutonomyLevel } from "../types/config.js";
|
||||||
|
import { IdeationCategory, Idea } from "../types/ideation.js";
|
||||||
import { initCIAgent, loadConfig, isCIAgentInitialized, saveConfig } from "../core/config.js";
|
import { initCIAgent, loadConfig, isCIAgentInitialized, saveConfig } from "../core/config.js";
|
||||||
import { Specification, parseSpecification } from "../types/specification.js";
|
import { Specification, parseSpecification } from "../types/specification.js";
|
||||||
import { saveSpecification } from "../core/clarify.js";
|
import { saveSpecification } from "../core/clarify.js";
|
||||||
@@ -942,3 +943,144 @@ function getPreviousTag(projectPath: string, currentTag: string): string | null
|
|||||||
|
|
||||||
return null;
|
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}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
+3
-1
@@ -17,6 +17,7 @@ import {
|
|||||||
createRollbackCommand,
|
createRollbackCommand,
|
||||||
createShipCommand,
|
createShipCommand,
|
||||||
createProjectsCommand,
|
createProjectsCommand,
|
||||||
|
createIdeateCommand,
|
||||||
} from "./commands.js";
|
} from "./commands.js";
|
||||||
|
|
||||||
let activeEscalationProtocol: { dispose(): void } | null = null;
|
let activeEscalationProtocol: { dispose(): void } | null = null;
|
||||||
@@ -63,6 +64,7 @@ program
|
|||||||
.addCommand(createClarifyCommand())
|
.addCommand(createClarifyCommand())
|
||||||
.addCommand(createRollbackCommand())
|
.addCommand(createRollbackCommand())
|
||||||
.addCommand(createShipCommand())
|
.addCommand(createShipCommand())
|
||||||
.addCommand(createProjectsCommand());
|
.addCommand(createProjectsCommand())
|
||||||
|
.addCommand(createIdeateCommand());
|
||||||
|
|
||||||
program.parse();
|
program.parse();
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import * as os from "node:os";
|
||||||
|
import { IdeationAgent } from "../agents/ideation-agent.js";
|
||||||
|
import { IdeationEngine, resetIdeaCounter } from "../core/ideation.js";
|
||||||
|
|
||||||
|
describe("IdeationAgent", () => {
|
||||||
|
let tempDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-ideation-test-"));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("agent name is ideation-agent", () => {
|
||||||
|
const agent = new IdeationAgent();
|
||||||
|
expect(agent.name).toBe("ideation-agent");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delegates to IdeationEngine for mechanical ideation", () => {
|
||||||
|
const agent = new IdeationAgent();
|
||||||
|
const ideas = agent.mechanicalIdeate(tempDir);
|
||||||
|
expect(Array.isArray(ideas)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("IdeationEngine", () => {
|
||||||
|
let tempDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
resetIdeaCounter();
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-engine-test-"));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generates ideas from uncovered requirements", () => {
|
||||||
|
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, "REQUIREMENTS.md"),
|
||||||
|
"# Requirements\n\n## v1 Requirements\n\n### Core\n\n- **REQ-01**: First requirement\n- **REQ-02**: Second requirement\n\n## Traceability\n\n| Requirement | Phase | Status |\n|-------------|-------|--------|\n| REQ-01 | Phase 1 | pending |\n| REQ-02 | Phase 1 | pending |\n"
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "PROJECT.md"),
|
||||||
|
"# Test Project\n\n## What This Is\n\nA test project.\n\n## Requirements\n\n### Validated\n\n- REQ-01: First\n\n### Active\n\n- [ ] REQ-02: Second\n\n## Constraints\n\n- Must work\n\n## Key Decisions\n\n| Decision | Rationale | Outcome |\n|----------|-----------|--------|\n"
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "ROADMAP.md"),
|
||||||
|
"# Roadmap\n\n## Overview\n\nTest roadmap.\n\n## Phases\n\n- [ ] **Phase 1: Init** - Starting\n\n## Phase Details\n\n### Phase 1: Init\n\n**Goal.**: Start\n**Status**: not_started\n**Requirements**: REQ-01\n**Depends on**: Nothing\n**Success Criteria**:\n1. Project initialized\n"
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "ARCHITECTURE.md"),
|
||||||
|
"# Architecture\n\n## Overview\n\nTest architecture.\n\n## Components\n\n### Core\n\n- **Description**: Core module\n- **Boundaries**: Internal only\n- **Depends on**: None\n\n## Data Flow\n\nSimple flow.\n\n## Build Order\n\n1. Core\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
const engine = new IdeationEngine(tempDir);
|
||||||
|
const ideas = engine.runMechanical(["coverage"]);
|
||||||
|
const reqIdeas = ideas.filter((i) => i.source === "uncovered_requirement");
|
||||||
|
expect(reqIdeas.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects architecture drift when documented components are missing", () => {
|
||||||
|
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### NonExistentModule\n\n- **Description**: A module that does not exist\n- **Boundaries**: None\n- **Depends on**: None\n\n## Data Flow\n\nFlow.\n\n## Build Order\n\n1. Core\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
const engine = new IdeationEngine(tempDir);
|
||||||
|
const ideas = engine.runMechanical(["architecture"]);
|
||||||
|
const driftIdeas = ideas.filter((i) => i.source === "architecture_drift");
|
||||||
|
expect(driftIdeas.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects spec ambiguity or spec missing", () => {
|
||||||
|
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, "PROJECT.md"),
|
||||||
|
"# Test\n\n## What This Is\n\nThe system should handle user input and could process data. It might also log events.\n\n## Requirements\n\n### Validated\n\n\n### Active\n\n- [ ] The system should handle errors\n- [ ] Users could configure settings\n- [ ] It might send notifications\n\n## Constraints\n\n- Must work\n\n## Key Decisions\n\n| Decision | Rationale | Outcome |\n|----------|-----------|--------|\n"
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "REQUIREMENTS.md"),
|
||||||
|
"# Requirements\n\n## v1\n\n- REQ-01: Test\n\n## Traceability\n\n| Requirement | Phase | Status |\n|-------------|-------|--------|\n"
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "ROADMAP.md"),
|
||||||
|
"# Roadmap\n\n## Overview\n\nTest\n\n## Phases\n\n\n## Phase Details\n\n"
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ciagentDir, "ARCHITECTURE.md"),
|
||||||
|
"# Architecture\n\n## Overview\n\nTest\n\n## Components\n\n## Data Flow\n\nTest\n\n## Build Order\n\n1. Test\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
const engine = new IdeationEngine(tempDir);
|
||||||
|
const ideas = engine.runMechanical(["spec"]);
|
||||||
|
const specIdeas = ideas.filter((i) => i.source === "spec_ambiguity" || i.source === "spec_missing" || i.source === "spec_contradiction");
|
||||||
|
expect(specIdeas.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty ideas when no project files exist", () => {
|
||||||
|
const engine = new IdeationEngine(tempDir);
|
||||||
|
const ideas = engine.runMechanical();
|
||||||
|
expect(Array.isArray(ideas)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats ideas as readable text", () => {
|
||||||
|
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, "PROJECT.md"), "# Test\n\n## What This Is\n\nTest\n\n## Requirements\n\n### Validated\n\n\n### Active\n\n\n## Constraints\n\n- None\n\n## Key Decisions\n\n| Decision | Rationale | Outcome |\n|----------|-----------|--------|\n");
|
||||||
|
fs.writeFileSync(path.join(ciagentDir, "REQUIREMENTS.md"), "# Requirements\n\n## v1\n\n- REQ-01: Test\n\n## Traceability\n\n| Requirement | Phase | Status |\n|-------------|-------|--------|\n| REQ-01 | Phase 1 | pending |\n");
|
||||||
|
fs.writeFileSync(path.join(ciagentDir, "ROADMAP.md"), "# Roadmap\n\n## Overview\n\nTest\n\n## Phases\n\n\n## Phase Details\n\n");
|
||||||
|
fs.writeFileSync(path.join(ciagentDir, "ARCHITECTURE.md"), "# Architecture\n\n## Overview\n\nTest\n\n## Components\n\n## Data Flow\n\nTest\n\n## Build Order\n\n1. Test\n");
|
||||||
|
|
||||||
|
const engine = new IdeationEngine(tempDir);
|
||||||
|
const formatted = engine.formatIdeas(engine.runMechanical());
|
||||||
|
expect(typeof formatted).toBe("string");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats ideas as JSON", () => {
|
||||||
|
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, "PROJECT.md"), "# Test\n\n## What This Is\n\nTest\n\n## Requirements\n\n### Validated\n\n\n### Active\n\n\n## Constraints\n\n- None\n\n## Key Decisions\n\n| Decision | Rationale | Outcome |\n|----------|-----------|--------|\n");
|
||||||
|
fs.writeFileSync(path.join(ciagentDir, "REQUIREMENTS.md"), "# Requirements\n\n## v1\n\n- REQ-01: Test\n\n## Traceability\n\n| Requirement | Phase | Status |\n|-------------|-------|--------|\n| REQ-01 | Phase 1 | pending |\n");
|
||||||
|
fs.writeFileSync(path.join(ciagentDir, "ROADMAP.md"), "# Roadmap\n\n## Overview\n\nTest\n\n## Phases\n\n\n## Phase Details\n\n");
|
||||||
|
fs.writeFileSync(path.join(ciagentDir, "ARCHITECTURE.md"), "# Architecture\n\n## Overview\n\nTest\n\n## Components\n\n## Data Flow\n\nTest\n\n## Build Order\n\n1. Test\n");
|
||||||
|
|
||||||
|
const engine = new IdeationEngine(tempDir);
|
||||||
|
const result = engine.formatIdeasJson(engine.runMechanical());
|
||||||
|
expect(result).toHaveProperty("ideas");
|
||||||
|
expect(result).toHaveProperty("summary");
|
||||||
|
expect(result).toHaveProperty("project");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,849 @@
|
|||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import { CIAgentFiles } from "./ciagent-files.js";
|
||||||
|
import { GitContext } from "./git-context.js";
|
||||||
|
|
||||||
|
export type IdeationSource =
|
||||||
|
| "uncovered_requirement"
|
||||||
|
| "repeated_lesson"
|
||||||
|
| "low_confidence_decision"
|
||||||
|
| "escalation_pattern"
|
||||||
|
| "compound_pattern"
|
||||||
|
| "partial_requirement"
|
||||||
|
| "gap_in_coverage"
|
||||||
|
| "improvement_pattern"
|
||||||
|
| "architecture_drift"
|
||||||
|
| "verification_inversion"
|
||||||
|
| "spec_ambiguity"
|
||||||
|
| "spec_contradiction"
|
||||||
|
| "spec_missing"
|
||||||
|
| "external_signal"
|
||||||
|
| "cross_project_lesson"
|
||||||
|
| "chaos_scenario";
|
||||||
|
|
||||||
|
export type IdeationCategory =
|
||||||
|
| "security"
|
||||||
|
| "quality"
|
||||||
|
| "architecture"
|
||||||
|
| "coverage"
|
||||||
|
| "improvement"
|
||||||
|
| "spec"
|
||||||
|
| "chaos";
|
||||||
|
|
||||||
|
export type IdeationAction =
|
||||||
|
| "add_requirement"
|
||||||
|
| "update_architecture"
|
||||||
|
| "update_roadmap"
|
||||||
|
| "fix_documentation"
|
||||||
|
| "add_test"
|
||||||
|
| "add_security_pattern"
|
||||||
|
| "refactor"
|
||||||
|
| "new_milestone_phase";
|
||||||
|
|
||||||
|
export type IdeationTier = "mechanical" | "backend-enriched" | "cross-project";
|
||||||
|
|
||||||
|
export interface Idea {
|
||||||
|
id: string;
|
||||||
|
source: IdeationSource;
|
||||||
|
category: IdeationCategory;
|
||||||
|
title: string;
|
||||||
|
rationale: string;
|
||||||
|
confidence: number;
|
||||||
|
relatedReq?: string;
|
||||||
|
actions: IdeationAction[];
|
||||||
|
tier: IdeationTier;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IdeationResult {
|
||||||
|
project: string;
|
||||||
|
milestone: string;
|
||||||
|
ideas: Idea[];
|
||||||
|
summary: IdeationSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IdeationSummary {
|
||||||
|
total: number;
|
||||||
|
accepted: number;
|
||||||
|
skipped: number;
|
||||||
|
by_category: Record<string, number>;
|
||||||
|
by_tier: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IdeationConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
categories: IdeationCategory[];
|
||||||
|
confidence_threshold: number;
|
||||||
|
max_ideas: number;
|
||||||
|
external_signals: {
|
||||||
|
npm_audit: boolean;
|
||||||
|
osv_advisories: boolean;
|
||||||
|
dependency_staleness: boolean;
|
||||||
|
};
|
||||||
|
cross_project: {
|
||||||
|
enabled: boolean;
|
||||||
|
similarity_weight: number;
|
||||||
|
};
|
||||||
|
chaos: {
|
||||||
|
enabled: boolean;
|
||||||
|
scenarios: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_IDEATION_CONFIG: IdeationConfig = {
|
||||||
|
enabled: true,
|
||||||
|
categories: ["security", "quality", "architecture", "coverage", "improvement"],
|
||||||
|
confidence_threshold: 0.6,
|
||||||
|
max_ideas: 20,
|
||||||
|
external_signals: {
|
||||||
|
npm_audit: true,
|
||||||
|
osv_advisories: true,
|
||||||
|
dependency_staleness: true,
|
||||||
|
},
|
||||||
|
cross_project: {
|
||||||
|
enabled: false,
|
||||||
|
similarity_weight: 0.5,
|
||||||
|
},
|
||||||
|
chaos: {
|
||||||
|
enabled: true,
|
||||||
|
scenarios: ["backend_unavailable", "requirement_change", "test_coverage_drop"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let ideaCounter = 0;
|
||||||
|
|
||||||
|
function nextIdeaId(): string {
|
||||||
|
ideaCounter++;
|
||||||
|
return `IDEATE-${String(ideaCounter).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetIdeaCounter(): void {
|
||||||
|
ideaCounter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IdeationEngine {
|
||||||
|
private ciFiles: CIAgentFiles;
|
||||||
|
private projectPath: string;
|
||||||
|
|
||||||
|
constructor(projectPath: string, projectSlug?: string) {
|
||||||
|
this.projectPath = projectPath;
|
||||||
|
this.ciFiles = new CIAgentFiles(projectPath);
|
||||||
|
if (projectSlug) {
|
||||||
|
this.ciFiles.setProjectSlug(projectSlug);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runMechanical(categories?: IdeationCategory[]): Idea[] {
|
||||||
|
resetIdeaCounter();
|
||||||
|
const ideas: Idea[] = [];
|
||||||
|
const filterCategories = categories || DEFAULT_IDEATION_CONFIG.categories;
|
||||||
|
|
||||||
|
const shouldCategory = (cat: IdeationCategory): boolean =>
|
||||||
|
filterCategories.length === 0 || filterCategories.includes(cat);
|
||||||
|
|
||||||
|
if (shouldCategory("coverage")) {
|
||||||
|
ideas.push(...this.mineUncoveredRequirements());
|
||||||
|
ideas.push(...this.minePartialRequirements());
|
||||||
|
ideas.push(...this.mineCoverageGaps());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldCategory("quality") || shouldCategory("improvement")) {
|
||||||
|
ideas.push(...this.mineRepeatedLessons());
|
||||||
|
ideas.push(...this.mineLowConfidenceDecisions());
|
||||||
|
ideas.push(...this.mineCompoundPatterns());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldCategory("architecture")) {
|
||||||
|
ideas.push(...this.mineArchitectureDrift());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldCategory("security")) {
|
||||||
|
ideas.push(...this.mineEscalationPatterns());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldCategory("improvement")) {
|
||||||
|
ideas.push(...this.mineImprovementPatterns());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldCategory("spec")) {
|
||||||
|
ideas.push(...this.mineSpecAmbiguity());
|
||||||
|
ideas.push(...this.mineSpecContradictions());
|
||||||
|
ideas.push(...this.mineSpecMissing());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldCategory("quality")) {
|
||||||
|
ideas.push(...this.mineVerificationInversion());
|
||||||
|
}
|
||||||
|
|
||||||
|
ideas.sort((a, b) => b.confidence - a.confidence);
|
||||||
|
|
||||||
|
return ideas.slice(0, DEFAULT_IDEATION_CONFIG.max_ideas);
|
||||||
|
}
|
||||||
|
|
||||||
|
private mineUncoveredRequirements(): Idea[] {
|
||||||
|
const ideas: Idea[] = [];
|
||||||
|
const reqs = this.ciFiles.readRequirementsMd();
|
||||||
|
if (!reqs) return ideas;
|
||||||
|
|
||||||
|
const coveredReqs = new Set<string>();
|
||||||
|
for (const t of reqs.traceability) {
|
||||||
|
if (t.status === "complete") {
|
||||||
|
coveredReqs.add(t.requirement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const allReqIds = new Set<string>();
|
||||||
|
for (const cat of [...reqs.v1, ...reqs.v2]) {
|
||||||
|
for (const item of cat.items) {
|
||||||
|
allReqIds.add(item.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const reqId of allReqIds) {
|
||||||
|
if (!coveredReqs.has(reqId)) {
|
||||||
|
ideas.push({
|
||||||
|
id: nextIdeaId(),
|
||||||
|
source: "uncovered_requirement",
|
||||||
|
category: "coverage",
|
||||||
|
title: `Address uncovered requirement: ${reqId}`,
|
||||||
|
rationale: `Requirement ${reqId} exists in REQUIREMENTS.md but has no completed implementation traceability record.`,
|
||||||
|
confidence: 0.85,
|
||||||
|
relatedReq: reqId,
|
||||||
|
actions: ["add_requirement", "update_roadmap"],
|
||||||
|
tier: "mechanical",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ideas;
|
||||||
|
}
|
||||||
|
|
||||||
|
private minePartialRequirements(): Idea[] {
|
||||||
|
const ideas: Idea[] = [];
|
||||||
|
const reqs = this.ciFiles.readRequirementsMd();
|
||||||
|
if (!reqs) return ideas;
|
||||||
|
|
||||||
|
for (const t of reqs.traceability) {
|
||||||
|
if (t.status === "in_progress") {
|
||||||
|
ideas.push({
|
||||||
|
id: nextIdeaId(),
|
||||||
|
source: "partial_requirement",
|
||||||
|
category: "coverage",
|
||||||
|
title: `Complete in-progress requirement: ${t.requirement}`,
|
||||||
|
rationale: `Requirement ${t.requirement} (Phase ${t.phase}) is in progress but not complete. In-progress items may be blocked or abandoned.`,
|
||||||
|
confidence: 0.75,
|
||||||
|
relatedReq: t.requirement,
|
||||||
|
actions: ["add_requirement"],
|
||||||
|
tier: "mechanical",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ideas;
|
||||||
|
}
|
||||||
|
|
||||||
|
private mineCoverageGaps(): Idea[] {
|
||||||
|
const ideas: Idea[] = [];
|
||||||
|
const projectMd = this.ciFiles.readProjectMd();
|
||||||
|
if (!projectMd) return ideas;
|
||||||
|
|
||||||
|
const mentionedAgents: string[] = [];
|
||||||
|
const agentRegex = /(?:agent|Agent)[:\s]+(\S+)/g;
|
||||||
|
let match;
|
||||||
|
while ((match = agentRegex.exec(projectMd.coreValue || "")) !== null) {
|
||||||
|
mentionedAgents.push(match[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentsDir = path.join(this.projectPath, "src", "agents");
|
||||||
|
if (!fs.existsSync(agentsDir)) return ideas;
|
||||||
|
|
||||||
|
const existingAgents = new Set(
|
||||||
|
fs.readdirSync(agentsDir)
|
||||||
|
.filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".d.ts") && f !== "index.ts" && f !== "base.ts")
|
||||||
|
.map((f) => f.replace(".ts", ""))
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const agent of mentionedAgents) {
|
||||||
|
if (!existingAgents.has(agent) && !existingAgents.has(agent.replace(/-agent$/, ""))) {
|
||||||
|
ideas.push({
|
||||||
|
id: nextIdeaId(),
|
||||||
|
source: "gap_in_coverage",
|
||||||
|
category: "coverage",
|
||||||
|
title: `Fill coverage gap: ${agent}`,
|
||||||
|
rationale: `Agent "${agent}" is mentioned in PROJECT.md but not found in the agent registry.`,
|
||||||
|
confidence: 0.75,
|
||||||
|
actions: ["add_requirement"],
|
||||||
|
tier: "mechanical",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ideas;
|
||||||
|
}
|
||||||
|
|
||||||
|
private mineRepeatedLessons(): Idea[] {
|
||||||
|
const ideas: Idea[] = [];
|
||||||
|
const lessons = this.readGitLessons();
|
||||||
|
const topicCounts: Record<string, number> = {};
|
||||||
|
const topicDetails: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
for (const lesson of lessons) {
|
||||||
|
const topic = lesson.topic;
|
||||||
|
topicCounts[topic] = (topicCounts[topic] || 0) + 1;
|
||||||
|
if (!topicDetails[topic]) topicDetails[topic] = [];
|
||||||
|
topicDetails[topic].push(lesson.detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [topic, count] of Object.entries(topicCounts)) {
|
||||||
|
if (count > 1) {
|
||||||
|
ideas.push({
|
||||||
|
id: nextIdeaId(),
|
||||||
|
source: "repeated_lesson",
|
||||||
|
category: "improvement",
|
||||||
|
title: `Investigate repeated lesson: ${topic}`,
|
||||||
|
rationale: `Topic "${topic}" appears ${count} times in commit lessons (${topicDetails[topic].slice(0, 2).join("; ")}), indicating a systemic issue.`,
|
||||||
|
confidence: Math.min(0.7 + count * 0.05, 0.95),
|
||||||
|
actions: ["add_requirement", "refactor"],
|
||||||
|
tier: "mechanical",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ideas;
|
||||||
|
}
|
||||||
|
|
||||||
|
private mineLowConfidenceDecisions(): Idea[] {
|
||||||
|
const ideas: Idea[] = [];
|
||||||
|
try {
|
||||||
|
const log = execSync(
|
||||||
|
'git log --all --grep="decisions:" --format="%B" -50',
|
||||||
|
{ cwd: this.projectPath, encoding: "utf-8", timeout: 5000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const decisionRegex = /confidence:\s*([\d.]+)/gi;
|
||||||
|
let match;
|
||||||
|
while ((match = decisionRegex.exec(log)) !== null) {
|
||||||
|
const confidence = parseFloat(match[1]);
|
||||||
|
if (confidence < 0.7 && confidence > 0) {
|
||||||
|
const contextStart = Math.max(0, match.index - 200);
|
||||||
|
const context = log.slice(contextStart, match.index + 100);
|
||||||
|
const idMatch = context.match(/id:\s*(D-\d+)/i);
|
||||||
|
ideas.push({
|
||||||
|
id: nextIdeaId(),
|
||||||
|
source: "low_confidence_decision",
|
||||||
|
category: "improvement",
|
||||||
|
title: `Revisit low-confidence decision${idMatch ? ` ${idMatch[1]}` : ""}`,
|
||||||
|
rationale: `A decision was made with confidence ${confidence.toFixed(2)} (below 0.7 threshold). Low-confidence decisions are prime candidates for re-evaluation.`,
|
||||||
|
confidence: 0.8,
|
||||||
|
actions: ["update_roadmap"],
|
||||||
|
tier: "mechanical",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return ideas;
|
||||||
|
}
|
||||||
|
|
||||||
|
private mineEscalationPatterns(): Idea[] {
|
||||||
|
const ideas: Idea[] = [];
|
||||||
|
try {
|
||||||
|
const log = execSync(
|
||||||
|
'git log --all --grep="escalation:" --format="%B" -50',
|
||||||
|
{ cwd: this.projectPath, encoding: "utf-8", timeout: 5000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const typeCounts: Record<string, number> = {};
|
||||||
|
const escalationRegex = /type:\s*(\S+)/gi;
|
||||||
|
let match;
|
||||||
|
while ((match = escalationRegex.exec(log)) !== null) {
|
||||||
|
const type = match[1].toLowerCase();
|
||||||
|
typeCounts[type] = (typeCounts[type] || 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [type, count] of Object.entries(typeCounts)) {
|
||||||
|
if (count >= 1) {
|
||||||
|
ideas.push({
|
||||||
|
id: nextIdeaId(),
|
||||||
|
source: "escalation_pattern",
|
||||||
|
category: "security",
|
||||||
|
title: `Address escalation pattern: ${type}`,
|
||||||
|
rationale: `Escalation type "${type}" occurred ${count} time(s). Recurring escalation types indicate process gaps that should be addressed.`,
|
||||||
|
confidence: 0.7 + Math.min(count * 0.1, 0.2),
|
||||||
|
actions: ["add_security_pattern", "update_roadmap"],
|
||||||
|
tier: "mechanical",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return ideas;
|
||||||
|
}
|
||||||
|
|
||||||
|
private mineCompoundPatterns(): Idea[] {
|
||||||
|
const ideas: Idea[] = [];
|
||||||
|
try {
|
||||||
|
const log = execSync(
|
||||||
|
'git log --all --grep="compound:" --format="%B" -50',
|
||||||
|
{ cwd: this.projectPath, encoding: "utf-8", timeout: 5000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const compoundRegex = /compound:\s*\n((?:\s+-\s+.+\n?)+)/g;
|
||||||
|
const topicCounts: Record<string, number> = {};
|
||||||
|
let match;
|
||||||
|
while ((match = compoundRegex.exec(log)) !== null) {
|
||||||
|
const items = match[1].split("\n").filter((l: string) => l.trim().startsWith("-"));
|
||||||
|
for (const item of items) {
|
||||||
|
const detail = item.replace(/^\s*-\s*/, "").trim();
|
||||||
|
const topic = detail.split(":")[0].trim().toLowerCase();
|
||||||
|
topicCounts[topic] = (topicCounts[topic] || 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [topic, count] of Object.entries(topicCounts)) {
|
||||||
|
if (count > 1) {
|
||||||
|
ideas.push({
|
||||||
|
id: nextIdeaId(),
|
||||||
|
source: "compound_pattern",
|
||||||
|
category: "improvement",
|
||||||
|
title: `Generalize compounded solution: ${topic}`,
|
||||||
|
rationale: `Solution pattern "${topic}" was compounded ${count} times. Consider generalizing this into a shared utility or documented approach.`,
|
||||||
|
confidence: 0.75,
|
||||||
|
actions: ["refactor", "update_architecture"],
|
||||||
|
tier: "mechanical",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return ideas;
|
||||||
|
}
|
||||||
|
|
||||||
|
private mineArchitectureDrift(): Idea[] {
|
||||||
|
const ideas: Idea[] = [];
|
||||||
|
const archMd = this.ciFiles.readArchitectureMd();
|
||||||
|
if (!archMd) return ideas;
|
||||||
|
|
||||||
|
for (const component of archMd.components) {
|
||||||
|
const expectedDir = component.name.toLowerCase().replace(/\s+/g, "-");
|
||||||
|
const possiblePaths = [
|
||||||
|
path.join(this.projectPath, "src", expectedDir),
|
||||||
|
path.join(this.projectPath, "src", component.name),
|
||||||
|
path.join(this.projectPath, component.name.toLowerCase()),
|
||||||
|
];
|
||||||
|
|
||||||
|
const dirExists = possiblePaths.some((p) => fs.existsSync(p));
|
||||||
|
if (!dirExists) {
|
||||||
|
ideas.push({
|
||||||
|
id: nextIdeaId(),
|
||||||
|
source: "architecture_drift",
|
||||||
|
category: "architecture",
|
||||||
|
title: `Documented component not found: ${component.name}`,
|
||||||
|
rationale: `ARCHITECTURE.md documents component "${component.name}" but no corresponding directory exists in src/. Either the component is missing or the documentation is stale.`,
|
||||||
|
confidence: 0.7,
|
||||||
|
actions: ["update_architecture", "fix_documentation"],
|
||||||
|
tier: "mechanical",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const srcDir = path.join(this.projectPath, "src");
|
||||||
|
if (fs.existsSync(srcDir)) {
|
||||||
|
const knownComponents = new Set(archMd.components.map((c) => c.name.toLowerCase().replace(/\s+/g, "-")));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const srcEntries = fs.readdirSync(srcDir, { withFileTypes: true })
|
||||||
|
.filter((d) => d.isDirectory())
|
||||||
|
.map((d) => d.name.toLowerCase());
|
||||||
|
|
||||||
|
for (const entry of srcEntries) {
|
||||||
|
if (!knownComponents.has(entry) && !entry.startsWith(".") && entry !== "types" && entry !== "utils") {
|
||||||
|
ideas.push({
|
||||||
|
id: nextIdeaId(),
|
||||||
|
source: "architecture_drift",
|
||||||
|
category: "architecture",
|
||||||
|
title: `Undocumented source directory: src/${entry}`,
|
||||||
|
rationale: `Directory src/${entry}/ exists but is not documented in ARCHITECTURE.md. This indicates architectural drift.`,
|
||||||
|
confidence: 0.65,
|
||||||
|
actions: ["update_architecture"],
|
||||||
|
tier: "mechanical",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ideas;
|
||||||
|
}
|
||||||
|
|
||||||
|
private mineVerificationInversion(): Idea[] {
|
||||||
|
const ideas: Idea[] = [];
|
||||||
|
|
||||||
|
const srcDir = path.join(this.projectPath, "src");
|
||||||
|
if (!fs.existsSync(srcDir)) return ideas;
|
||||||
|
|
||||||
|
const testFiles: string[] = [];
|
||||||
|
const srcFiles: string[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const walkDir = (dir: string) => {
|
||||||
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
||||||
|
walkDir(fullPath);
|
||||||
|
} else if (entry.isFile() && entry.name.endsWith(".ts")) {
|
||||||
|
if (entry.name.endsWith(".test.ts")) {
|
||||||
|
testFiles.push(entry.name.replace(".test.ts", ""));
|
||||||
|
} else if (!entry.name.endsWith(".d.ts") && !entry.name.includes(".test.")) {
|
||||||
|
srcFiles.push(entry.name.replace(".ts", ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walkDir(srcDir);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const testedModules = new Set(testFiles);
|
||||||
|
for (const srcModule of srcFiles) {
|
||||||
|
if (!testedModules.has(srcModule) && srcModule !== "index" && srcModule !== "base") {
|
||||||
|
ideas.push({
|
||||||
|
id: nextIdeaId(),
|
||||||
|
source: "verification_inversion",
|
||||||
|
category: "quality",
|
||||||
|
title: `Missing tests for: ${srcModule}`,
|
||||||
|
rationale: `Source file ${srcModule}.ts has no corresponding test file ${srcModule}.test.ts. The behavioral verification layer identifies this as a coverage gap.`,
|
||||||
|
confidence: 0.7,
|
||||||
|
actions: ["add_test"],
|
||||||
|
tier: "mechanical",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ideas.slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
private mineImprovementPatterns(): Idea[] {
|
||||||
|
const ideas: Idea[] = [];
|
||||||
|
const reqs = this.ciFiles.readRequirementsMd();
|
||||||
|
const lessons = this.readGitLessons();
|
||||||
|
|
||||||
|
if (!reqs) return ideas;
|
||||||
|
|
||||||
|
const uncoveredSet = new Set<string>();
|
||||||
|
for (const t of reqs.traceability) {
|
||||||
|
if (t.status === "pending") {
|
||||||
|
uncoveredSet.add(t.requirement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const topics = lessons.map((l) => l.topic.toLowerCase());
|
||||||
|
|
||||||
|
for (const reqId of uncoveredSet) {
|
||||||
|
for (const topic of topics) {
|
||||||
|
if (reqId.toLowerCase().includes(topic) || topic.includes(reqId.toLowerCase())) {
|
||||||
|
ideas.push({
|
||||||
|
id: nextIdeaId(),
|
||||||
|
source: "improvement_pattern",
|
||||||
|
category: "improvement",
|
||||||
|
title: `Cross-reference: ${reqId} ↔ ${topic}`,
|
||||||
|
rationale: `Repeated lesson "${topic}" directly relates to uncovered requirement ${reqId}. Addressing the lesson may resolve the requirement.`,
|
||||||
|
confidence: 0.85,
|
||||||
|
relatedReq: reqId,
|
||||||
|
actions: ["add_requirement", "update_roadmap"],
|
||||||
|
tier: "mechanical",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ideas;
|
||||||
|
}
|
||||||
|
|
||||||
|
private mineSpecAmbiguity(): Idea[] {
|
||||||
|
const ideas: Idea[] = [];
|
||||||
|
const projectMd = this.ciFiles.readProjectMd();
|
||||||
|
if (!projectMd) return ideas;
|
||||||
|
|
||||||
|
const ambiguousTerms = ["should", "could", "might", "may", "would", "possibly", "perhaps"];
|
||||||
|
const specText = [projectMd.coreValue, ...projectMd.requirements.active].join(" ");
|
||||||
|
|
||||||
|
for (const term of ambiguousTerms) {
|
||||||
|
const regex = new RegExp(`\\b${term}\\b`, "gi");
|
||||||
|
const matches = specText.match(regex);
|
||||||
|
if (matches && matches.length > 2) {
|
||||||
|
ideas.push({
|
||||||
|
id: nextIdeaId(),
|
||||||
|
source: "spec_ambiguity",
|
||||||
|
category: "spec",
|
||||||
|
title: `Ambiguous language in specification: "${term}" (${matches.length} occurrences)`,
|
||||||
|
rationale: `The term "${term}" appears ${matches.length} times in project specification. Consider replacing with "must" or "shall" for clarity, or marking as optional.`,
|
||||||
|
confidence: 0.65,
|
||||||
|
actions: ["fix_documentation"],
|
||||||
|
tier: "mechanical",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ideas;
|
||||||
|
}
|
||||||
|
|
||||||
|
private mineSpecContradictions(): Idea[] {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private mineSpecMissing(): Idea[] {
|
||||||
|
const ideas: Idea[] = [];
|
||||||
|
const projectMd = this.ciFiles.readProjectMd();
|
||||||
|
if (!projectMd) return ideas;
|
||||||
|
|
||||||
|
const specText = (projectMd.coreValue + " " + projectMd.requirements.active.join(" ")).toLowerCase();
|
||||||
|
|
||||||
|
const commonCategories: Array<{ keyword: string; title: string; category: IdeationCategory }> = [
|
||||||
|
{ keyword: "auth", title: "Add authentication and authorization requirements", category: "security" },
|
||||||
|
{ keyword: "rate", title: "Add rate limiting requirements", category: "security" },
|
||||||
|
{ keyword: "log", title: "Add logging and observability requirements", category: "quality" },
|
||||||
|
{ keyword: "error", title: "Add error handling and recovery requirements", category: "quality" },
|
||||||
|
{ keyword: "test", title: "Add testing strategy requirements", category: "coverage" },
|
||||||
|
{ keyword: "doc", title: "Add documentation requirements", category: "improvement" },
|
||||||
|
{ keyword: "config", title: "Add configuration management requirements", category: "architecture" },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const cat of commonCategories) {
|
||||||
|
if (!specText.includes(cat.keyword)) {
|
||||||
|
ideas.push({
|
||||||
|
id: nextIdeaId(),
|
||||||
|
source: "spec_missing",
|
||||||
|
category: cat.category,
|
||||||
|
title: cat.title,
|
||||||
|
rationale: `No mention of "${cat.keyword}" in the project specification. This is a common requirement category that may be missing.`,
|
||||||
|
confidence: 0.55,
|
||||||
|
actions: ["add_requirement"],
|
||||||
|
tier: "mechanical",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ideas;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readGitLessons(): Array<{ topic: string; detail: string }> {
|
||||||
|
const lessons: Array<{ topic: string; detail: string }> = [];
|
||||||
|
try {
|
||||||
|
const log = execSync('git log --all --grep="lessons:" --format="%B" -50', {
|
||||||
|
cwd: this.projectPath,
|
||||||
|
encoding: "utf-8",
|
||||||
|
timeout: 5000,
|
||||||
|
});
|
||||||
|
const lessonsRegex = /lessons:\s*\n((?:\s+-\s+.+\n?)+)/g;
|
||||||
|
let match;
|
||||||
|
while ((match = lessonsRegex.exec(log)) !== null) {
|
||||||
|
const items = match[1].split("\n").filter((l: string) => l.trim().startsWith("-"));
|
||||||
|
for (const item of items) {
|
||||||
|
const detail = item.replace(/^\s*-\s*/, "").trim();
|
||||||
|
const topic = detail.split(":")[0].trim().toLowerCase();
|
||||||
|
lessons.push({ topic, detail });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return lessons;
|
||||||
|
}
|
||||||
|
|
||||||
|
runAffected(): Idea[] {
|
||||||
|
resetIdeaCounter();
|
||||||
|
const ideas: Idea[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const diff = execSync("git diff --name-only HEAD", {
|
||||||
|
cwd: this.projectPath,
|
||||||
|
encoding: "utf-8",
|
||||||
|
timeout: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const changedFiles = diff.trim().split("\n").filter(Boolean);
|
||||||
|
if (changedFiles.length === 0) return ideas;
|
||||||
|
|
||||||
|
const archMd = this.ciFiles.readArchitectureMd();
|
||||||
|
if (!archMd) return ideas;
|
||||||
|
|
||||||
|
for (const changedFile of changedFiles) {
|
||||||
|
const parts = changedFile.split("/").filter(Boolean);
|
||||||
|
const srcIdx = parts.indexOf("src");
|
||||||
|
if (srcIdx >= 0 && parts.length > srcIdx + 1) {
|
||||||
|
const component = parts[srcIdx + 1];
|
||||||
|
const matchingComponent = archMd.components.find(
|
||||||
|
(c) => c.name.toLowerCase().replace(/\s+/g, "-") === component.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingComponent) {
|
||||||
|
for (const dep of matchingComponent.dependsOn) {
|
||||||
|
ideas.push({
|
||||||
|
id: nextIdeaId(),
|
||||||
|
source: "gap_in_coverage",
|
||||||
|
category: "architecture",
|
||||||
|
title: `Cascade impact: ${changedFile} may affect ${dep}`,
|
||||||
|
rationale: `Component "${matchingComponent.name}" (which depends on "${dep}") was modified. Verify that "${dep}" still works correctly.`,
|
||||||
|
confidence: 0.7,
|
||||||
|
actions: ["add_test"],
|
||||||
|
tier: "mechanical",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return ideas;
|
||||||
|
}
|
||||||
|
|
||||||
|
runExternal(): Idea[] {
|
||||||
|
resetIdeaCounter();
|
||||||
|
const ideas: Idea[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const auditResult = execSync("npm audit --json 2>/dev/null || echo '{}'", {
|
||||||
|
cwd: this.projectPath,
|
||||||
|
encoding: "utf-8",
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const audit = JSON.parse(auditResult);
|
||||||
|
const vulnerabilities = audit.vulnerabilities || {};
|
||||||
|
|
||||||
|
for (const [pkg, info] of Object.entries(vulnerabilities as Record<string, { severity?: string; title?: string }>)) {
|
||||||
|
const severity = info.severity || "unknown";
|
||||||
|
if (severity === "high" || severity === "critical") {
|
||||||
|
ideas.push({
|
||||||
|
id: nextIdeaId(),
|
||||||
|
source: "external_signal",
|
||||||
|
category: "security",
|
||||||
|
title: `${severity.toUpperCase()} vulnerability in ${pkg}`,
|
||||||
|
rationale: `npm audit reports a ${severity} severity vulnerability in "${pkg}". This should be addressed immediately.`,
|
||||||
|
confidence: 0.95,
|
||||||
|
actions: ["add_security_pattern", "update_roadmap"],
|
||||||
|
tier: "mechanical",
|
||||||
|
});
|
||||||
|
} else if (severity === "moderate") {
|
||||||
|
ideas.push({
|
||||||
|
id: nextIdeaId(),
|
||||||
|
source: "external_signal",
|
||||||
|
category: "security",
|
||||||
|
title: `Moderate vulnerability in ${pkg}`,
|
||||||
|
rationale: `npm audit reports a moderate severity vulnerability in "${pkg}". Consider upgrading this dependency.`,
|
||||||
|
confidence: 0.8,
|
||||||
|
actions: ["add_security_pattern"],
|
||||||
|
tier: "mechanical",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = execSync("npm outdated --json 2>/dev/null || echo '{}'", {
|
||||||
|
cwd: this.projectPath,
|
||||||
|
encoding: "utf-8",
|
||||||
|
timeout: 15000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const outdated = JSON.parse(result);
|
||||||
|
let staleCount = 0;
|
||||||
|
for (const pkg of Object.keys(outdated)) {
|
||||||
|
staleCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (staleCount > 5) {
|
||||||
|
ideas.push({
|
||||||
|
id: nextIdeaId(),
|
||||||
|
source: "external_signal",
|
||||||
|
category: "quality",
|
||||||
|
title: `${staleCount} outdated dependencies`,
|
||||||
|
rationale: `${staleCount} packages are outdated. Consider scheduling a dependency upgrade task.`,
|
||||||
|
confidence: 0.6,
|
||||||
|
actions: ["update_roadmap"],
|
||||||
|
tier: "mechanical",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return ideas;
|
||||||
|
}
|
||||||
|
|
||||||
|
runCrossProject(): Idea[] {
|
||||||
|
resetIdeaCounter();
|
||||||
|
const ideas: Idea[] = [];
|
||||||
|
|
||||||
|
const projects = this.ciFiles.listProjects();
|
||||||
|
if (projects.length <= 1) return ideas;
|
||||||
|
|
||||||
|
const currentSlug = this.ciFiles.getProjectSlug() || projects[0].slug;
|
||||||
|
|
||||||
|
for (const project of projects) {
|
||||||
|
if (project.slug === currentSlug) continue;
|
||||||
|
|
||||||
|
const projectDir = path.join(this.projectPath, ".ciagent", project.slug);
|
||||||
|
if (!fs.existsSync(projectDir)) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const log = execSync(
|
||||||
|
`git log --all --grep="lessons:" --format="%B" -20`,
|
||||||
|
{ cwd: this.projectPath, encoding: "utf-8", timeout: 5000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const lessonsRegex = /lessons:\s*\n((?:\s+-\s+.+\n?)+)/g;
|
||||||
|
let match;
|
||||||
|
while ((match = lessonsRegex.exec(log)) !== null) {
|
||||||
|
const items = match[1].split("\n").filter((l: string) => l.trim().startsWith("-"));
|
||||||
|
for (const item of items) {
|
||||||
|
const detail = item.replace(/^\s*-\s*/, "").trim();
|
||||||
|
const topic = detail.split(":")[0].trim();
|
||||||
|
ideas.push({
|
||||||
|
id: nextIdeaId(),
|
||||||
|
source: "cross_project_lesson",
|
||||||
|
category: "improvement",
|
||||||
|
title: `Cross-project lesson from ${project.slug}: ${topic}`,
|
||||||
|
rationale: `Project "${project.slug}" learned: "${detail}". Consider whether this applies to the current project.`,
|
||||||
|
confidence: 0.6,
|
||||||
|
actions: ["add_requirement"],
|
||||||
|
tier: "cross-project",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ideas;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatIdeas(ideas: Idea[]): string {
|
||||||
|
if (ideas.length === 0) return "No improvement ideas identified for this project.";
|
||||||
|
|
||||||
|
const lines: string[] = ["Improvement Ideas:", ""];
|
||||||
|
for (const idea of ideas) {
|
||||||
|
lines.push(`[${idea.source}|${idea.confidence.toFixed(2)}] ${idea.title} — ${idea.rationale}${idea.relatedReq ? ` (req: ${idea.relatedReq})` : ""}`);
|
||||||
|
}
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
formatIdeasJson(ideas: Idea[]): IdeationResult {
|
||||||
|
const byCategory: Record<string, number> = {};
|
||||||
|
const byTier: Record<string, number> = {};
|
||||||
|
for (const idea of ideas) {
|
||||||
|
byCategory[idea.category] = (byCategory[idea.category] || 0) + 1;
|
||||||
|
byTier[idea.tier] = (byTier[idea.tier] || 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
project: this.ciFiles.getProjectSlug() || "default",
|
||||||
|
milestone: "",
|
||||||
|
ideas,
|
||||||
|
summary: {
|
||||||
|
total: ideas.length,
|
||||||
|
accepted: 0,
|
||||||
|
skipped: 0,
|
||||||
|
by_category: byCategory,
|
||||||
|
by_tier: byTier,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BackendConfigSection } from "../backends/types.js";
|
import { BackendConfigSection } from "../backends/types.js";
|
||||||
|
import { IdeationConfig, IdeationCategory } from "./ideation.js";
|
||||||
|
|
||||||
export type AutonomyLevel = "full" | "supervised" | "guided";
|
export type AutonomyLevel = "full" | "supervised" | "guided";
|
||||||
|
|
||||||
@@ -82,6 +83,7 @@ export interface ProjectEntry {
|
|||||||
export interface CIAgentConfig {
|
export interface CIAgentConfig {
|
||||||
projects: ProjectEntry[];
|
projects: ProjectEntry[];
|
||||||
active_project: string;
|
active_project: string;
|
||||||
|
active_projects: string[];
|
||||||
autonomy: AutonomyConfig;
|
autonomy: AutonomyConfig;
|
||||||
model_profile: ModelProfile;
|
model_profile: ModelProfile;
|
||||||
parallelization: ParallelizationConfig;
|
parallelization: ParallelizationConfig;
|
||||||
@@ -90,11 +92,13 @@ export interface CIAgentConfig {
|
|||||||
git: GitConfig;
|
git: GitConfig;
|
||||||
backend: BackendConfigSection;
|
backend: BackendConfigSection;
|
||||||
gitea?: GiteaConfig;
|
gitea?: GiteaConfig;
|
||||||
|
ideation?: IdeationConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_CIAGENT_CONFIG: CIAgentConfig = {
|
export const DEFAULT_CIAGENT_CONFIG: CIAgentConfig = {
|
||||||
projects: [],
|
projects: [],
|
||||||
active_project: "",
|
active_project: "",
|
||||||
|
active_projects: [],
|
||||||
autonomy: {
|
autonomy: {
|
||||||
level: "full",
|
level: "full",
|
||||||
escalation_hooks: ["deploy", "delete_data", "merge_to_main"],
|
escalation_hooks: ["deploy", "delete_data", "merge_to_main"],
|
||||||
@@ -165,4 +169,23 @@ export const DEFAULT_CIAGENT_CONFIG: CIAgentConfig = {
|
|||||||
owner: "",
|
owner: "",
|
||||||
repo: "",
|
repo: "",
|
||||||
},
|
},
|
||||||
|
ideation: {
|
||||||
|
enabled: true,
|
||||||
|
categories: ["security", "quality", "architecture", "coverage", "improvement"] as IdeationCategory[],
|
||||||
|
confidence_threshold: 0.6,
|
||||||
|
max_ideas: 20,
|
||||||
|
external_signals: {
|
||||||
|
npm_audit: true,
|
||||||
|
osv_advisories: true,
|
||||||
|
dependency_staleness: true,
|
||||||
|
},
|
||||||
|
cross_project: {
|
||||||
|
enabled: false,
|
||||||
|
similarity_weight: 0.5,
|
||||||
|
},
|
||||||
|
chaos: {
|
||||||
|
enabled: true,
|
||||||
|
scenarios: ["backend_unavailable", "requirement_change", "test_coverage_drop"],
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
export type IdeationSource =
|
||||||
|
| "uncovered_requirement"
|
||||||
|
| "repeated_lesson"
|
||||||
|
| "low_confidence_decision"
|
||||||
|
| "escalation_pattern"
|
||||||
|
| "compound_pattern"
|
||||||
|
| "partial_requirement"
|
||||||
|
| "gap_in_coverage"
|
||||||
|
| "improvement_pattern"
|
||||||
|
| "architecture_drift"
|
||||||
|
| "verification_inversion"
|
||||||
|
| "spec_ambiguity"
|
||||||
|
| "spec_contradiction"
|
||||||
|
| "spec_missing"
|
||||||
|
| "external_signal"
|
||||||
|
| "cross_project_lesson"
|
||||||
|
| "chaos_scenario";
|
||||||
|
|
||||||
|
export type IdeationCategory =
|
||||||
|
| "security"
|
||||||
|
| "quality"
|
||||||
|
| "architecture"
|
||||||
|
| "coverage"
|
||||||
|
| "improvement"
|
||||||
|
| "spec"
|
||||||
|
| "chaos";
|
||||||
|
|
||||||
|
export type IdeationAction =
|
||||||
|
| "add_requirement"
|
||||||
|
| "update_architecture"
|
||||||
|
| "update_roadmap"
|
||||||
|
| "fix_documentation"
|
||||||
|
| "add_test"
|
||||||
|
| "add_security_pattern"
|
||||||
|
| "refactor"
|
||||||
|
| "new_milestone_phase";
|
||||||
|
|
||||||
|
export type IdeationTier = "mechanical" | "backend-enriched" | "cross-project";
|
||||||
|
|
||||||
|
export interface Idea {
|
||||||
|
id: string;
|
||||||
|
source: IdeationSource;
|
||||||
|
category: IdeationCategory;
|
||||||
|
title: string;
|
||||||
|
rationale: string;
|
||||||
|
confidence: number;
|
||||||
|
relatedReq?: string;
|
||||||
|
actions: IdeationAction[];
|
||||||
|
tier: IdeationTier;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IdeationResult {
|
||||||
|
project: string;
|
||||||
|
milestone: string;
|
||||||
|
ideas: Idea[];
|
||||||
|
summary: IdeationSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IdeationSummary {
|
||||||
|
total: number;
|
||||||
|
accepted: number;
|
||||||
|
skipped: number;
|
||||||
|
by_category: Record<string, number>;
|
||||||
|
by_tier: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IdeationConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
categories: IdeationCategory[];
|
||||||
|
confidence_threshold: number;
|
||||||
|
max_ideas: number;
|
||||||
|
external_signals: {
|
||||||
|
npm_audit: boolean;
|
||||||
|
osv_advisories: boolean;
|
||||||
|
dependency_staleness: boolean;
|
||||||
|
};
|
||||||
|
cross_project: {
|
||||||
|
enabled: boolean;
|
||||||
|
similarity_weight: number;
|
||||||
|
};
|
||||||
|
chaos: {
|
||||||
|
enabled: boolean;
|
||||||
|
scenarios: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_IDEATION_CONFIG: IdeationConfig = {
|
||||||
|
enabled: true,
|
||||||
|
categories: ["security", "quality", "architecture", "coverage", "improvement"],
|
||||||
|
confidence_threshold: 0.6,
|
||||||
|
max_ideas: 20,
|
||||||
|
external_signals: {
|
||||||
|
npm_audit: true,
|
||||||
|
osv_advisories: true,
|
||||||
|
dependency_staleness: true,
|
||||||
|
},
|
||||||
|
cross_project: {
|
||||||
|
enabled: false,
|
||||||
|
similarity_weight: 0.5,
|
||||||
|
},
|
||||||
|
chaos: {
|
||||||
|
enabled: true,
|
||||||
|
scenarios: ["backend_unavailable", "requirement_change", "test_coverage_drop"],
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -7,7 +7,7 @@ import { DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
|
|||||||
|
|
||||||
describe("Type exports", () => {
|
describe("Type exports", () => {
|
||||||
it("pipeline types are importable and functional", () => {
|
it("pipeline types are importable and functional", () => {
|
||||||
expect(STAGE_ORDER).toHaveLength(8);
|
expect(STAGE_ORDER).toHaveLength(9);
|
||||||
expect(getNextStage("specify")).toBe("clarify");
|
expect(getNextStage("specify")).toBe("clarify");
|
||||||
const state = createInitialPipelineState("/tmp/test");
|
const state = createInitialPipelineState("/tmp/test");
|
||||||
expect(state.current_stage).toBe("specify");
|
expect(state.current_stage).toBe("specify");
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import {
|
|||||||
} from "../types/pipeline.js";
|
} from "../types/pipeline.js";
|
||||||
|
|
||||||
describe("STAGE_ORDER", () => {
|
describe("STAGE_ORDER", () => {
|
||||||
it("has 8 stages in correct order", () => {
|
it("has 9 stages in correct order", () => {
|
||||||
expect(STAGE_ORDER).toEqual([
|
expect(STAGE_ORDER).toEqual([
|
||||||
"specify",
|
"specify",
|
||||||
"clarify",
|
"clarify",
|
||||||
"research",
|
"research",
|
||||||
|
"ideate",
|
||||||
"plan",
|
"plan",
|
||||||
"execute",
|
"execute",
|
||||||
"test",
|
"test",
|
||||||
@@ -26,7 +27,8 @@ describe("getNextStage", () => {
|
|||||||
it("returns the next stage in sequence", () => {
|
it("returns the next stage in sequence", () => {
|
||||||
expect(getNextStage("specify")).toBe("clarify");
|
expect(getNextStage("specify")).toBe("clarify");
|
||||||
expect(getNextStage("clarify")).toBe("research");
|
expect(getNextStage("clarify")).toBe("research");
|
||||||
expect(getNextStage("research")).toBe("plan");
|
expect(getNextStage("research")).toBe("ideate");
|
||||||
|
expect(getNextStage("ideate")).toBe("plan");
|
||||||
expect(getNextStage("plan")).toBe("execute");
|
expect(getNextStage("plan")).toBe("execute");
|
||||||
expect(getNextStage("execute")).toBe("test");
|
expect(getNextStage("execute")).toBe("test");
|
||||||
expect(getNextStage("test")).toBe("verify");
|
expect(getNextStage("test")).toBe("verify");
|
||||||
@@ -51,6 +53,7 @@ describe("createInitialPipelineState", () => {
|
|||||||
expect(state.specification_loaded).toBe(false);
|
expect(state.specification_loaded).toBe(false);
|
||||||
expect(state.clarify_completed).toBe(false);
|
expect(state.clarify_completed).toBe(false);
|
||||||
expect(state.research_completed).toBe(false);
|
expect(state.research_completed).toBe(false);
|
||||||
|
expect(state.ideate_completed).toBe(false);
|
||||||
expect(state.plan_completed).toBe(false);
|
expect(state.plan_completed).toBe(false);
|
||||||
expect(state.execute_completed).toBe(false);
|
expect(state.execute_completed).toBe(false);
|
||||||
expect(state.test_completed).toBe(false);
|
expect(state.test_completed).toBe(false);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export type PipelineStage =
|
|||||||
| "specify"
|
| "specify"
|
||||||
| "clarify"
|
| "clarify"
|
||||||
| "research"
|
| "research"
|
||||||
|
| "ideate"
|
||||||
| "plan"
|
| "plan"
|
||||||
| "execute"
|
| "execute"
|
||||||
| "test"
|
| "test"
|
||||||
@@ -18,6 +19,7 @@ export interface PipelineState {
|
|||||||
specification_loaded: boolean;
|
specification_loaded: boolean;
|
||||||
clarify_completed: boolean;
|
clarify_completed: boolean;
|
||||||
research_completed: boolean;
|
research_completed: boolean;
|
||||||
|
ideate_completed: boolean;
|
||||||
plan_completed: boolean;
|
plan_completed: boolean;
|
||||||
execute_completed: boolean;
|
execute_completed: boolean;
|
||||||
test_completed: boolean;
|
test_completed: boolean;
|
||||||
@@ -61,6 +63,7 @@ export const STAGE_ORDER: PipelineStage[] = [
|
|||||||
"specify",
|
"specify",
|
||||||
"clarify",
|
"clarify",
|
||||||
"research",
|
"research",
|
||||||
|
"ideate",
|
||||||
"plan",
|
"plan",
|
||||||
"execute",
|
"execute",
|
||||||
"test",
|
"test",
|
||||||
@@ -85,6 +88,7 @@ export function createInitialPipelineState(
|
|||||||
specification_loaded: false,
|
specification_loaded: false,
|
||||||
clarify_completed: false,
|
clarify_completed: false,
|
||||||
research_completed: false,
|
research_completed: false,
|
||||||
|
ideate_completed: false,
|
||||||
plan_completed: false,
|
plan_completed: false,
|
||||||
execute_completed: false,
|
execute_completed: false,
|
||||||
test_completed: false,
|
test_completed: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user