Files
ci/src/agents/doc-writer.ts
T
Jon Chery 93967feb68 feat(P05): flesh 4 agents with intrinsic mechanical logic
---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.
2026-05-29 20:30:45 +00:00

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");
}
}