import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import { execSync } from "child_process"; import { existsSync, readFileSync, readdirSync } from "fs"; import { join } from "path"; import { homedir } from "os"; const STATUS_SCRIPT = join(homedir(), ".automaton", "scripts", "status.py"); function resolveProjectDir(cwd: string): string | null { const inFramework = cwd === join(homedir(), ".automaton") || cwd.startsWith(join(homedir(), ".automaton") + "/"); if (inFramework) return homedir() + "/.automaton"; if (existsSync(join(cwd, ".automaton"))) return cwd; return null; } function getTasks(projectDir: string) { const tasksDir = join(projectDir, ".automaton", "tasks"); if (!existsSync(tasksDir)) return []; const tasks: { name: string; phase: string }[] = []; for (const entry of readdirSync(tasksDir)) { const stateFile = join(tasksDir, entry, ".state"); if (existsSync(stateFile)) { try { const phase = readFileSync(stateFile, "utf-8").trim(); if (phase) tasks.push({ name: entry, phase }); } catch {} } } tasks.sort((a, b) => a.name.localeCompare(b.name)); return tasks; } export default function (pi: ExtensionAPI) { pi.registerTool({ name: "automaton_create_task", label: "Create Automaton Task", description: "Create a new automaton task and transition it to the specified phase. " + "Use this when the user asks you to do work that requires file edits — you need a task in 'implement' or 'doc_review' phase before you can edit files.", promptSnippet: "Create automaton tasks for work management", promptGuidelines: [ "When the user asks you to build, implement, fix, or change something, first call automaton_create_task to create a task and transition it to 'implement' phase", "Only after the task is in 'implement' can you edit files — the guard will block edits otherwise", "Use a descriptive task name based on what the user wants (e.g., 'add-login-page', 'fix-api-timeout')", "Default phase is 'implement' — omit phase for most cases", ], parameters: Type.Object({ name: Type.String({ minLength: 1, description: "Task name (kebab-case, e.g. 'add-login-page')" }), phase: Type.Optional(Type.String({ default: "implement", description: "Phase to transition to after creation" })), }), async execute(_toolCallId, params, _signal, _onUpdate, _ctx) { const { name, phase = "implement" } = params; const projectDir = resolveProjectDir(process.cwd()); if (!projectDir) { return { content: [{ type: "text", text: "Not in an automaton-managed project. No .automaton/ directory found." }], details: {}, }; } try { const createOut = execSync( `python3 ${STATUS_SCRIPT} --create-task ${JSON.stringify(name)} --project ${JSON.stringify(projectDir)}`, { encoding: "utf-8", timeout: 10000 }, ); const transitionOut = execSync( `python3 ${STATUS_SCRIPT} --transition ${phase} --task ${JSON.stringify(name)} --project ${JSON.stringify(projectDir)}`, { encoding: "utf-8", timeout: 10000 }, ); return { content: [{ type: "text", text: `Created task "${name}" and transitioned to "${phase}".` }], details: { task: name, phase }, }; } catch (e: any) { return { content: [{ type: "text", text: `Failed to create task: ${e.stderr || e.message || String(e)}`, }], details: { error: e.stderr || e.message, task: name }, }; } }, }); pi.registerTool({ name: "automaton_transition", label: "Transition Automaton Task", description: "Transition an existing automaton task to a new phase. " + "Valid phases: research, decomposition, design, implement, test_design, testing, doc_review, complete. " + "Use this to move a task forward (e.g. from research to implement).", promptSnippet: "Transition automaton tasks between phases", parameters: Type.Object({ task: Type.String({ minLength: 1, description: "Task name" }), phase: Type.String({ minLength: 1, description: "Target phase" }), }), async execute(_toolCallId, params, _signal, _onUpdate, _ctx) { const { task, phase } = params; const projectDir = resolveProjectDir(process.cwd()); if (!projectDir) { return { content: [{ type: "text", text: "Not in an automaton-managed project." }], details: {}, }; } try { execSync( `python3 ${STATUS_SCRIPT} --transition ${phase} --task ${JSON.stringify(task)} --project ${JSON.stringify(projectDir)}`, { encoding: "utf-8", timeout: 10000 }, ); return { content: [{ type: "text", text: `Task "${task}" transitioned to "${phase}".` }], details: { task, phase }, }; } catch (e: any) { return { content: [{ type: "text", text: `Failed to transition task: ${e.stderr || e.message || String(e)}`, }], details: { error: e.stderr || e.message, task, phase }, }; } }, }); pi.registerTool({ name: "automaton_status", label: "Automaton Status", description: "Show current automaton project status — all tasks, their phases, and any running loops. " + "Call this to check what state things are in before deciding what to do.", promptSnippet: "Check automaton project status", parameters: Type.Object({}), async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) { const projectDir = resolveProjectDir(process.cwd()); if (!projectDir) { return { content: [{ type: "text", text: "Not in an automaton-managed project." }], details: {}, }; } const tasks = getTasks(projectDir); const editTasks = tasks.filter((t) => t.phase === "implement" || t.phase === "doc_review"); const currentTask = editTasks.length > 0 ? editTasks[0] : tasks.length > 0 ? tasks[0] : null; const lines: string[] = []; lines.push(`Project: ${projectDir.split("/").pop()}`); if (currentTask) { lines.push(`Current: "${currentTask.name}" (${currentTask.phase})`); } else { lines.push("No tasks. Create one with automaton_create_task."); } lines.push(""); for (const t of tasks) { const marker = currentTask && t.name === currentTask.name ? ">" : " "; lines.push(`${marker} ${t.name.padEnd(35)} ${t.phase}`); } return { content: [{ type: "text", text: lines.join("\n") }], details: { tasks: tasks.length, current: currentTask?.name || null }, }; }, }); }