import type { ActiveRun } from "./types.ts"; import { defaultArtifactContent, ensureArtifact, readArtifact, writeArtifact } from "./artifacts.ts"; import { writeEvidenceFile } from "./evidence.ts"; import { inspectGate } from "./gates.ts"; import { appendLedgerEvent } from "./ledger.ts"; import { writeActiveRun } from "./state.ts"; export interface VerifyResultInput { command: unknown; exitCode: unknown; stdout?: unknown; stderr?: unknown; summary?: unknown; } export interface VerifyResultOutcome { status: "passed" | "repairing" | "blocked" | "rejected"; run: ActiveRun; evidencePath?: string; message: string; } interface NormalizedVerifyResultInput { command: string; exitCode: number; stdout: string; stderr: string; summary: string; } const DEFAULT_MAX_REPAIR_ATTEMPTS = 3; export function recordVerifyResult(cwd: string, run: ActiveRun, input: VerifyResultInput): VerifyResultOutcome { const blockReason = verifyResultBlockReason(run); if (blockReason) { appendLedgerEvent(cwd, run, { type: "verify.recorded", summary: blockReason, data: { status: "rejected" }, }); return { status: "rejected", run, message: blockReason, }; } const normalized = normalizeInput(input); const inputProblem = verifyInputBlockReason(normalized); if (inputProblem) { appendLedgerEvent(cwd, run, { type: "verify.recorded", summary: inputProblem, data: { status: "rejected", command: normalized.command }, }); return { status: "rejected", run, message: inputProblem, }; } if (normalized.exitCode === 0) { return recordVerifyPass(cwd, run, normalized); } return recordVerifyFailure(cwd, run, normalized); } function recordVerifyPass(cwd: string, run: ActiveRun, input: NormalizedVerifyResultInput): VerifyResultOutcome { const evidence = "evidence/verify-pass.md"; const evidencePath = writeEvidenceFile(cwd, run, evidence, formatVerifyEvidence("验证通过", input), { kind: "verify-pass", command: input.command, exitCode: input.exitCode, }); appendVerifyRecord(cwd, run, "验证通过", evidence, input); closeRepairQuestions(run, evidence); run.repair = { attempts: 0, maxAttempts: run.repair?.maxAttempts ?? DEFAULT_MAX_REPAIR_ATTEMPTS, status: "idle", updatedAt: new Date().toISOString(), }; run.gate = inspectGate(cwd, run); writeActiveRun(cwd, run); appendLedgerEvent(cwd, run, { type: "verify.recorded", summary: `验证通过:${input.command}`, data: { status: "passed", command: input.command, exitCode: input.exitCode, evidence }, }); return { status: "passed", run, evidencePath, message: `验证通过,已记录证据:${evidence}`, }; } function recordVerifyFailure(cwd: string, run: ActiveRun, input: NormalizedVerifyResultInput): VerifyResultOutcome { const maxAttempts = run.repair?.maxAttempts ?? DEFAULT_MAX_REPAIR_ATTEMPTS; const attempts = (run.repair?.attempts ?? 0) + 1; const evidence = `evidence/verify-failure-${String(attempts).padStart(3, "0")}.md`; const signature = failureSignature(input); const evidencePath = writeEvidenceFile(cwd, run, evidence, formatVerifyEvidence("验证失败", input), { kind: "verify-failure", command: input.command, exitCode: input.exitCode, }); appendVerifyRecord(cwd, run, "验证失败", evidence, input); if (attempts >= maxAttempts) { run.phase = "verify"; closeRepairQuestions(run, evidence); run.repair = { attempts, maxAttempts, lastFailureEvidence: evidence, lastFailureSignature: signature, status: "blocked", updatedAt: new Date().toISOString(), }; run.questions = [ ...(Array.isArray(run.questions) ? run.questions : []), { id: `Q-${String((run.questions?.length ?? 0) + 1).padStart(3, "0")}`, phase: "verify", question: `验证失败已达到 ${maxAttempts} 轮。选择继续修复、回到 plan 调整范围或停止。`, reason: `最近失败证据:${evidence}`, choices: ["继续修复", "回到 plan", "停止"], blocking: true, status: "open", createdAt: new Date().toISOString(), }, ]; run.gate = inspectGate(cwd, run); writeActiveRun(cwd, run); appendLedgerEvent(cwd, run, { type: "verify.recorded", summary: `验证失败达到上限:${input.command}`, data: { status: "blocked", command: input.command, exitCode: input.exitCode, evidence, attempts, maxAttempts }, }); return { status: "blocked", run, evidencePath, message: `验证失败已达到 ${maxAttempts} 轮,已记录 ${evidence} 并创建阻断问题。`, }; } run.phase = "execute"; ensureArtifact(cwd, run, "execute", defaultArtifactContent("execute", run.goal, run.profile)); appendExecuteRepairRecord(cwd, run, evidence, attempts, maxAttempts); run.repair = { attempts, maxAttempts, lastFailureEvidence: evidence, lastFailureSignature: signature, status: "repairing", updatedAt: new Date().toISOString(), }; run.gate = inspectGate(cwd, run); writeActiveRun(cwd, run); appendLedgerEvent(cwd, run, { type: "verify.recorded", summary: `验证失败,进入自动修复:${input.command}`, data: { status: "repairing", command: input.command, exitCode: input.exitCode, evidence, attempts, maxAttempts }, }); return { status: "repairing", run, evidencePath, message: `验证失败,已记录 ${evidence},自动回到 execute 修复(${attempts}/${maxAttempts})。`, }; } function appendVerifyRecord(cwd: string, run: ActiveRun, title: string, evidence: string, input: NormalizedVerifyResultInput): void { const existing = readArtifact(cwd, run, "verify") ?? defaultArtifactContent("verify", run.goal, run.profile); const section = [ "", `### ${title} - ${new Date().toISOString()}`, "", `- 命令:${input.command}`, `- Exit:${input.exitCode}`, `- 证据:${evidence}`, input.summary ? `- 摘要:${input.summary}` : undefined, "", ].filter(Boolean).join("\n"); writeArtifact(cwd, run, "verify", `${existing.trimEnd()}\n${section}`); } function appendExecuteRepairRecord(cwd: string, run: ActiveRun, evidence: string, attempts: number, maxAttempts: number): void { const existing = readArtifact(cwd, run, "execute") ?? defaultArtifactContent("execute", run.goal, run.profile); const section = [ "", "## 自动修复循环", "", `- 修复轮次:${attempts}/${maxAttempts}`, `- 失败证据:${evidence}`, "- 下一步:读取失败证据,分析失败原因,只在 PLAN.md 批准文件内修复;涉及未批准文件时回到 plan 更新计划。", "", ].join("\n"); writeArtifact(cwd, run, "execute", `${existing.trimEnd()}\n${section}`); } function formatVerifyEvidence(title: string, input: NormalizedVerifyResultInput): string { return [ `# ${title}`, "", `Captured: ${new Date().toISOString()}`, `Command: ${input.command}`, `Exit: ${input.exitCode}`, input.summary ? `Summary: ${input.summary}` : undefined, input.stdout.trim() ? `\nSTDOUT:\n${input.stdout.trim()}` : undefined, input.stderr.trim() ? `\nSTDERR:\n${input.stderr.trim()}` : undefined, "", ].filter(Boolean).join("\n"); } function normalizeInput(input: VerifyResultInput): NormalizedVerifyResultInput { const exitCode = typeof input.exitCode === "number" ? input.exitCode : Number(input.exitCode); return { command: normalizeText(input.command).trim(), exitCode: Number.isFinite(exitCode) ? Math.trunc(exitCode) : 1, stdout: normalizeText(input.stdout), stderr: normalizeText(input.stderr), summary: normalizeText(input.summary), }; } function normalizeText(value: unknown): string { return typeof value === "string" ? value : ""; } function verifyResultBlockReason(run: ActiveRun): string | undefined { if (run.phase === "verify") return undefined; if (run.phase === "execute" && run.repair?.status === "repairing") return undefined; return `kd_verify_result 只能在 verify 阶段记录,或在自动修复循环的 execute 阶段记录。当前阶段:${run.phase}。`; } function verifyInputBlockReason(input: NormalizedVerifyResultInput): string | undefined { if (!input.command || /^(unknown|待确认|未知|todo|tbd|n\/a)$/i.test(input.command)) { return "kd_verify_result 缺少真实验证命令,拒绝记录证据。下一步:运行 PLAN.md 中声明的验证命令;外部系统或人工验证必须记录用户提供的命令、环境、测试数据和证据来源。"; } return undefined; } function closeRepairQuestions(run: ActiveRun, evidence: string): void { if (!Array.isArray(run.questions)) return; const now = new Date().toISOString(); for (const question of run.questions) { if (question.status !== "open" || !question.blocking || question.phase !== "verify") continue; if (!isRepairQuestion(question.question, question.reason)) continue; question.status = "answered"; question.answer = `验证已继续处理,最新证据:${evidence}`; question.answeredAt = now; } } function isRepairQuestion(question: string, reason?: string): boolean { return /验证失败已达到/.test(question) || /最近失败证据:evidence\/verify-failure-\d+\.md/.test(reason ?? ""); } function failureSignature(input: NormalizedVerifyResultInput): string { const text = [input.stderr, input.stdout, input.summary].join("\n").replace(/\s+/g, " ").trim(); return `${input.exitCode}:${text.slice(0, 300)}`; }