#!/usr/bin/env python3 """gofmt_gate.py — every Go file in this module is gofmt-clean (R-1160; the controller's R-454 gate, same shape). MEASURED 2026-10-19: 14 agent files were not gofmt-clean and nothing noticed (fixed by one formatting-only commit, 1edfd8e). The controller's history below is why a gate and not a one-off clean-up. MEASURED 2026-09-02: five internal/web test files had been gofmt-unclean for an unknown length of time and nothing noticed — `go vet` does not check formatting and no gate did. By 2026-10-05 the five were clean and TWELVE others were not: the count only grows while nothing looks, and every `gofmt -l` someone runs by hand is then noise that hides a real one. Run from the repo root: python3 scripts/gofmt_gate.py Exit 0 clean · 1 convicted (lists the files) · 2 inconclusive (gofmt not found — never a pass). gofmt is the Go toolchain's own formatter (not a shell utility), found on PATH or under `go env GOROOT`/bin. Decoy: scripts/test_gate_decoys.py ("gofmt"), in a scratch copy of this script. """ import os import shutil import subprocess import sys ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ROOTS = ["cmd", "internal"] def find_gofmt(): p = shutil.which("gofmt") if p: return p go = shutil.which("go") if go: try: root = subprocess.check_output([go, "env", "GOROOT"], text=True).strip() cand = os.path.join(root, "bin", "gofmt") if os.path.isfile(cand): return cand except Exception: pass return None def in_ci(): """True on the Gitea Actions runner (act sets GITHUB_ACTIONS, Gitea sets GITEA_ACTIONS).""" return os.environ.get("GITEA_ACTIONS") == "true" or os.environ.get("GITHUB_ACTIONS") == "true" def main(): gofmt = find_gofmt() if not gofmt: # The CI runner may have no Go toolchain (felhom.eu CLAUDE.md, R-887 note). There the gate # says so OUT LOUD and passes: the pre-push hook on the developer's machine, where Go always exists, is where # formatting is enforced. Anywhere else a missing gofmt stays INCONCLUSIVE — never a silent pass. # Pinned by the "gofmt: CI without Go" and "gofmt: dev without Go" decoys in test_gate_decoys.py. if in_ci(): print("gofmt gate NOT CHECKED in CI: no Go toolchain on the runner — the pre-push hook (with Go) enforces it") return 0 print("gofmt gate INCONCLUSIVE: gofmt not found on PATH or under `go env GOROOT`/bin") return 2 roots = [r for r in ROOTS if os.path.isdir(os.path.join(ROOT, r))] if not roots: print("gofmt gate INCONCLUSIVE: none of %s exists under %s" % (ROOTS, ROOT)) return 2 p = subprocess.run([gofmt, "-l"] + roots, cwd=ROOT, capture_output=True, text=True) if p.returncode != 0: print("gofmt gate INCONCLUSIVE: gofmt exited %d: %s" % (p.returncode, p.stderr.strip()[:400])) return 2 bad = [l for l in p.stdout.splitlines() if l.strip()] if bad: for f in bad: print(" not gofmt-clean: %s" % f) print("GOFMT GATE FAILED: %d file(s) — run `gofmt -w ` (formatting only, no behaviour change)" % len(bad)) return 1 print("gofmt gate OK — every Go file under %s is gofmt-clean" % ", ".join(roots)) return 0 if __name__ == "__main__": sys.exit(main())