import { tool } from "@opencode-ai/plugin" import { readState, writeState, appendLog, loadConfig, resolveWorktree, debugLog } from "../state-io.ts" import type { TaskNode } from "../types.ts" function hasCycle(tasks: TaskNode[]): boolean { const visited = new Set() const inStack = new Set() const byId = new Map(tasks.map((t) => [t.id, t])) function dfs(id: string): boolean { if (inStack.has(id)) return true if (visited.has(id)) return false visited.add(id) inStack.add(id) const task = byId.get(id) if (task) { for (const dep of task.deps) { if (byId.has(dep) && dfs(dep)) return true } } inStack.delete(id) return false } for (const t of tasks) { if (dfs(t.id)) return true } return false } const s = tool.schema const TaskInputSchema = s.object({ id: s.string(), desc: s.string(), tier: s.enum(["cheap", "smart"]), files: s.array(s.string()), deps: s.array(s.string()), }) export default tool({ description: "Manage the task DAG. " + "create: validate + store tasks (moves phase to executing). " + "next: returns tasks whose deps are all done. " + "complete/fail: marks task status. " + "retriage: change a task's tier mid-execution.", args: { action: s.enum(["create", "next", "complete", "fail", "retriage"]), tasks: s.array(TaskInputSchema).optional().describe("Required for create"), task_id: s.string().optional().describe("Required for complete/fail/retriage"), user_confirmed: s.boolean().optional().describe("Pass true if the user explicitly confirmed this plan"), retriage_tier: s.enum(["cheap", "smart"]).optional().describe("New tier for the task (retriage action)"), }, async execute(args, context) { const wt = resolveWorktree(context.worktree) const state = readState(wt) // --- CREATE --- if (args.action === "create") { const taskList = args.tasks as Array<{ id: string; desc: string; tier: "cheap" | "smart"; files: string[]; deps: string[] }> | undefined if (!taskList?.length) throw new Error("tasks required for create") const ids = taskList.map((t) => t.id) const idSet = new Set(ids) if (ids.length !== idSet.size) { const dupes = ids.filter((id, i) => ids.indexOf(id) !== i) throw new Error(`Duplicate task IDs: ${dupes.join(", ")}`) } for (const t of taskList) { for (const dep of t.deps) { if (!idSet.has(dep)) throw new Error(`Task '${t.id}' depends on unknown '${dep}'`) } } const full: TaskNode[] = taskList.map((t) => ({ ...t, status: "pending" as const, review: { attempts: 0, review_passed: false }, started_at: undefined, completed_at: undefined, })) if (hasCycle(full)) throw new Error("DAG contains a cycle") const updated = appendLog(wt, { ...state, tasks: full, phase: "executing", active_tasks: [] }, { task_id: null, event: "dag.created", detail: `${full.length} tasks` }, ) writeState(wt, updated) const ready = full.filter((t) => t.deps.length === 0).map((t) => t.id) const result: any = { valid: true, count: full.length, ready } const warnings: string[] = [] if (full.length > 5) { warnings.push(`Plan has ${full.length} tasks, recommended max is 5. Consider lazy planning in the future.`) } if (state.triage_result?.complexity === "complex" && !args.user_confirmed) { warnings.push("Complex plan created without user confirmation. Proceed with caution.") } if (warnings.length > 0) result.warning = warnings.join(" ") debugLog("tool", "dag_create", { action: "create", task_count: full.length, ready_count: ready.length, cycle_detected: false, warnings, user_confirmed: args.user_confirmed, }) return JSON.stringify(result) } // --- NEXT --- if (args.action === "next") { const done = new Set( state.tasks.filter((t) => t.status === "done").map((t) => t.id), ) const readyTasks = state.tasks.filter( (t) => t.status === "pending" && t.deps.every((d) => done.has(d)), ) let updated = state if (readyTasks.length > 0) { const tasks = [...state.tasks] for (const t of readyTasks) { const idx = tasks.findIndex(x => x.id === t.id) tasks[idx] = { ...t, status: "running", started_at: Date.now(), } } const activeTasks = readyTasks.map(t => t.id) updated = appendLog(wt, { ...state, tasks, active_tasks: activeTasks }, { task_id: null, event: "tasks.started", detail: `${activeTasks.length} tasks: ${activeTasks.join(", ")}` }, ) writeState(wt, updated) } const ready = readyTasks.map((t) => ({ id: t.id, desc: t.desc, tier: t.tier, files: t.files })) debugLog("tool", "dag_next", { action: "next", ready_count: ready.length, ready_tasks: ready, auto_started: readyTasks.length > 0, }) return JSON.stringify({ ready }) } // --- RETRIAGE --- if (args.action === "retriage") { const taskId = args.task_id as string | undefined const newTier = args.retriage_tier as "cheap" | "smart" | undefined if (!taskId || !newTier) throw new Error("retriage requires task_id and retriage_tier") const idx = state.tasks.findIndex((t) => t.id === taskId) if (idx === -1) throw new Error(`Task '${taskId}' not found`) const oldTier = state.tasks[idx].tier const tasks = [...state.tasks] tasks[idx] = { ...tasks[idx], tier: newTier } const updated = appendLog(wt, { ...state, tasks }, { task_id: taskId, event: "task.retriaged", detail: `${oldTier} -> ${newTier}` }, ) writeState(wt, updated) debugLog("tool", "dag_retriage", { task_id: taskId, old_tier: oldTier, new_tier: newTier, }) return JSON.stringify({ ok: true, task_id: taskId, old_tier: oldTier, new_tier: newTier }) } // --- COMPLETE / FAIL --- if (args.action === "complete" || args.action === "fail") { const taskId = args.task_id as string | undefined if (!taskId) throw new Error("task_id required") const idx = state.tasks.findIndex((t) => t.id === taskId) if (idx === -1) throw new Error(`Task '${taskId}' not found`) if (args.action === "complete") { const task = state.tasks[idx] if (!task.review.review_passed) { throw new Error( `Task '${taskId}' cannot be completed — review gate has not passed. Call agenteam_review_gate first.`, ) } } const newStatus = args.action === "complete" ? "done" : "failed" const tasks = [...state.tasks] tasks[idx] = { ...tasks[idx], status: newStatus, ...(args.action === "complete" ? { completed_at: Date.now() } : {}), } const remaining = tasks.filter( (t) => t.status === "pending" || t.status === "running", ).length const active_tasks = (state.active_tasks || []).filter(id => id !== taskId) const updated = appendLog(wt, { ...state, tasks, phase: remaining === 0 ? "done" : state.phase, active_tasks, }, { task_id: taskId, event: `task.${newStatus}` }, ) writeState(wt, updated) debugLog("tool", "dag_complete_fail", { action: args.action, task_id: taskId, status: newStatus, remaining_tasks: remaining, all_done: remaining === 0, }) return JSON.stringify({ ok: true, remaining, all_done: remaining === 0 }) } throw new Error(`Unknown action: ${args.action}`) }, })