Drive all 4 remaining tasks to completion through full lifecycle
CI / build (push) Has been cancelled
CI / build (push) Has been cancelled
- actionable-phase-guidance: lifecycle artifacts + .state->complete - harden-enforcement-layers: pre-push hook, install-hooks.sh, register-guards.sh, prompt pre-edit checks, harness contract update, install/update/upgrade script integration - plug-stale-task-hole: lifecycle artifacts + .state->complete - port-pi-guard: pi dev guard plugin, package.json, register-guards integration All tasks passed bug_find, adversarial_bug_find, doc_review, and referee phases with PASS verdict.
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
# Automaton Guard Plugin
|
||||
|
||||
**Harness**: OpenCode (`@opencode-ai/plugin`)
|
||||
**Status**: Active
|
||||
**Enforcement**: Pre-edit (`tool.execute.before`)
|
||||
|
||||
Blocks file modifications (`edit`, `write` tools) when no automaton task is in
|
||||
implement or doc_review phase. Calls `status.py --can-edit --file <path> --json`
|
||||
before every edit.
|
||||
|
||||
## Installation
|
||||
|
||||
The framework install/update scripts auto-register this plugin in
|
||||
`~/.config/opencode/opencode.jsonc`:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": ["~/.automaton/plugins/automaton-guard"]
|
||||
}
|
||||
```
|
||||
|
||||
## Other Harnesses
|
||||
|
||||
| Harness | Plugin | Status |
|
||||
|---------|--------|--------|
|
||||
| OpenCode | `plugins/automaton-guard/plugin.ts` | ✅ Active |
|
||||
| Pi Dev | `plugins/automaton-guard-pi/guard.ts` | ✅ Active |
|
||||
| Other | N/A | N/A |
|
||||
|
||||
To add support for a new harness, create a plugin in `plugins/` that calls
|
||||
`status.py --can-edit --file <path>` before tool execution, matching the
|
||||
`contracts/harness-integration.md` specification.
|
||||
|
||||
## Enforcement Layers
|
||||
|
||||
| Layer | Mechanism | Harness scope |
|
||||
|-------|-----------|---------------|
|
||||
| Pre-edit | This plugin | OpenCode only |
|
||||
| Pre-commit | `scripts/git-hooks/pre-commit` | All git projects |
|
||||
| Pre-push | `scripts/git-hooks/pre-push` | All git projects |
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Automaton Guard — Pi Dev Harness Edition
|
||||
*
|
||||
* Pre-edit guard that calls `status.py --can-edit` before every file
|
||||
* modification. Blocks edits when no automaton task is in implement or
|
||||
* doc_review phase.
|
||||
*
|
||||
* Install:
|
||||
* pi install ~/.automaton/plugins/automaton-guard-pi
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
const STATUS_SCRIPT = `${process.env.HOME}/.automaton/scripts/status.py`;
|
||||
const STALE_THRESHOLD_MINUTES = 30;
|
||||
|
||||
interface CanEditResult {
|
||||
allowed: boolean;
|
||||
reason: string;
|
||||
task?: string;
|
||||
stale_task?: string;
|
||||
stale_minutes?: number;
|
||||
}
|
||||
|
||||
async function checkCanEdit(file: string): Promise<CanEditResult> {
|
||||
const cmd = [
|
||||
"python3",
|
||||
STATUS_SCRIPT,
|
||||
"--can-edit",
|
||||
`--project`,
|
||||
process.cwd(),
|
||||
`--file`,
|
||||
file,
|
||||
"--json",
|
||||
];
|
||||
|
||||
try {
|
||||
const { stdout, code } = await (await import("@earendil-works/pi-coding-agent")).default?.exec
|
||||
? {} as any
|
||||
: { stdout: "", code: 1 };
|
||||
|
||||
// Use child_process directly since pi.exec is only available on ExtensionAPI
|
||||
const { execSync } = await import("child_process");
|
||||
const output = execSync(cmd.join(" "), { 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,
|
||||
stale_task: result.stale_task,
|
||||
stale_minutes: result.stale_minutes,
|
||||
};
|
||||
} catch (e: any) {
|
||||
if (e.status !== undefined && e.status !== 0) {
|
||||
const text = (e.stdout || e.stderr || "").trim();
|
||||
const lines = text.split("\n");
|
||||
const jsonLine = lines[lines.length - 1];
|
||||
try {
|
||||
const result = JSON.parse(jsonLine);
|
||||
return {
|
||||
allowed: false,
|
||||
reason: result.reason,
|
||||
stale_task: result.stale_task,
|
||||
stale_minutes: result.stale_minutes,
|
||||
};
|
||||
} catch {
|
||||
return { allowed: false, reason: text || "Denied by automaton" };
|
||||
}
|
||||
}
|
||||
return { allowed: true, reason: "status.py not available, allowing edit" };
|
||||
}
|
||||
}
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
const EDIT_TOOLS = new Set(["edit", "write", "bash"]);
|
||||
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
if (!EDIT_TOOLS.has(event.toolName)) return undefined;
|
||||
|
||||
const filePath =
|
||||
event.input?.file_path ||
|
||||
event.input?.filePath ||
|
||||
event.input?.path ||
|
||||
event.input?.target ||
|
||||
"";
|
||||
|
||||
// For bash commands, skip unless it's a write/redirect operation
|
||||
if (event.toolName === "bash") {
|
||||
const cmd = event.input?.command || "";
|
||||
if (!/\b(write|tee|cp|mv|sed\b.*-i|dd\b.*of=)\b/.test(cmd)) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (!filePath) return undefined;
|
||||
|
||||
const result = await checkCanEdit(filePath);
|
||||
|
||||
if (result.allowed) return undefined;
|
||||
|
||||
if (result.reason === "stale_task") {
|
||||
if (ctx.hasUI) {
|
||||
ctx.ui.notify(
|
||||
`BLOCKED: Task '${result.stale_task}' stale for ${result.stale_minutes ?? "?"} min. Create a new task or touch it.`,
|
||||
"warning",
|
||||
);
|
||||
}
|
||||
return {
|
||||
block: true,
|
||||
reason:
|
||||
`[AUTOMATON GUARD] Task '${result.stale_task || "unknown"}' has been in edit phase for ${result.stale_minutes || "?"} minutes (stale).\n\n` +
|
||||
`To continue: python ~/.automaton/scripts/status.py --touch --task ${result.stale_task || "<task>"}\n` +
|
||||
`Or create a new task: python ~/.automaton/scripts/status.py --create-task <name>`,
|
||||
};
|
||||
}
|
||||
|
||||
const msg =
|
||||
result.reason === "no_edit_tasks"
|
||||
? "No task in implement or doc_review phase. Create or transition a task first."
|
||||
: result.reason === "out_of_scope"
|
||||
? "File is outside the project scope."
|
||||
: result.reason === "wrong_phase"
|
||||
? "Current task is not in an edit-allowed phase."
|
||||
: `Edit denied: ${result.reason}`;
|
||||
|
||||
if (ctx.hasUI) {
|
||||
ctx.ui.notify(`BLOCKED: ${msg}`, "warning");
|
||||
}
|
||||
|
||||
return {
|
||||
block: true,
|
||||
reason:
|
||||
`[AUTOMATON GUARD] ${msg}\n\n` +
|
||||
`To proceed:\n` +
|
||||
`1. Create a task: python ~/.automaton/scripts/status.py --create-task <name>\n` +
|
||||
`2. Transition it: python ~/.automaton/scripts/status.py --transition implement --task <name>`,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "automaton-guard-pi",
|
||||
"version": "1.0.0",
|
||||
"description": "Pre-edit guard for pi dev harness — blocks file edits when no automaton task is in an edit-allowed phase",
|
||||
"main": "guard.ts",
|
||||
"type": "module",
|
||||
"peerDependencies": {
|
||||
"@earendil-works/pi-coding-agent": "*"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user