import { tool } from "@opencode-ai/plugin" import { readState, writeState, appendLog, loadConfig, resolveWorktree, debugLog } from "../state-io.ts" const s = tool.schema export default tool({ description: "Gate task completion. Validates lint + adversary results, tracks attempts, " + "enforces retry limits. Returns: pass | retry | escalate.", args: { task_id: s.string().describe("Task ID to gate"), lint_ok: s.boolean().describe("Did lint/type checks pass?"), lint_output: s.string().optional().describe("Lint errors if lint_ok=false"), adversary: s .object({ passed: s.boolean(), critique: s.string(), }) .optional() .describe("Adversary review result. Required when always_adversary is enabled."), }, async execute(args, context) { const wt = resolveWorktree(context.worktree) const state = readState(wt) const config = loadConfig(wt) const max = config.limits.max_review_retries const requireAdversary = config.review.always_adversary const idx = state.tasks.findIndex((t) => t.id === args.task_id) if (idx === -1) throw new Error(`Task '${args.task_id}' not found`) const task = state.tasks[idx] const attempts = task.review.attempts + 1 const advResult = args.adversary as { passed: boolean; critique: string } | undefined let decision: "pass" | "retry" | "escalate" let reason: string if (!args.lint_ok) { if (attempts >= max) { decision = "escalate" reason = `Lint failed after ${attempts} attempts: ${args.lint_output ?? "no output"}` } else { decision = "retry" reason = `Lint failed: ${args.lint_output ?? "check output"}` } } else if (requireAdversary && !advResult) { decision = "retry" reason = "Adversary review required but not provided. Dispatch adversary subagent and pass its result." } else if (advResult && !advResult.passed) { if (attempts >= max) { decision = "escalate" reason = `Review failed after ${attempts} attempts: ${advResult.critique}` } else { decision = "retry" reason = `Review failed: ${advResult.critique}` } } else { decision = "pass" reason = advResult ? "All checks passed" : "Lint passed (no adversary review)" } const tasks = [...state.tasks] tasks[idx] = { ...task, review: { attempts, review_passed: decision === "pass", }, } const updated = appendLog(wt, { ...state, tasks }, { task_id: args.task_id as string, event: `review.${decision}`, detail: reason }, ) writeState(wt, updated) debugLog("tool", "review_gate", { task_id: args.task_id, lint_ok: args.lint_ok, adversary_result: advResult, decision, reason, attempts, max_attempts: max, }) return JSON.stringify({ decision, reason, attempts }) }, })