#!/usr/bin/env python3 """Require PLAN/PROGRESS handoff discipline for multi-agent work.""" from __future__ import annotations import json import subprocess import sys from pathlib import Path TRACKED_PREFIXES = ( ".agents/", ".codex/", "AGENTS.md", "PLAN.md", "docs/", "phases/", "plugins/", "requirements.txt", "scripts/", "src/", "tests/", ) def git_status_names(root: Path) -> list[str]: result = subprocess.run( ["git", "status", "--porcelain"], cwd=root, capture_output=True, text=True, timeout=20, ) if result.returncode != 0: return [] names: list[str] = [] for line in result.stdout.splitlines(): if not line.strip(): continue path = line[3:].replace("\\", "/") if " -> " in path: path = path.split(" -> ", 1)[1] names.append(path) return names def is_coordination_relevant(path: str) -> bool: return any(path == prefix or path.startswith(prefix) for prefix in TRACKED_PREFIXES) def block(reason: str) -> int: json.dump({"decision": "block", "reason": reason}, sys.stdout) return 0 def main() -> int: try: payload = json.load(sys.stdin) except json.JSONDecodeError: return 0 if payload.get("stop_hook_active"): return 0 root = Path(payload.get("cwd") or ".").resolve() plan = root / "PLAN.md" progress = root / "PROGRESS.md" if not plan.exists() or not progress.exists(): return block( "Multi-agent coordination requires PLAN.md and PROGRESS.md. " "Create or restore both files before ending the turn." ) changed = git_status_names(root) if not changed: return 0 relevant = [path for path in changed if is_coordination_relevant(path)] progress_changed = "PROGRESS.md" in changed if relevant and not progress_changed: return block( "Repository planning, docs, code, tests, requirements, or .codex files changed, " "but PROGRESS.md was not updated. Add a concise handoff note so the next agent " "can see what changed, what was verified, and what remains next." ) return 0 if __name__ == "__main__": raise SystemExit(main())