import type { ActiveRun } from "./types.ts"; import { readActiveRun, writeActiveRun } from "./state.ts"; import { recordVerifyResult } from "./repair.ts"; import { executeVerifyScript } from "./tools/verify-executor.ts"; import { isV2Enabled, runGateV2 } from "./gates/v2-bridge.ts"; import { createInitialLoopState, loopReducer } from "./loop/reducers.ts"; import type { LoopState } from "./loop/reducers.ts"; import { StuckDetector } from "./loop/stop-hook.ts"; import { generatePlanId } from "./loop/action-plan.ts"; import type { ActionPlan, ActionPlanConstraints } from "./loop/action-plan.ts"; import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; // ── Package scripts ─────────────────────────────────────────────────────────── interface PackageJson { scripts?: Record; } function readPackageScripts(cwd: string): Record { const pkgPath = join(cwd, "package.json"); if (!existsSync(pkgPath)) return {}; try { const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as PackageJson; return pkg.scripts ?? {}; } catch { return {}; } } // ── Goal parsing ────────────────────────────────────────────────────────────── interface ParsedGoal { script: string; } export function parseGoal(text: string): ParsedGoal | null { const runMatch = text.match(/\bnpm\s+run\s+([A-Za-z0-9:_-]+)\b/i); if (runMatch) { return { script: runMatch[1] }; } if (/\bnpm\s+test\b/i.test(text)) { return { script: "test" }; } return null; } export function resolveVerifyCommand(goal: string, cwd: string): string { const script = resolveVerifyScript(goal, cwd); return script ? `npm run ${script}` : ""; } export function resolveVerifyScript(goal: string, cwd: string): string { const packageScripts = readPackageScripts(cwd); const scripts = Object.keys(packageScripts); const goalLower = goal.toLowerCase(); const parsed = parseGoal(goal); if (parsed?.script && packageScripts[parsed.script]) return parsed.script; if (/type(script|check)|tsc/.test(goalLower)) { const found = scripts.find((s) => /check|type/.test(s)); if (found) return found; } if (/test/.test(goalLower)) { const found = scripts.find((s) => /test/.test(s)); if (found) return found; } if (/lint/.test(goalLower)) { const found = scripts.find((s) => /lint/.test(s)); if (found) return found; } if (/build/.test(goalLower)) { const found = scripts.find((s) => /build/.test(s)); if (found) return found; } return ""; } // ── Goal execution ──────────────────────────────────────────────────────────── export interface GoalResult { achieved: boolean; attempts: number; message: string; } /** * 执行验证目标(如 npm run check)。 * * 架构说明: * - Shell 执行委托给 VerifyExecutor(executeVerifyScript),不直接调用子进程接口。 * - 循环状态通过 LoopKernel 的 reducer 模式跟踪(loopReducer), * 每次迭代产生一个不可变的 LoopState 快照。 * - StuckDetector 在每次迭代后检测重复失败模式,防止修复死循环。 * - V2 门禁预检在 handleGoalCommand 中通过 runGateV2("pre-verify-command") 异步完成, * 本函数只负责执行循环本身。 * * 使用 `for` 而非 `while` 的原因: * reducer 模式下每轮迭代的状态转换是确定性的,`for` 的三段式 * (初始化 → 条件 → 递增)天然匹配 turn 计数器,避免手动维护循环变量。 */ export function executeGoal(cwd: string, run: ActiveRun, goal: string): GoalResult { const script = resolveVerifyScript(goal, cwd); const command = script ? `npm run ${script}` : ""; if (!command) { return { achieved: false, attempts: 0, message: "无法解析目标中的验证脚本。请使用 package.json 中已声明的脚本,如:/kd goal \"npm run check 通过\"" }; } const maxAttempts = run.repair?.maxAttempts ?? 3; const resumeAttempts = run.repair?.status === "repairing" ? (run.repair?.attempts ?? 0) : 0; // 使用 reducer 模式跟踪修复循环状态;StuckDetector 记录每轮动作以识别重复失败 let state: LoopState = createInitialLoopState("verify"); const stuckDetector = new StuckDetector(); for (let turn = resumeAttempts; turn < maxAttempts; turn++) { const stepAction: ActionPlan = { planId: generatePlanId(), kind: "verify" as const, intent: command, completionPromiseId: "", gateTraceId: "", parameters: { turn, script, maxAttempts } as Record, constraints: { allowWrite: false } as ActionPlanConstraints, }; console.log(`[${turn + 1}/${maxAttempts}] 执行 ${command}...`); // Act: execute via authorized executor const execResult = executeVerifyScript(cwd, script); // Observe: record verify result const outcome = recordVerifyResult(cwd, run, { command: execResult.command, exitCode: execResult.exitCode, stdout: execResult.stdout, stderr: execResult.stderr, summary: execResult.exitCode === 0 ? "验证通过" : `退出码 ${execResult.exitCode}`, }); // Stuck detection BEFORE state update (failure pattern recognition) stuckDetector.recordAction(stepAction, { actionPlanId: stepAction.planId, toolName: "npm-run", success: execResult.exitCode === 0, summary: outcome.message, output: execResult.stderr, evidencePaths: [], sourceRefs: [], timestamp: new Date().toISOString(), }); // Update: single reducer call per iteration with action + observation const observation = { actionPlanId: stepAction.planId, toolName: "npm-run", success: execResult.exitCode === 0, summary: outcome.message, output: execResult.stderr, evidencePaths: [] as string[], sourceRefs: [] as string[], timestamp: new Date().toISOString(), }; if (execResult.exitCode === 0) { state = loopReducer(state, { type: "loop.completed", payload: { message: outcome.message } }); } else { state = loopReducer(state, { type: "loop.step", payload: { action: stepAction, observation } }); } // Evaluate: check completion if (outcome.status === "passed") { return { achieved: true, attempts: turn + 1, message: `目标达成(${turn + 1}/${maxAttempts})` }; } if (outcome.status === "blocked") { state = loopReducer(state, { type: "loop.blocked", payload: { reason: `已达最大修复轮次 ${maxAttempts}` } }); return { achieved: false, attempts: turn + 1, message: `已达到最大修复轮次 ${maxAttempts},已创建阻断问题。` }; } // Stuck: trigger stop decision instead of silently continuing const stuckSig = stuckDetector.detect(); if (stuckSig && turn >= 2) { state = loopReducer(state, { type: "loop.stuck", payload: { reason: `${stuckSig.kind}:${stuckSig.value} (${stuckSig.count}/${stuckSig.threshold})`, turnsAtSameState: stuckSig.count, }, }); return { achieved: false, attempts: turn + 1, message: `检测到修复死循环:${stuckSig.kind}「${stuckSig.value.slice(0, 80)}」重复 ${stuckSig.count} 次。请变更修复策略或手动介入。`, }; } console.log(` [失败] exit ${execResult.exitCode},进入修复(${turn + 1}/${maxAttempts})`); } return { achieved: false, attempts: maxAttempts, message: `修复循环结束,状态:${run.repair?.status ?? "idle"}` }; } // ── Top-level command handler ───────────────────────────────────────────────── /** * 处理 /kd goal 命令入口。 * * V2 路径(KCODE_GATE_RUNNER_V2=1): * 先通过 runGateV2("pre-verify-command") 将验证命令提交给 GateRunner 做异步门禁校验。 * GateRunner 会检查策略注册表中的所有 pre-verify-command 策略,任一拒绝则阻断执行。 * * 旧路径(V2 未启用): * 直接进入 executeGoal 执行循环,无额外门禁。 */ export async function handleGoalCommand(cwd: string, goalText: string): Promise { const run = readActiveRun(cwd); if (!run) return "当前没有 active run。先用 /kd quick <需求> 或 /kd normal <需求> 开始。"; // V2: run pre-verify-command gate through GateRunner if (isV2Enabled()) { const command = resolveVerifyCommand(goalText, cwd); if (!command) { return `V2 门禁拒绝:无法解析目标中的验证脚本。请使用 package.json 中已声明的脚本。`; } const gateDecision = await runGateV2("pre-verify-command", cwd, run, { command, payload: { goalText }, }); if (gateDecision && !gateDecision.allowed) { const reasons = gateDecision.findings.map((f) => f.message).join("; "); return `V2 GateRunner 拒绝执行验证命令:${reasons}`; } } // Resume from interrupted repair if (run.repair?.status === "repairing" && run.repair.goal) { const result = executeGoal(cwd, run, run.repair.goal); return result.message; } // New goal const result = executeGoal(cwd, run, goalText); // Persist goal for resume if (!result.achieved) { run.repair = { ...run.repair, goal: goalText, status: run.repair?.status ?? "idle", attempts: run.repair?.attempts ?? 0, maxAttempts: run.repair?.maxAttempts ?? 3, updatedAt: new Date().toISOString() }; writeActiveRun(cwd, run); } return result.message; }