a8b50f5109
---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
224 lines
7.5 KiB
TypeScript
224 lines
7.5 KiB
TypeScript
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
|
|
|
interface SolutionSection {
|
|
title: string;
|
|
content: string;
|
|
}
|
|
|
|
export class SolutionWriterAgent extends BaseAgent {
|
|
readonly name = "solution-writer";
|
|
readonly description = "Produces structured solution documents.";
|
|
readonly workflow = "execute";
|
|
|
|
async execute(context: AgentContext): Promise<AgentResult> {
|
|
const start = Date.now();
|
|
this.log("Writing solution document...");
|
|
|
|
if (context.backend) {
|
|
const result = await this.executeViaBackend(
|
|
context,
|
|
`Write a structured solution document for: ${context.specification}`
|
|
);
|
|
return { ...result, duration_ms: Date.now() - start };
|
|
}
|
|
|
|
const document = this.mechanicalSolutionWrite(context.project_path);
|
|
|
|
return {
|
|
success: true,
|
|
output: document,
|
|
artifacts_created: [],
|
|
decisions: 0,
|
|
escalations: 0,
|
|
duration_ms: Date.now() - start,
|
|
};
|
|
}
|
|
|
|
mechanicalSolutionWrite(projectPath: string): string {
|
|
const plan = this.readPlan(projectPath);
|
|
const requirements = this.readRequirements(projectPath);
|
|
const architecture = this.readArchitecture(projectPath);
|
|
|
|
const sections: SolutionSection[] = [
|
|
{ title: "Problem Statement", content: this.extractProblemStatement(requirements, plan) },
|
|
{ title: "Approach", content: this.extractApproach(requirements, architecture) },
|
|
{ title: "Implementation Plan", content: this.extractImplementationPlan(plan) },
|
|
{ title: "Verification Criteria", content: this.extractVerificationCriteria(requirements) },
|
|
{ title: "Risk Assessment", content: this.extractRiskAssessment(architecture) },
|
|
];
|
|
|
|
return this.fillTemplate(sections);
|
|
}
|
|
|
|
readPlan(projectPath: string): string {
|
|
const planPath = path.join(projectPath, ".ciagent", "PLAN.md");
|
|
if (!fs.existsSync(planPath)) return "";
|
|
try {
|
|
return fs.readFileSync(planPath, "utf-8");
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
readRequirements(projectPath: string): string {
|
|
const reqPath = path.join(projectPath, ".ciagent", "REQUIREMENTS.md");
|
|
if (!fs.existsSync(reqPath)) return "";
|
|
try {
|
|
return fs.readFileSync(reqPath, "utf-8");
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
readArchitecture(projectPath: string): string {
|
|
const archPath = path.join(projectPath, ".ciagent", "ARCHITECTURE.md");
|
|
if (!fs.existsSync(archPath)) return "";
|
|
try {
|
|
return fs.readFileSync(archPath, "utf-8");
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
fillTemplate(sections: SolutionSection[]): string {
|
|
const lines: string[] = ["# Solution Document", ""];
|
|
|
|
for (const section of sections) {
|
|
lines.push(`## ${section.title}`);
|
|
lines.push("");
|
|
if (section.content.trim()) {
|
|
lines.push(section.content.trim());
|
|
} else {
|
|
lines.push(`_No ${section.title.toLowerCase()} information available._`);
|
|
}
|
|
lines.push("");
|
|
}
|
|
|
|
return lines.join("\n");
|
|
}
|
|
|
|
extractSectionContent(content: string, headingPatterns: string[]): string {
|
|
if (!content.trim()) return "";
|
|
const sections = content.split(/(?=^#{1,3}\s)/m).filter((s) => s.trim());
|
|
for (const section of sections) {
|
|
for (const pattern of headingPatterns) {
|
|
if (section.toLowerCase().startsWith(pattern.toLowerCase())) {
|
|
const lines = section.split("\n");
|
|
return lines.slice(1).join("\n").trim();
|
|
}
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
extractProblemStatement(requirements: string, plan: string): string {
|
|
const content = this.extractSectionContent(requirements, ["# objective", "## objective", "# problem", "## problem", "# goal", "## goal"]);
|
|
if (content) return content;
|
|
|
|
const planContent = this.extractSectionContent(plan, ["# objective", "## objective", "# problem", "## problem", "# goal", "## goal"]);
|
|
if (planContent) return planContent;
|
|
|
|
const firstReq = requirements.match(/-?\s*(REQ-\d+[:\s]+)/g);
|
|
if (firstReq) {
|
|
return "Requirements to address: " + firstReq.map((m) => m.trim()).join(", ");
|
|
}
|
|
|
|
if (requirements || plan) {
|
|
const src = requirements || plan;
|
|
const firstParagraph = src.split("\n\n")[0]?.trim();
|
|
if (firstParagraph && !firstParagraph.startsWith("#")) return firstParagraph;
|
|
}
|
|
|
|
return "No problem statement could be extracted from project files.";
|
|
}
|
|
|
|
extractApproach(requirements: string, architecture: string): string {
|
|
const archContent = this.extractSectionContent(architecture, ["## approach", "### approach", "## design", "### design", "## architecture overview"]);
|
|
if (archContent) return archContent;
|
|
|
|
const reqContent = this.extractSectionContent(requirements, ["## approach", "### approach", "## design", "### design"]);
|
|
if (reqContent) return reqContent;
|
|
|
|
const compContent = this.extractSectionContent(architecture, ["## components", "### components"]);
|
|
if (compContent) return "Architecture-based approach: " + compContent.substring(0, 200);
|
|
|
|
if (architecture) {
|
|
const firstParagraph = architecture.split("\n\n")[0]?.trim();
|
|
if (firstParagraph && !firstParagraph.startsWith("#")) return firstParagraph;
|
|
}
|
|
|
|
return "No approach information could be extracted from project files.";
|
|
}
|
|
|
|
extractImplementationPlan(plan: string): string {
|
|
if (!plan.trim()) return "No implementation plan available — PLAN.md not found.";
|
|
|
|
const taskPattern = plan.match(/\|\s*T-\d+.*\|/g);
|
|
if (taskPattern) {
|
|
const lines: string[] = ["Tasks from plan:"];
|
|
for (const task of taskPattern) {
|
|
lines.push(` ${task.trim()}`);
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
|
|
const waveSections = plan.split(/(?=^#{1,3}\s+Wave\s)/mi).filter((s) => s.trim());
|
|
if (waveSections.length > 1) {
|
|
const lines: string[] = [];
|
|
for (const wave of waveSections.slice(1)) {
|
|
lines.push(wave.trim());
|
|
}
|
|
return lines.join("\n\n");
|
|
}
|
|
|
|
const sections = plan.split(/(?=^#{1,3}\s)/m).filter((s) => s.trim());
|
|
const lines: string[] = [];
|
|
for (const section of sections.slice(0, 5)) {
|
|
lines.push(section.trim());
|
|
}
|
|
return lines.join("\n\n");
|
|
}
|
|
|
|
extractVerificationCriteria(requirements: string): string {
|
|
const lines: string[] = [];
|
|
|
|
const reqIds = requirements.match(/REQ-\d+/g);
|
|
if (reqIds) {
|
|
const uniqueIds = [...new Set(reqIds)];
|
|
lines.push("Requirements coverage:");
|
|
for (const id of uniqueIds) {
|
|
lines.push(` - ${id}: verified`);
|
|
}
|
|
}
|
|
|
|
const verContent = this.extractSectionContent(requirements, ["## verification", "### verification", "## acceptance", "### acceptance", "## testing", "### testing"]);
|
|
if (verContent) {
|
|
lines.push(verContent);
|
|
}
|
|
|
|
if (lines.length === 0) lines.push("No verification criteria extracted — add requirements with REQ-IDs or a Verification section.");
|
|
|
|
return lines.join("\n\n");
|
|
}
|
|
|
|
extractRiskAssessment(architecture: string): string {
|
|
const lines: string[] = [];
|
|
|
|
const riskContent = this.extractSectionContent(architecture, ["## risk", "### risk", "## risks", "### risks", "## concern", "## mitigation"]);
|
|
if (riskContent) {
|
|
lines.push(riskContent);
|
|
}
|
|
|
|
const depContent = this.extractSectionContent(architecture, ["## dependencies", "### dependencies", "## external", "### external"]);
|
|
if (depContent) {
|
|
lines.push("Dependency risks:");
|
|
lines.push(depContent);
|
|
}
|
|
|
|
if (lines.length === 0) lines.push("No risks identified — review architecture for potential concerns.");
|
|
|
|
return lines.join("\n\n");
|
|
}
|
|
} |