feat(ci): v0.9.0 — Distribution & Expansion milestone complete
---ci---
project: ci
phase: 6
milestone: v0.9
status: complete
artifacts:
tags: [v0.9.0]
decisions:
- id: D-047
decision: v0.9 theme = Distribution & Expansion
rationale: npm publish + OpenAI/Anthropic backends + agent flesh + parallel execution
confidence: 0.92
- id: D-049
decision: Feature milestone — patch tags v0.8.1-v0.8.6 then v0.9.0
rationale: OpenAI backend, agent flesh, npm publish all feat
confidence: 0.95
- id: D-059
decision: Rename OllamaBaseBackend to LLMBaseBackend + thin OllamaBaseBackend subclass
rationale: 15 of 17 methods backend-agnostic
confidence: 0.92
- id: D-060
decision: OpenAI/Anthropic backends use native fetch() not SDK packages
rationale: No dependency bloat; fetch native in Node 18+
confidence: 0.85
- id: D-066
decision: Concurrency limiter internal (no p-limit dependency)
rationale: 15 lines; avoids dependency for trivial feature
confidence: 0.90
- id: D-067
decision: Promise.allSettled for review agents at orchestrator lines 373-400
rationale: Current sequential loop replaced with parallel execution
confidence: 0.88
requirements:
covered: [PUBLISH-01, PUBLISH-02, PUBLISH-03, PUBLISH-04, OPENAI-01, OPENAI-02, OPENAI-03, OPENAI-04, OPENAI-05, FLESH-01, FLESH-02, FLESH-03, FLESH-04, FLESH-05, ANTHROPIC-01, ANTHROPIC-02, FLESH-06, FLESH-07, NPM-01, NPM-02, PARALLEL-01, PARALLEL-02, PARALLEL-03, INTEG-01, INTEG-02, INTEG-03, INTEG-04, INTEG-05]
---/ci---
6 phases, 28 tasks, 4077 net lines added, 57 test suites, 527 tests, zero stub agents
This commit is contained in:
@@ -1,5 +1,15 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { BaseAgent, AgentContext, AgentResult } from "./base.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 {
|
||||
readonly name = "ideation-agent";
|
||||
readonly description = "Generates improvement ideas. Output feeds directly into planning pipeline.";
|
||||
@@ -8,6 +18,7 @@ export class IdeationAgent extends BaseAgent {
|
||||
async execute(context: AgentContext): Promise<AgentResult> {
|
||||
const start = Date.now();
|
||||
this.log("Generating improvement ideas...");
|
||||
|
||||
if (context.backend) {
|
||||
const result = await this.executeViaBackend(
|
||||
context,
|
||||
@@ -15,14 +26,167 @@ export class IdeationAgent extends BaseAgent {
|
||||
);
|
||||
return { ...result, duration_ms: Date.now() - start };
|
||||
}
|
||||
|
||||
const ideas = this.mechanicalIdeate(context.project_path);
|
||||
const output = this.formatIdeas(ideas);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: "Ideation requires an intelligence backend.",
|
||||
success: true,
|
||||
output,
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
};
|
||||
}
|
||||
|
||||
mechanicalIdeate(projectPath: string): Idea[] {
|
||||
const ideas: Idea[] = [];
|
||||
const uncoveredReqs = this.readUncoveredRequirements(projectPath);
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user