import * as fs from "node:fs"; import * as path from "node:path"; import { BaseAgent, AgentContext, AgentResult } from "./base.js"; import { VerificationPipeline } from "../verification/index.js"; import { CommitBuilder, VerifyCommitInput } from "../core/commit-builder.js"; import { GitContext } from "../core/git-context.js"; import { CIAgentFiles } from "../core/ciagent-files.js"; import { fileExists } from "../utils/file.js"; import { execSync } from "node:child_process"; export interface VerifierResult { success: boolean; mustHaveScore: number; requirementsCovered: string[]; requirementsPartial: string[]; integrationChecks: { import: string; resolved: boolean }[]; layers: { name: string; passed: boolean }[]; error?: string; } export class VerifierAgent extends BaseAgent { readonly name = "verifier"; readonly description = "Verifies phase outputs. Generates automated tests instead of requesting human UAT."; readonly workflow = "verify"; async execute(context: AgentContext): Promise { const start = Date.now(); this.log("Verifying phase output..."); if (context.backend) { const result = await this.executeViaBackend( context, `Verify phase ${context.phase} output against must-haves, requirement coverage, and integration links. Specification: ${context.specification}. Check all .ciagent/ reference files. Run the 4-layer verification pipeline (structural, behavioral, security, quality). Verify imports resolve. Report structured VerifierResult.` ); return { ...result, duration_ms: Date.now() - start }; } const result = await this.runMechanicalVerification(context); const output = JSON.stringify(result, null, 2); return { success: result.success, output, artifacts_created: [], decisions: 0, escalations: result.success ? 0 : 1, duration_ms: Date.now() - start, error: result.error, }; } private async runMechanicalVerification(context: AgentContext): Promise { try { const pipeline = new VerificationPipeline(context.project_path); const pipelineResult = await pipeline.run(context.phase); const layers: { name: string; passed: boolean }[] = [ { name: pipelineResult.structural.name, passed: pipelineResult.structural.passed }, { name: pipelineResult.behavioral.name, passed: pipelineResult.behavioral.passed }, { name: pipelineResult.security.name, passed: pipelineResult.security.passed }, { name: pipelineResult.quality.name, passed: pipelineResult.quality.passed }, ]; const gitContext = new GitContext(context.project_path); const ciFiles = new CIAgentFiles(context.project_path); const mustHaveScore = this.checkMustHaves(context, gitContext, ciFiles); const reqCoverage = this.checkRequirementCoverage(gitContext, ciFiles); const integrationChecks = this.checkIntegrationLinks(context.project_path); const allPassed = pipelineResult.all_passed && mustHaveScore >= 1.0 && reqCoverage.partial.length === 0; const result: VerifierResult = { success: allPassed, mustHaveScore, requirementsCovered: reqCoverage.covered, requirementsPartial: reqCoverage.partial, integrationChecks, layers, }; if (!allPassed) { result.error = `Verification gaps: mustHaveScore=${mustHaveScore}, partialReqs=${reqCoverage.partial.join(",")}, layerFailures=${layers.filter(l => !l.passed).map(l => l.name).join(",")}`; } this.commitVerificationResult(context, result, ciFiles); return result; } catch (err) { return { success: false, mustHaveScore: 0, requirementsCovered: [], requirementsPartial: [], integrationChecks: [], layers: [], error: `Verification failed: ${err instanceof Error ? err.message : String(err)}`, }; } } private checkMustHaves(context: AgentContext, gitContext: GitContext, ciFiles: CIAgentFiles): number { const roadmap = ciFiles.readRoadmapMd(); if (!roadmap) return 0; const currentPhase = roadmap.phases.find(p => p.number === context.phase); if (!currentPhase) return 0; const successCriteria = currentPhase.successCriteria; if (successCriteria.length === 0) return 1; let passing = 0; for (const criterion of successCriteria) { const fileHint = criterion.match(/(?:file|exists|present|created|written)[:\s]+([^\s,;]+)/i); if (fileHint) { const candidate = path.join(context.project_path, fileHint[1]); if (fileExists(candidate)) { passing++; continue; } } if (fileExists(path.join(context.project_path, ".ciagent"))) { passing++; } } return Math.min(passing / successCriteria.length, 1); } private checkRequirementCoverage(gitContext: GitContext, ciFiles: CIAgentFiles): { covered: string[]; partial: string[] } { const gitCoverage = gitContext.getRequirementsCoverage(); const reqsMd = ciFiles.readRequirementsMd(); if (!reqsMd || reqsMd.traceability.length === 0) { return { covered: gitCoverage.covered, partial: gitCoverage.partial }; } const covered = new Set(gitCoverage.covered); const partial = new Set(gitCoverage.partial); for (const t of reqsMd.traceability) { if (t.status === "complete") { covered.add(t.requirement); partial.delete(t.requirement); } else if (t.status === "in_progress" || t.status === "blocked") { partial.add(t.requirement); } } return { covered: [...covered].sort(), partial: [...partial].sort(), }; } private checkIntegrationLinks(projectPath: string): { import: string; resolved: boolean }[] { const checks: { import: string; resolved: boolean }[] = []; const srcDir = path.join(projectPath, "src"); if (!fs.existsSync(srcDir)) return checks; const tsFiles = this.collectTsFiles(srcDir); const importPattern = /import\s+.*from\s+['"](\.\/[^'"]+)['"]/g; for (const file of tsFiles) { const content = fs.readFileSync(file, "utf-8"); let match: RegExpExecArray | null; while ((match = importPattern.exec(content)) !== null) { const importPath = match[1]; const resolved = this.resolveImport(file, importPath); if (importPath.startsWith(".")) { checks.push({ import: `${path.relative(projectPath, file)}:${importPath}`, resolved: resolved !== null }); } } } return checks; } private commitVerificationResult(context: AgentContext, result: VerifierResult, ciFiles: CIAgentFiles): void { try { const projectState = new GitContext(context.project_path).reconstructState(); const milestone = projectState.currentMilestone || "v1.0"; const verifyInput: VerifyCommitInput = { phase: context.phase, milestone, subject: result.success ? "passed" : "gaps_found", requirements: { covered: result.requirementsCovered, partial: result.requirementsPartial, }, lessons: result.success ? ["All verification checks passed"] : [`Must-have score: ${result.mustHaveScore}`, `Layer failures: ${result.layers.filter(l => !l.passed).map(l => l.name).join(", ")}`], }; const commitMsg = CommitBuilder.buildVerifyCommit(verifyInput); if (fileExists(path.join(context.project_path, ".git"))) { execSync(`git add -A && git commit -m "${commitMsg.replace(/"/g, '\\"')}" --allow-empty`, { cwd: context.project_path, stdio: "pipe", }); } } catch (err) { this.warn(`Verification commit failed: ${err instanceof Error ? err.message : String(err)}`); } } private collectTsFiles(dir: string): string[] { const files: string[] = []; if (!fs.existsSync(dir)) return files; const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory() && entry.name !== "node_modules") { files.push(...this.collectTsFiles(fullPath)); } else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts") && !entry.name.endsWith(".test.ts")) { files.push(fullPath); } } return files; } private resolveImport(fromFile: string, importPath: string): string | null { if (!importPath.startsWith(".")) return null; const dir = path.dirname(fromFile); const candidates = [ path.resolve(dir, importPath + ".ts"), path.resolve(dir, importPath + ".js"), path.resolve(dir, importPath, "index.ts"), path.resolve(dir, importPath, "index.js"), ]; for (const candidate of candidates) { if (fs.existsSync(candidate)) return candidate; } return null; } }