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:
+110
-3
@@ -1,5 +1,16 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||
|
||||
interface PhaseDefinition {
|
||||
number: number;
|
||||
name: string;
|
||||
description: string;
|
||||
requirements: string[];
|
||||
dependencies: number[];
|
||||
successCriteria: string[];
|
||||
}
|
||||
|
||||
export class RoadmapperAgent extends BaseAgent {
|
||||
readonly name = "roadmapper";
|
||||
readonly description = "Creates and maintains project roadmaps.";
|
||||
@@ -8,6 +19,7 @@ export class RoadmapperAgent extends BaseAgent {
|
||||
async execute(context: AgentContext): Promise<AgentResult> {
|
||||
const start = Date.now();
|
||||
this.log("Creating roadmap...");
|
||||
|
||||
if (context.backend) {
|
||||
const result = await this.executeViaBackend(
|
||||
context,
|
||||
@@ -15,14 +27,109 @@ export class RoadmapperAgent extends BaseAgent {
|
||||
);
|
||||
return { ...result, duration_ms: Date.now() - start };
|
||||
}
|
||||
|
||||
const phases = this.mechanicalRoadmapGenerate(context.project_path);
|
||||
const output = this.formatPhases(phases);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: "Roadmap creation requires an intelligence backend.",
|
||||
success: true,
|
||||
output,
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
};
|
||||
}
|
||||
|
||||
mechanicalRoadmapGenerate(projectPath: string): PhaseDefinition[] {
|
||||
const requirements = this.readRequirements(projectPath);
|
||||
const grouped = this.groupRequirementsByPhase(requirements);
|
||||
const phases = this.assignPhases(grouped);
|
||||
return phases.map((phase) => ({
|
||||
...phase,
|
||||
successCriteria: this.generateSuccessCriteria(phase),
|
||||
}));
|
||||
}
|
||||
|
||||
readRequirements(projectPath: string): Array<{ id: string; phase: number; text: string }> {
|
||||
const reqPath = path.join(projectPath, ".ciagent", "REQUIREMENTS.md");
|
||||
if (!fs.existsSync(reqPath)) return [];
|
||||
|
||||
const content = fs.readFileSync(reqPath, "utf-8");
|
||||
const requirements: Array<{ id: string; phase: number; text: string }> = [];
|
||||
|
||||
const reqBlockRegex = /REQ-(\d+)[^]*?(?=REQ-\d+|$)/g;
|
||||
let match;
|
||||
while ((match = reqBlockRegex.exec(content)) !== null) {
|
||||
const block = match[0];
|
||||
const id = `REQ-${match[1]}`;
|
||||
const phaseMatch = block.match(/phase[:\s]+(\d+)/i);
|
||||
const phase = phaseMatch ? parseInt(phaseMatch[1], 10) : 1;
|
||||
const textMatch = block.match(/(?:title|description|requirement)[:\s]+(.+)/i);
|
||||
const text = textMatch ? textMatch[1].trim() : id;
|
||||
requirements.push({ id, phase, text });
|
||||
}
|
||||
|
||||
return requirements;
|
||||
}
|
||||
|
||||
groupRequirementsByPhase(requirements: Array<{ id: string; phase: number; text: string }>): Record<number, Array<{ id: string; text: string }>> {
|
||||
const groups: Record<number, Array<{ id: string; text: string }>> = {};
|
||||
for (const req of requirements) {
|
||||
if (!groups[req.phase]) {
|
||||
groups[req.phase] = [];
|
||||
}
|
||||
groups[req.phase].push({ id: req.id, text: req.text });
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
assignPhases(grouped: Record<number, Array<{ id: string; text: string }>>): PhaseDefinition[] {
|
||||
const phaseNumbers = Object.keys(grouped).map(Number).sort((a, b) => a - b);
|
||||
if (phaseNumbers.length === 0) return [];
|
||||
|
||||
return phaseNumbers.map((num, idx) => {
|
||||
const reqs = grouped[num];
|
||||
const dependencies = idx === 0 ? [] : [phaseNumbers[idx - 1]];
|
||||
return {
|
||||
number: num,
|
||||
name: `Phase ${num}`,
|
||||
description: `Implementation phase ${num} covering ${reqs.length} requirement(s).`,
|
||||
requirements: reqs.map((r) => r.id),
|
||||
dependencies,
|
||||
successCriteria: [],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
generateSuccessCriteria(phase: PhaseDefinition): string[] {
|
||||
const criteria: string[] = [];
|
||||
for (const reqId of phase.requirements) {
|
||||
criteria.push(`${reqId} fully implemented and verified`);
|
||||
}
|
||||
if (phase.requirements.length > 0) {
|
||||
criteria.push("All tests passing for phase requirements");
|
||||
}
|
||||
if (phase.dependencies.length > 0) {
|
||||
criteria.push(`Phase ${phase.dependencies[0]} completion confirmed`);
|
||||
}
|
||||
return criteria;
|
||||
}
|
||||
|
||||
private formatPhases(phases: PhaseDefinition[]): string {
|
||||
if (phases.length === 0) return "No phases generated — no requirements found.";
|
||||
const lines: string[] = ["Roadmap:", ""];
|
||||
for (const phase of phases) {
|
||||
lines.push(`Phase ${phase.number}: ${phase.name}`);
|
||||
lines.push(` Description: ${phase.description}`);
|
||||
lines.push(` Requirements: ${phase.requirements.join(", ") || "none"}`);
|
||||
lines.push(` Dependencies: ${phase.dependencies.map(String).join(", ") || "none"}`);
|
||||
lines.push(` Success Criteria:`);
|
||||
for (const criterion of phase.successCriteria) {
|
||||
lines.push(` - ${criterion}`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user