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:
+153
-4
@@ -1,5 +1,16 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
||||
|
||||
interface PlanCheckResult {
|
||||
type: "missing_section" | "task_id_gap" | "missing_must_haves" | "wave_order_invalid" | "uncovered_requirement";
|
||||
severity: "P0" | "P1" | "P2";
|
||||
description: string;
|
||||
taskId?: string;
|
||||
}
|
||||
|
||||
const REQUIRED_SECTIONS = ["# Phase", "## Phase Goal", "## Plans"];
|
||||
|
||||
export class PlanCheckerAgent extends BaseAgent {
|
||||
readonly name = "plan-checker";
|
||||
readonly description = "Verifies plan quality. On ISSUES FOUND, triggers automatic plan revision (up to 3 iterations).";
|
||||
@@ -8,6 +19,7 @@ export class PlanCheckerAgent extends BaseAgent {
|
||||
async execute(context: AgentContext): Promise<AgentResult> {
|
||||
const start = Date.now();
|
||||
this.log("Checking plan quality...");
|
||||
|
||||
if (context.backend) {
|
||||
const result = await this.executeViaBackend(
|
||||
context,
|
||||
@@ -15,14 +27,151 @@ export class PlanCheckerAgent extends BaseAgent {
|
||||
);
|
||||
return { ...result, duration_ms: Date.now() - start };
|
||||
}
|
||||
|
||||
const planPath = path.join(context.project_path, ".ciagent", "PLAN.md");
|
||||
let planContent = "";
|
||||
if (fs.existsSync(planPath)) {
|
||||
planContent = fs.readFileSync(planPath, "utf-8");
|
||||
}
|
||||
|
||||
const results = this.mechanicalPlanCheck(context.project_path, planContent);
|
||||
const p0Count = results.filter((r) => r.severity === "P0").length;
|
||||
const output = this.formatResults(results);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: "Plan checking requires an intelligence backend.",
|
||||
success: p0Count === 0,
|
||||
output,
|
||||
artifacts_created: [],
|
||||
decisions: 0,
|
||||
escalations: 0,
|
||||
escalations: p0Count,
|
||||
duration_ms: Date.now() - start,
|
||||
error: "No intelligence backend available",
|
||||
error: p0Count > 0 ? `${p0Count} P0 issue(s) found` : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
mechanicalPlanCheck(projectPath: string, planContent: string): PlanCheckResult[] {
|
||||
const results: PlanCheckResult[] = [];
|
||||
|
||||
this.checkStructure(planContent, results);
|
||||
this.checkTaskIds(planContent, results);
|
||||
this.checkMustHavesPresent(planContent, results);
|
||||
this.checkWaveOrdering(planContent, results);
|
||||
this.checkRequirementCoverage(projectPath, planContent, results);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
checkStructure(planContent: string, results: PlanCheckResult[]): void {
|
||||
for (const section of REQUIRED_SECTIONS) {
|
||||
if (!planContent.includes(section)) {
|
||||
results.push({
|
||||
type: "missing_section",
|
||||
severity: "P0",
|
||||
description: `Plan is missing required section: ${section}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkTaskIds(planContent: string, results: PlanCheckResult[]): void {
|
||||
const taskIdRegex = /###?\s+Task\s+[\d.]+[:\s]+T?([\d.]+)/gi;
|
||||
const ids: number[] = [];
|
||||
let match;
|
||||
while ((match = taskIdRegex.exec(planContent)) !== null) {
|
||||
const idParts = match[1].split(".");
|
||||
const taskId = parseInt(idParts[idParts.length - 1], 10);
|
||||
if (!isNaN(taskId)) ids.push(taskId);
|
||||
}
|
||||
|
||||
if (ids.length === 0) return;
|
||||
|
||||
for (let i = 1; i <= Math.max(...ids); i++) {
|
||||
if (!ids.includes(i)) {
|
||||
results.push({
|
||||
type: "task_id_gap",
|
||||
severity: "P1",
|
||||
description: `Task ID gap: missing Task ${i}`,
|
||||
taskId: `T${i}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkMustHavesPresent(planContent: string, results: PlanCheckResult[]): void {
|
||||
const taskRegex = /###?\s+Task[^]*?(?=###?\s+Task|$)/g;
|
||||
const taskBlocks = planContent.match(taskRegex) || [];
|
||||
|
||||
for (const block of taskBlocks) {
|
||||
const headerMatch = block.match(/###?\s+Task\s+([\d.]+)/);
|
||||
if (!headerMatch) continue;
|
||||
const taskId = headerMatch[1];
|
||||
const hasMustHaves = /must.haves|acceptance.criteria|must.?have/i.test(block);
|
||||
if (!hasMustHaves) {
|
||||
results.push({
|
||||
type: "missing_must_haves",
|
||||
severity: "P1",
|
||||
description: `Task ${taskId} is missing must-haves/acceptance criteria`,
|
||||
taskId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkWaveOrdering(planContent: string, results: PlanCheckResult[]): void {
|
||||
const waveRegex = /##?\s+Wave\s+(\d+)/gi;
|
||||
const waves: number[] = [];
|
||||
let match;
|
||||
while ((match = waveRegex.exec(planContent)) !== null) {
|
||||
waves.push(parseInt(match[1], 10));
|
||||
}
|
||||
|
||||
for (let i = 1; i < waves.length; i++) {
|
||||
if (waves[i] < waves[i - 1]) {
|
||||
results.push({
|
||||
type: "wave_order_invalid",
|
||||
severity: "P0",
|
||||
description: `Wave ordering invalid: Wave ${waves[i]} appears after Wave ${waves[i - 1]}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkRequirementCoverage(projectPath: string, planContent: string, results: PlanCheckResult[]): void {
|
||||
const reqPath = path.join(projectPath, ".ciagent", "REQUIREMENTS.md");
|
||||
if (!fs.existsSync(reqPath)) return;
|
||||
|
||||
const reqContent = fs.readFileSync(reqPath, "utf-8");
|
||||
const reqIdRegex = /REQ-(\d+)/g;
|
||||
const requirements = new Set<string>();
|
||||
let reqMatch;
|
||||
while ((reqMatch = reqIdRegex.exec(reqContent)) !== null) {
|
||||
requirements.add(`REQ-${reqMatch[1]}`);
|
||||
}
|
||||
|
||||
const planReqIdRegex = /REQ-(\d+)/g;
|
||||
const coveredReqs = new Set<string>();
|
||||
let planMatch;
|
||||
while ((planMatch = planReqIdRegex.exec(planContent)) !== null) {
|
||||
coveredReqs.add(`REQ-${planMatch[1]}`);
|
||||
}
|
||||
|
||||
for (const req of requirements) {
|
||||
if (!coveredReqs.has(req)) {
|
||||
results.push({
|
||||
type: "uncovered_requirement",
|
||||
severity: "P2",
|
||||
description: `Requirement ${req} not covered in plan`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private formatResults(results: PlanCheckResult[]): string {
|
||||
if (results.length === 0) return "Plan check passed — no issues found.";
|
||||
const lines: string[] = ["Plan Check Results:", ""];
|
||||
for (const r of results) {
|
||||
lines.push(`[${r.type}|${r.severity}] ${r.description}${r.taskId ? ` (task: ${r.taskId})` : ""}`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user