gates: one entry point (scripts/agent_gates.py) + pre-push hook

A census of all thirteen gate scripts across the four felhom repos on 2026-08-02 found that
every check a CLAUDE.md names was passing and two of the four nobody is told to run were
failing. This repo was the extreme case: nothing ran against it at all, and its REUSE.md — 90
cited paths — was checked by no one.

agent_gates.py exists at ONE gate on purpose, so the agent is not the one repo with nowhere to
put a check and so the pre-push hook has the same entry point in all four repos. It grows when
the agent grows a second gate. The shared reuse checker stays in felhom.eu/scripts/ and is
invoked across the workspace — never copied here; an absent sibling clone FAILS the gate and
prints the path tried, which test_agent_gates.py pins by running the entry point from a lone
directory with no sibling.

.githooks/pre-push runs it with --fast and refuses the push. Per-clone and --no-verify-able,
both stated in the hook itself; a manual run WARNS when the clone is unarmed.

Tooling only: no Go change, no build, no deploy, no version bump.
This commit is contained in:
2026-08-02 15:22:58 +02:00
parent 4663df7ff3
commit 054e85a2bf
4 changed files with 245 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""agent_gates.py — THE entry point for this repo's gates. Run from the repo root:
python3 scripts/agent_gates.py # every gate
python3 scripts/agent_gates.py --fast # only gates that touch no network and no container
# runtime (what .githooks/pre-push runs)
Gates (all must pass; **non-zero exit on any failure**):
1. reuse-refs every path cited by this repo's REUSE.md still resolves
WHY THIS FILE EXISTS, WITH ONE GATE (2026-08-02, R-29 leg (b)).
A census of all thirteen gate scripts across the four felhom repos found one clean correlation:
**every check a CLAUDE.md tells a person to run was passing, and two of the four nobody is told
to run were failing** — one since 14 July. This repo was the extreme case: nothing at all ran
against it, and its REUSE.md — 90 cited paths — was checked by no one. This file exists so the
agent is not the one repo with nowhere to put a gate, and so the pre-push hook has the same entry
point in all four repos. It grows when the agent grows a second check.
THE SHARED CHECKER. `reuse_refs_check.py` lives in ONE place — `felhom.eu/scripts/` — and is
invoked here across the workspace at `<repo-root>/../felhom.eu/scripts/`. It is deliberately NOT
copied into this repo: duplicating it would recreate exactly the drift it exists to detect. If the
sibling clone is absent the gate FAILS and prints the path it tried — fail-closed, because a
runner that quietly skips a gate is the inert-seam failure this project has shipped four times.
EXIT CODES. Each gate returns 0 clean / 1 convicted / 2 inconclusive. This runner exits non-zero
if any gate is non-zero, and reports 2 distinctly as INCONCLUSIVE — an undetermined result is
never a pass, but it is not a conviction either.
"""
import os
import subprocess
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SHARED_REUSE = os.path.join(os.path.dirname(ROOT), "felhom.eu", "scripts", "reuse_refs_check.py")
# (label, absolute script path, args, fast)
GATES = [
("reuse-refs", SHARED_REUSE, [ROOT], True),
]
VERDICT = {0: "OK", 1: "FAILED", 2: "INCONCLUSIVE"}
def hooks_armed_note(root):
"""Print a WARNING (never a failure) when this clone's pre-push hook is not switched on.
core.hooksPath is local config and a clone does not carry it, so an unarmed clone is silent
by construction — this is the only place it becomes visible.
"""
try:
val = subprocess.check_output(["git", "config", "--get", "core.hooksPath"],
cwd=root, stderr=subprocess.DEVNULL).decode().strip()
except Exception:
val = ""
norm = val.replace("\\", "/").rstrip("/")
if norm == ".githooks" or norm.endswith("/.githooks"):
return
print("WARNING: this clone is UNARMED — core.hooksPath is %s, so the pre-push hook will not\n"
" run here. Switch it on once with: git config core.hooksPath .githooks"
% (("'" + val + "'") if val else "unset"))
def run_gate(label, path, args):
if not os.path.exists(path):
print("\nFAIL: gate '%s' is MISSING — tried %s" % (label, path))
print(" A missing gate is a failure, never a skip (fail-closed). The reuse-refs")
print(" checker is shared and lives in the felhom.eu sibling clone; it is never copied.")
return 1
print("\n" + "=" * 78)
print("== gate: %s (%s%s)" % (label, os.path.basename(path),
(" " + " ".join(args)) if args else ""))
print("=" * 78, flush=True)
# stream the gate's own output rather than capturing it — its diagnostics are the point.
return subprocess.call([sys.executable, path] + args, cwd=ROOT)
def main(argv):
fast = "--fast" in argv
unknown = [a for a in argv if a != "--fast"]
if unknown:
print("unknown argument(s): %s" % " ".join(unknown))
print("usage: python3 scripts/agent_gates.py [--fast]")
return 2
selected = [g for g in GATES if g[3] or not fast]
skipped = [g[0] for g in GATES if not (g[3] or not fast)]
print("agent_gates — %d gate(s)%s" % (len(selected), " [--fast]" if fast else ""))
if skipped:
print(" --fast SKIPPED (deliberate periodic runs, never in a hook): %s" % ", ".join(skipped))
hooks_armed_note(ROOT)
results = [(label, run_gate(label, path, args)) for label, path, args, _f in selected]
print("\n" + "=" * 78)
print("== summary")
print("=" * 78)
worst = 0
for label, rc in results:
print(" %-18s %-13s (exit %d)" % (label, VERDICT.get(rc, "ERROR"), rc))
if rc != 0:
worst = 1 if rc == 1 or worst == 1 else 2
if worst == 0:
print("\nall agent gates OK")
return 0
convicted = [l for l, rc in results if rc == 1]
undecided = [l for l, rc in results if rc not in (0, 1)]
if convicted:
print("\nCONVICTED: %s" % ", ".join(convicted))
if undecided:
print("UNDETERMINED (never a pass): %s" % ", ".join(undecided))
return worst
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
+67
View File
@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
"""Seam test for scripts/agent_gates.py.
Run from the repo root: python3 scripts/test_agent_gates.py
WHY THIS EXISTS. An entry point is a seam by definition: a runner that LISTS a gate but never
executes it is inert and fully green, and this project has shipped an inert seam four times. So
the assertion is on the member gate's OWN distinctive stdout — never on the runner's summary
line, which the runner can print without ever calling anything — plus the exit code, which is a
runner's actual effect.
The second test is the one that matters here: this repo's only gate lives in a SIBLING clone, so
"the sibling is missing" must be a FAILURE and not a quiet skip.
"""
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ENTRY = os.path.join(ROOT, "scripts", "agent_gates.py")
class AgentGatesTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
p = subprocess.run([sys.executable, ENTRY, "--fast"], cwd=ROOT,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
cls.rc = p.returncode
cls.out = p.stdout.decode("utf-8", "replace")
def test_exit_code_is_zero(self):
self.assertEqual(self.rc, 0, self.out)
def test_member_gate_actually_ran(self):
self.assertIn("cited paths — exact", self.out,
"the reuse-refs gate is listed but its own output never appeared — an inert "
"runner prints the summary without calling anything:\n%s" % self.out)
def test_missing_shared_checker_is_a_failure_not_a_skip(self):
"""Fail-closed. Copy the entry point into a lone directory with no felhom.eu sibling and
confirm it CONVICTS rather than reporting green with nothing run."""
tmp = tempfile.mkdtemp(prefix="agent-gates-")
try:
lone = os.path.join(tmp, "felhom-agent", "scripts")
os.makedirs(lone)
shutil.copy(ENTRY, os.path.join(lone, "agent_gates.py"))
p = subprocess.run([sys.executable, os.path.join(lone, "agent_gates.py"), "--fast"],
cwd=os.path.dirname(lone),
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
out = p.stdout.decode("utf-8", "replace")
self.assertNotEqual(p.returncode, 0, out)
self.assertIn("is MISSING — tried", out)
finally:
shutil.rmtree(tmp, ignore_errors=True)
def test_unknown_argument_is_rejected(self):
p = subprocess.run([sys.executable, ENTRY, "--nope"], cwd=ROOT,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
self.assertEqual(p.returncode, 2, p.stdout.decode("utf-8", "replace"))
if __name__ == "__main__":
unittest.main(verbosity=2)