v2.0: state enforcement, project scoping, harness integration
CI / build (push) Has been cancelled

State Enforcement (v2.0):
- .state file as single source of truth for task phase
- Approval gates for research, decomposition, design, test_design
- status.py --transition refuses illegal phase transitions
- status.py --validate-folder detects out-of-order artifacts
- status.py --audit checks all tasks for violations
- status.py --create-task is the only valid way to create tasks
- Pre-v2.0 tasks without .state are UNTRACKED -- all commands refuse them
- New --upgrade command bootstraps .state files for existing tasks

Project Scoping:
- --project flag added to all status.py commands across 16+ files
- _find_project_dir errors instead of silently falling back to ~/.automaton/
- --scope-check marks framework files OUT_OF_SCOPE when working on a project
- Dashboard handlers use stored project_root instead of re-detecting from CWD
- Prompts reference ~/.automaton/scripts/vram_detect.py (not {project}/.automaton/)

Harness Integration:
- status.py --can-edit now supports project-level checks (no --task required)
- --can-edit --file checks file scope without --task
- --json output for machine-readable harness integration
- opencode plugin (plugins/automaton-guard/plugin.ts) intercepts edit/write
- Git pre-commit hook (scripts/git-hooks/pre-commit) blocks commits without task
- Formal integration contract (contracts/harness-integration.md)

Other:
- upgrade.sh delegates to status.py --upgrade instead of manual heuristics
- Phase prompts reference --project {project} for multi-project scoping
- 200 tests passing (14 new)
This commit is contained in:
2026-06-15 14:16:46 -04:00
parent 79b783864e
commit 05c76852a2
151 changed files with 7295 additions and 632 deletions
+10
View File
@@ -0,0 +1,10 @@
{
"name": "automaton-guard",
"version": "1.0.0",
"description": "Pre-edit guard that blocks file modifications when no automaton task is in an edit-allowed phase",
"main": "plugin.ts",
"type": "module",
"peerDependencies": {
"@opencode-ai/plugin": "*"
}
}
+68
View File
@@ -0,0 +1,68 @@
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