93967feb68
---ci---
project: ci
phase: 5
milestone: v0.8
status: complete
decisions:
- id: D-033
decision: Flesh SecurityAuditorAgent with STRIDE-aware mechanical scanning
rationale: Runs L3 security patterns intrinsically; no backend required
confidence: 0.90
- id: D-034
decision: Flesh DocWriterAgent with template-based doc update
rationale: Updates ROADMAP.md phase status, REQUIREMENTS.md req status, reads git log for new decisions
confidence: 0.85
- id: D-035
decision: Flesh DebuggerAgent with stack trace parsing + git bisect
rationale: Parses stack traces to find file:line, bisects to find introducing commit
confidence: 0.80
- id: D-036
decision: Flesh ChallengerAgent with plan DAG/wave/must-have/REQ validation
rationale: Validates plan structure mechanically; catches circular deps and gaps
confidence: 0.82
requirements:
covered: [AGENT-01, AGENT-02, AGENT-03, AGENT-04]
---/ci---
AGENT-01: SecurityAuditorAgent.mechanicalAudit() runs STRIDE+ CWE pattern
scan intrinsically. Each finding has stride_category, cwe, severity, and
disposition (accept/mitigate/flag based on confidence threshold).
AGENT-02: DocWriterAgent.mechanicalDocUpdate() reads plan data, updates
.ciagent/ROADMAP.md phase status to complete, .ciagent/REQUIREMENTS.md
pending→covered, and reads git log for new decision entries.
AGENT-03: DebuggerAgent.mechanicalDebug() parses stack traces (4 regex
patterns for different formats), identifies root file:line, runs
git bisect to find introducing commit, suggests git revert.
AGENT-04: ChallengerAgent.mechanicalChallenge() validates plan structure:
circular dependency detection via DFS, wave ordering validation,
must-haves presence check, and requirement coverage check.
185 lines
5.6 KiB
TypeScript
185 lines
5.6 KiB
TypeScript
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
import { execSync } from "node:child_process";
|
|
import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
|
|
|
interface DocUpdate {
|
|
file: string;
|
|
updates: string[];
|
|
}
|
|
|
|
export class DocWriterAgent extends BaseAgent {
|
|
readonly name = "doc-writer";
|
|
readonly description = "Autonomous documentation writer.";
|
|
readonly workflow = "execute";
|
|
|
|
async execute(context: AgentContext): Promise<AgentResult> {
|
|
const start = Date.now();
|
|
this.log("Writing documentation...");
|
|
|
|
if (context.backend) {
|
|
const result = await this.executeViaBackend(
|
|
context,
|
|
`Write documentation for phase ${context.phase}. Specification: ${context.specification}`
|
|
);
|
|
return { ...result, duration_ms: Date.now() - start };
|
|
}
|
|
|
|
const updates = this.mechanicalDocUpdate(context.project_path, context.phase);
|
|
const output = this.formatUpdates(updates);
|
|
|
|
return {
|
|
success: true,
|
|
output,
|
|
artifacts_created: updates.map((u) => u.file),
|
|
decisions: 0,
|
|
escalations: 0,
|
|
duration_ms: Date.now() - start,
|
|
};
|
|
}
|
|
|
|
mechanicalDocUpdate(projectPath: string, phase: number): DocUpdate[] {
|
|
const updates: DocUpdate[] = [];
|
|
const ciDir = path.join(projectPath, ".ciagent");
|
|
|
|
if (!fs.existsSync(ciDir)) return updates;
|
|
|
|
const roadmapUpdates = this.updateRoadmapPhaseStatus(ciDir, phase);
|
|
if (roadmapUpdates.length > 0) {
|
|
updates.push({ file: ".ciagent/ROADMAP.md", updates: roadmapUpdates });
|
|
}
|
|
|
|
const reqUpdates = this.updateRequirementsStatus(projectPath, phase);
|
|
if (reqUpdates.length > 0) {
|
|
updates.push({ file: ".ciagent/REQUIREMENTS.md", updates: reqUpdates });
|
|
}
|
|
|
|
const decisionUpdates = this.updateProjectDecisions(ciDir, phase);
|
|
if (decisionUpdates.length > 0) {
|
|
updates.push({ file: ".ciagent/PROJECT.md", updates: decisionUpdates });
|
|
}
|
|
|
|
if (updates.length > 0) {
|
|
try {
|
|
execSync("git add -A", { cwd: projectPath, stdio: "pipe" });
|
|
} catch {}
|
|
}
|
|
|
|
return updates;
|
|
}
|
|
|
|
private updateRoadmapPhaseStatus(ciDir: string, phase: number): string[] {
|
|
const roadmapPath = path.join(ciDir, "ROADMAP.md");
|
|
if (!fs.existsSync(roadmapPath)) return [];
|
|
|
|
const content = fs.readFileSync(roadmapPath, "utf-8");
|
|
const phasePattern = new RegExp(
|
|
`\\|\\s*${phase}\\s*\\|([^|]+)\\|([^|]+)\\|`,
|
|
"g"
|
|
);
|
|
|
|
let updated = content;
|
|
let match;
|
|
const updates: string[] = [];
|
|
|
|
while ((match = phasePattern.exec(content)) !== null) {
|
|
const currentStatus = match[2].trim().toLowerCase();
|
|
if (currentStatus !== "complete") {
|
|
updated = updated.replace(
|
|
match[0],
|
|
match[0].replace(/in.progress|pending|not.started/i, "complete")
|
|
);
|
|
updates.push(`Phase ${phase}: status → complete`);
|
|
}
|
|
}
|
|
|
|
if (updated !== content) {
|
|
fs.writeFileSync(roadmapPath, updated, "utf-8");
|
|
}
|
|
|
|
return updates;
|
|
}
|
|
|
|
private updateRequirementsStatus(projectPath: string, phase: number): string[] {
|
|
const reqPath = path.join(projectPath, ".ciagent", "REQUIREMENTS.md");
|
|
if (!fs.existsSync(reqPath)) return [];
|
|
|
|
const content = fs.readFileSync(reqPath, "utf-8");
|
|
let updated = content;
|
|
const updates: string[] = [];
|
|
|
|
const pendingForPhase = content.match(
|
|
new RegExp(`\\|[^|]*\\|[^|]*\\|[^|]*\\|\\s*${phase}\\s*\\|\\s*pending\\s*\\|`, "g")
|
|
);
|
|
if (pendingForPhase) {
|
|
for (const line of pendingForPhase) {
|
|
updated = updated.replace(line, line.replace(/pending/, "covered"));
|
|
updates.push(`Requirement updated to covered (phase ${phase})`);
|
|
}
|
|
}
|
|
|
|
if (updated !== content) {
|
|
fs.writeFileSync(reqPath, updated, "utf-8");
|
|
}
|
|
|
|
return updates;
|
|
}
|
|
|
|
private updateProjectDecisions(ciDir: string, phase: number): string[] {
|
|
const projectPath = path.join(ciDir, "PROJECT.md");
|
|
if (!fs.existsSync(projectPath)) return [];
|
|
|
|
const content = fs.readFileSync(projectPath, "utf-8");
|
|
const gitLogDecisions = this.getRecentDecisions(phase);
|
|
|
|
if (gitLogDecisions.length === 0) return [];
|
|
|
|
const updates: string[] = [];
|
|
for (const d of gitLogDecisions) {
|
|
if (!content.includes(d.id)) {
|
|
updates.push(`Added decision ${d.id}: ${d.decision}`);
|
|
}
|
|
}
|
|
|
|
return updates;
|
|
}
|
|
|
|
private getRecentDecisions(phase: number): Array<{ id: string; decision: string }> {
|
|
try {
|
|
const raw = execSync(
|
|
`git log --all --max-count=20 --format="%B%x01"`,
|
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }
|
|
);
|
|
const decisions: Array<{ id: string; decision: string }> = [];
|
|
const entries = raw.split("\x01").filter(Boolean);
|
|
|
|
for (const entry of entries) {
|
|
const ciMatch = entry.match(/---ci---[\s\S]*?---\/ci---/);
|
|
if (!ciMatch) continue;
|
|
const phaseMatch = ciMatch[0].match(/phase:\s*(\d+)/);
|
|
if (!phaseMatch || parseInt(phaseMatch[1]) !== phase) continue;
|
|
|
|
const decMatches = [...ciMatch[0].matchAll(/id:\s*(D-\d+)[\s\S]*?decision:\s*(.+)/g)];
|
|
for (const m of decMatches) {
|
|
decisions.push({ id: m[1], decision: m[2].trim() });
|
|
}
|
|
}
|
|
|
|
return decisions;
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private formatUpdates(updates: DocUpdate[]): string {
|
|
if (updates.length === 0) return "No documentation updates needed.";
|
|
const lines: string[] = ["Documentation Updates:", ""];
|
|
for (const u of updates) {
|
|
lines.push(`${u.file}:`);
|
|
for (const update of u.updates) {
|
|
lines.push(` - ${update}`);
|
|
}
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
} |