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
+161
View File
@@ -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");
});
});