import { resolveWorktree } from "./state-io.ts" import { existsSync, readFileSync } from "fs" import { join } from "path" const CODERS = new Set(["coder", "coder-hard"]) const GATED = new Set(["agenteam_triage", "agenteam_dag", "agenteam_review_gate", "task"]) function agentFromTaskArgs(args: Record): string | null { const v = args?.agent ?? args?.name ?? args?.subagent_type ?? args?.subagent return typeof v === "string" ? v : null } function fail(msg: string): never { throw new Error(`[AgenTeam] ${msg}`) } function stateFilePath(worktree: string): string { return join(resolveWorktree(worktree), ".agenteam", "state.json") } function readPhaseFromDisk(worktree: string): string { const sp = stateFilePath(worktree) try { if (existsSync(sp)) { const raw = JSON.parse(readFileSync(sp, "utf-8")) return raw.phase ?? "idle" } } catch {} return "idle" } function readFullStateFromDisk(worktree: string): any { const sp = stateFilePath(worktree) try { if (existsSync(sp)) return JSON.parse(readFileSync(sp, "utf-8")) } catch {} return { phase: "idle" } } export function createEnforcer(worktree: string) { return async ( input: { tool: string }, output: { args: Record }, ): Promise => { const t = input.tool if (!GATED.has(t)) return const ph = readPhaseFromDisk(worktree) // --- agenteam_triage: only from idle --- if (t === "agenteam_triage") { if (ph !== "idle" && ph !== "done") fail(`triage already done (phase='${ph}'). Read state to see result.`) return } // --- agenteam_dag --- if (t === "agenteam_dag") { const action = output.args?.action as string | undefined if (action === "create" && ph !== "triaged") fail(`dag(create) needs phase=triaged (current='${ph}')`) if (["next", "complete", "fail"].includes(action ?? "") && ph !== "executing") fail(`dag(${action}) needs phase=executing (current='${ph}')`) if (action === "retriage" && ph !== "executing") fail(`dag(retriage) needs phase=executing (current='${ph}')`) return } // --- agenteam_review_gate: needs executing --- if (t === "agenteam_review_gate") { if (ph !== "executing") fail(`review-gate needs phase=executing (current='${ph}')`) return } // --- Task tool: per-agent enforcement --- if (t === "task") { const agent = agentFromTaskArgs(output.args) if (!agent) return if (agent === "planner") { if (ph !== "triaged") fail(`planner needs phase=triaged (current='${ph}'). Run triage first.`) const state = readFullStateFromDisk(worktree) if (state.triage_result?.complexity !== "complex") fail(`planner not needed for simple tasks. Dispatch coder directly.`) return } if (CODERS.has(agent)) { if (ph !== "executing") fail( `${agent} needs phase=executing (current='${ph}'). ` + (ph === "triaged" ? "Create DAG first (simple: 1 task, complex: via planner)." : ""), ) const tid = output.args?.task_id as string | undefined if (tid) { const state = readFullStateFromDisk(worktree) const task = (state.tasks as any[])?.find((x: any) => x.id === tid) if (task) { if (agent === "coder" && task.tier === "smart") fail( `coder blocked: task '${tid}' is tier=smart. Use coder-hard for complex tasks.`, ) if (agent === "coder-hard" && task.tier === "cheap") fail( `coder-hard blocked: task '${tid}' is tier=cheap. Use coder for simple tasks.`, ) } } return } if (agent === "adversary") { if (ph !== "executing") fail(`adversary needs phase=executing (current='${ph}')`) return } } } }