#!/usr/bin/env python3 """ Normalize gate artifact JSON schema — minimal `agent` field fix. History: F-123..F-135 (13 features x 3 gate files = 39 files) were closed with the wrong field name (`reviewer` instead of `agent`). `verify.sh` rejects those files because it checks `obj.get('agent') == ''`. This script is idempotent: it adds `agent` from the legacy `reviewer` field (when present and matching) without touching any other field. Re-running it on an already-normalized file is a no-op. `stage` is not required by `verify.sh`, so we leave it alone. Usage: python3 scripts/fix_gate_schema.py # all artifacts python3 scripts/fix_gate_schema.py F-123 F-124 # specific features python3 scripts/fix_gate_schema.py --dry-run # preview only """ from __future__ import annotations import json import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] ARTIFACTS_DIR = ROOT / 'work' / 'artifacts' FILE_TO_AGENT = { 'reviewer.json': 'reviewer', 'security.json': 'security', 'qa.json': 'qa', } def normalize(feature_dir: Path, dry_run: bool = False) -> list[str]: """Normalize gate files in `feature_dir`. Returns list of changes made.""" changes: list[str] = [] fid = feature_dir.name for filename, expected_agent in FILE_TO_AGENT.items(): path = feature_dir / filename if not path.is_file(): continue try: data = json.loads(path.read_text(encoding='utf-8')) except Exception: continue if data.get('agent') == expected_agent: continue # already correct, no-op legacy = data.get('reviewer') if isinstance(legacy, str) and legacy == expected_agent: data['agent'] = legacy changes.append(f"{fid}/{filename}: copied agent='{expected_agent}' from legacy 'reviewer' field") else: data['agent'] = expected_agent changes.append(f"{fid}/{filename}: set agent='{expected_agent}'") if not dry_run: path.write_text( json.dumps(data, indent=2, ensure_ascii=False) + '\n', encoding='utf-8', ) return changes def main() -> int: args = [a for a in sys.argv[1:] if not a.startswith('--')] dry_run = '--dry-run' in sys.argv if args: targets = [ARTIFACTS_DIR / a for a in args] for t in targets: if not t.is_dir(): print(f"[WARN] {t} is not a directory, skipping") targets = [t for t in targets if t.is_dir()] else: targets = sorted(p for p in ARTIFACTS_DIR.iterdir() if p.is_dir()) total_changes: list[str] = [] for d in targets: total_changes.extend(normalize(d, dry_run=dry_run)) if not total_changes: print('[OK] All gate files already conform to schema (agent field present)') return 0 verb = 'Would fix' if dry_run else 'Fixed' print(f"{verb} {len(total_changes)} file(s):") for c in total_changes: print(f" - {c}") return 0 if __name__ == '__main__': sys.exit(main())