Fix 11 automation gaps: dead-end phases, autopilot runtime, guard plugin, status.py bugs, Category 3 audit
CI / build (push) Has been cancelled
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:
+105
-7
@@ -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.")
|
||||
|
||||
Reference in New Issue
Block a user