Fix 11 automation gaps: dead-end phases, autopilot runtime, guard plugin, status.py bugs, Category 3 audit
CI / build (push) Has been cancelled

- Fix decomposition:approved and human_intervention dead-end phases
- Add scripts/autopilot.py: real drive_all() implementation
- Fix guard plugin: throw Error instead of injecting user messages
- Fix status.py: double continue, _require_state, --list-states
- Add Category 3 (git-based modification) audit
- Add Category 5 (stuck-task detection) audit
- All 206 tests pass
This commit is contained in:
2026-06-15 17:42:17 -04:00
parent af66f5081d
commit 3480e4ecba
59 changed files with 679 additions and 13 deletions
+105 -7
View File
@@ -88,7 +88,7 @@ LEGAL_TRANSITIONS = {
"research:approved": ["decomposition", "design", "implement"],
"decomposition": ["decomposition:awaiting_approval"],
"decomposition:awaiting_approval": ["decomposition:approved"],
"decomposition:approved": [],
"decomposition:approved": ["complete"],
"design": ["design:awaiting_approval", "test_design", "implement"],
"design:awaiting_approval": ["design:approved"],
"design:approved": ["test_design", "implement"],
@@ -100,6 +100,7 @@ LEGAL_TRANSITIONS = {
"adversarial_bug_find": ["doc_review"],
"doc_review": ["referee"],
"referee": ["complete", "human_intervention"],
"human_intervention": ["referee", "complete"],
}
PHASE_REQUIRED_ARTIFACTS = {
@@ -518,10 +519,9 @@ def cmd_approve(args):
if not task_path.exists():
print(f"ERROR: Task '{args.task}' not found in {task_path.parent}")
return 2
current = _read_state(task_path)
current = _require_state(task_path, args.task)
if current is None:
print(f"ERROR: Cannot determine current phase for task '{args.task}'")
return 2
return 1
base = _base_phase(current)
if base not in APPROVAL_PHASES:
print(f"This phase ({base}) does not require approval.")
@@ -602,6 +602,73 @@ def _check_forbidden_artifacts(task_path: Path, phase: str) -> list[tuple[str, s
return found
def _audit_category3(project_dir, tasks):
"""Audit Category 3: Git-based unauthorized modification detection."""
import subprocess
# Collect active edit-allowed tasks
active_edit_tasks = set()
for name, path in tasks:
phase = _read_state(path)
if phase and _base_phase(phase) in ("implement", "doc_review"):
active_edit_tasks.add(name)
# Get uncommitted changes (working tree + staged)
try:
result = subprocess.run(
["git", "diff", "--name-only", "HEAD"],
capture_output=True, text=True, cwd=str(project_dir), timeout=10
)
uncommitted = [f.strip() for f in result.stdout.splitlines() if f.strip()]
except Exception as e:
print(f"[WARN] Failed to check git diff: {e}")
return 0
try:
result = subprocess.run(
["git", "diff", "--cached", "--name-only", "HEAD"],
capture_output=True, text=True, cwd=str(project_dir), timeout=10
)
staged = [f.strip() for f in result.stdout.splitlines() if f.strip()]
except Exception:
staged = []
all_changed = set(uncommitted + staged)
violations = 0
if not all_changed:
print("[PASS] No uncommitted modifications detected")
return violations
# Exclude files inside task folders
task_names = {name for name, _ in tasks}
unauthorized = set()
for changed_file in all_changed:
parts = Path(changed_file).parts
is_in_task_folder = len(parts) >= 2 and parts[0] == "tasks" and parts[1] in task_names
if not is_in_task_folder:
unauthorized.add(changed_file)
if not unauthorized:
print("[PASS] All uncommitted changes are within task folders — no unauthorized modifications")
return violations
if not active_edit_tasks:
print("[FAIL] No task in implement or doc_review phase, but uncommitted changes exist outside task folders:")
for f in sorted(unauthorized):
print(f" - {f}")
violations += 1
print(f" To allow edits: create a task and transition to implement phase")
else:
print(f"[INFO] Active edit tasks: {', '.join(sorted(active_edit_tasks))}")
print("[INFO] Uncommitted changes outside task folders exist (may be authorized if within active task scope):")
for f in sorted(unauthorized):
print(f" - {f}")
return violations
def cmd_audit(args):
project_dir = _find_project_dir(args.project)
tasks = _all_task_dirs(args.project)
@@ -699,7 +766,27 @@ def cmd_audit(args):
if not git_dir.exists():
print("Skipped: not a git repository")
else:
print("Git-based modification checking is available but requires implementation (future work)")
violations += _audit_category3(project_dir, tasks)
print("\n=== Category 5: Stuck Tasks ===")
import time as _time
stuck_threshold = 60
stuck_found = 0
for name, path in tasks:
state_file = path / ".state"
if not state_file.exists():
continue
phase = _read_state(path)
if phase is None or phase in ("complete", "human_intervention"):
continue
mtime = state_file.stat().st_mtime
age_minutes = (_time.time() - mtime) / 60
if age_minutes > stuck_threshold:
print(f"[WARN] {name}: stuck at '{phase}' for {age_minutes:.0f} minutes (threshold: {stuck_threshold} min)")
stuck_found += 1
violations += 1
if stuck_found == 0:
print(f"[PASS] No stuck tasks (threshold: {stuck_threshold} min)")
print(f"\n=== Summary ===")
total = len(tasks)
@@ -1001,7 +1088,6 @@ def cmd_next_available(args):
phase = _read_state(path)
if phase is None:
continue
continue
base = _base_phase(phase)
if phase in ("complete", "human_intervention"):
continue
@@ -1055,7 +1141,6 @@ def cmd_available(args):
phase = _read_state(path)
if phase is None:
continue
continue
base = _base_phase(phase)
if phase in ("complete", "human_intervention"):
continue
@@ -1083,6 +1168,16 @@ def cmd_available(args):
return 0
def cmd_list_states(args):
"""Print all valid phase names."""
print("Valid phases:")
for phase in VALID_PHASES:
base = _base_phase(phase)
approvals = " * requires approval" if base in APPROVAL_PHASES and phase == base else ""
print(f" {phase}{approvals}")
return 0
def main():
parser = argparse.ArgumentParser(description="Automaton status and enforcement script")
parser.add_argument("--project", help="Project root directory (defaults to CWD)")
@@ -1103,6 +1198,7 @@ def main():
parser.add_argument("--scope-check", action="store_true", help="Check if a file is in project scope")
parser.add_argument("--file", help="File path for scope check or can-edit file scope check")
parser.add_argument("--same-session", action="store_true", help="Check if task was created in current session")
parser.add_argument("--list-states", action="store_true", help="List all valid phase names")
parser.add_argument("--json", action="store_true", dest="json_output", help="Output machine-readable JSON on last line (for harness integration)")
args = parser.parse_args()
@@ -1156,6 +1252,8 @@ def main():
print("ERROR: --task is required for --same-session")
return 2
return cmd_same_session(args)
if args.list_states:
return cmd_list_states(args)
if args.task:
return cmd_show_task(args)
print("ERROR: No command specified. Use --help for usage information.")