Files

172 lines
6.7 KiB
TypeScript

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 },
};
},
});
}