68 lines
2.6 KiB
TypeScript
68 lines
2.6 KiB
TypeScript
import type { Plugin, PluginInput, Hooks } from "@opencode-ai/plugin"
|
|||
|
|
|
||
|
|
const STATUS_SCRIPT = process.env.HOME + "/.automaton/scripts/status.py"
|
||
|
|
const PROJECT_ROOT = process.cwd()
|
||
|
|
|
||
|
|
async function checkCanEdit(file?: string): Promise<{ allowed: boolean; reason: string; task?: string }> {
|
||
|
|
const { execSync } = await import("child_process")
|
||
|
|
let cmd = `python3 "${STATUS_SCRIPT}" --can-edit --project "${PROJECT_ROOT}"`
|
||
|
|
if (file) {
|
||
|
|
cmd += ` --file "${file}"`
|
||
|
|
}
|
||
|
|
cmd += " --json"
|
||
|
|
|
||
|
|
try {
|
||
|
|
const output = execSync(cmd, { encoding: "utf-8", timeout: 5000 })
|
||
|
|
const lines = output.trim().split("\n")
|
||
|
|
const jsonLine = lines[lines.length - 1]
|
||
|
|
const result = JSON.parse(jsonLine)
|
||
|
|
return { allowed: result.allowed, reason: result.reason, task: result.primary_task?.task }
|
||
|
|
} catch (e: any) {
|
||
|
|
if (e.status === 1) {
|
||
|
|
const stderr = (e.stderr || "").trim()
|
||
|
|
const stdout = (e.stdout || "").trim()
|
||
|
|
const lines = (stdout || stderr).split("\n")
|
||
|
|
const jsonLine = lines[lines.length - 1]
|
||
|
|
try {
|
||
|
|
const result = JSON.parse(jsonLine)
|
||
|
|
return { allowed: false, reason: result.reason }
|
||
|
|
} catch {
|
||
|
|
return { allowed: false, reason: stderr || "Denied by automaton" }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return { allowed: true, reason: "status.py not available, allowing edit" }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export default (async ({ client, project, directory }: PluginInput): Promise<Hooks> => {
|
||
|
|
return {
|
||
|
|
"tool.execute.before": async (input, output) => {
|
||
|
|
if (input.tool !== "edit" && input.tool !== "write") {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
const filePath = input.args?.file_path || input.args?.path || input.args?.[0] || ""
|
||
|
|
if (!filePath) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
const { allowed, reason, task } = await checkCanEdit(filePath)
|
||
|
|
|
||
|
|
if (!allowed) {
|
||
|
|
const msg = reason === "no_edit_tasks"
|
||
|
|
? `BLOCKED: No task in implement or doc_review phase. Create or transition a task first.`
|
||
|
|
: reason === "out_of_scope"
|
||
|
|
? `BLOCKED: File is outside the project scope.`
|
||
|
|
: reason === "wrong_phase"
|
||
|
|
? `BLOCKED: Current task is not in an edit-allowed phase. Transition it to implement or doc_review first.`
|
||
|
|
: `BLOCKED: ${reason || "Edit denied by automaton framework"}`
|
||
|
|
|
||
|
|
output.args = null as any
|
||
|
|
client.chat({
|
||
|
|
role: "user",
|
||
|
|
content: `[AUTOMATON GUARD] ${msg}\n\nTo proceed:\n1. Create a task: python ~/.automaton/scripts/status.py --create-task <name> --project ${directory}\n2. Transition it: python ~/.automaton/scripts/status.py --transition implement --task <name> --project ${directory}`,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}) satisfies Plugin
|