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:
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/bin/sh
|
||||
# pre-push — refuse a push that carries a broken gate. (2026-08-02, R-29 leg (b) first half.)
|
||||
#
|
||||
# Runs this repo's ONE gate entry point in --fast mode: only checks that touch no network and no
|
||||
# container runtime, so a push stays a push and never pulls images or starts containers. The slow
|
||||
# gates stay deliberate periodic runs; a hook that takes minutes gets bypassed within a week and
|
||||
# the bypass becomes the habit.
|
||||
#
|
||||
# BOTH LINES BELOW ARE DELIBERATE. An absent log line is not evidence a hook ran — a silent pass is
|
||||
# equally consistent with "gates green" and "hook never fired", so a passing push says so out loud.
|
||||
#
|
||||
# HONEST LIMITS, stated so this is not mistaken for enforcement it cannot provide:
|
||||
# * per-clone — core.hooksPath is local config and a clone does not carry it. Arm a clone once:
|
||||
# git config core.hooksPath .githooks
|
||||
# Any manual entry-point run WARNS when the clone is unarmed.
|
||||
# * skippable — `git push --no-verify` bypasses this entirely. That is on purpose: an escape
|
||||
# hatch that cannot be reached is one that gets removed the first time it is
|
||||
# inconvenient. USING IT MUST BE STATED IN THE SESSION REPORT.
|
||||
# The half that is neither per-clone nor skippable is CI — felhom.eu OPEN-ITEMS.md R-168.
|
||||
#
|
||||
# Measured 2026-08-02 (git 2.47.3): a relative core.hooksPath resolves correctly and the hook's cwd
|
||||
# is the repo root whether `git push` is issued from the root or from any subdirectory. The
|
||||
# explicit rev-parse below does not depend on that.
|
||||
set -u
|
||||
|
||||
root=$(git rev-parse --show-toplevel 2>/dev/null) || {
|
||||
echo "pre-push: FAIL - cannot resolve the repo root (git rev-parse --show-toplevel)." >&2
|
||||
exit 1
|
||||
}
|
||||
cd "$root" || exit 1
|
||||
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
echo "pre-push: FAIL - python3 not found, so the gates CANNOT run. This is a failure, never a" >&2
|
||||
echo " pass by default. Install python3, or push with --no-verify and say so." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "pre-push [felhom-agent]: running scripts/agent_gates.py --fast ..."
|
||||
python3 "scripts/agent_gates.py" --fast
|
||||
rc=$?
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
echo "pre-push [felhom-agent]: PUSH REFUSED - gates exited $rc. Fix the finding above, or bypass with" >&2
|
||||
echo " 'git push --no-verify' and state that you did in the session report." >&2
|
||||
else
|
||||
echo "pre-push [felhom-agent]: gates OK - push proceeding."
|
||||
fi
|
||||
exit $rc
|
||||
@@ -143,6 +143,19 @@ All shippable work commits **directly to `main`**; `main` equals what is deploye
|
||||
measurement lives in `felhom.eu/CLAUDE.md` "Code quality rules"; it is repeated here because health
|
||||
checks are written in THIS repo and that file does not load in an agent-only session. R-117 spike §6.3.
|
||||
- Update `REUSE.md` if you added/changed/deprecated a shared helper or pattern (same commit).
|
||||
- **Run `python3 scripts/agent_gates.py` from the repo root after ANY change in this repo.** It is
|
||||
the ONE entry point for this repo's gates. Today it runs one — `reuse_refs_check` over this
|
||||
repo's `REUSE.md` — and it exists at one gate on purpose: a census 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, and
|
||||
this repo was the extreme case, with nothing running against it at all and 90 cited paths checked
|
||||
by no one. It grows when the agent grows a second check. `--fast` selects the gates that touch no
|
||||
network and no container runtime; today that is all of them. A missing gate is a FAILURE, never a
|
||||
skip. **The shared `reuse_refs_check.py` lives in `felhom.eu/scripts/` and is never copied here**
|
||||
— a copy would recreate the drift it detects; an absent sibling clone FAILS the gate.
|
||||
**The pre-push hook** (`.githooks/pre-push`) runs it with `--fast` and refuses a failing push. It
|
||||
is per-clone — switch it on once with `git config core.hooksPath .githooks`, and a manual run
|
||||
WARNS when this clone is unarmed. `git push --no-verify` bypasses it deliberately; **say so in the
|
||||
session report when you use it.** Both facts are why CI is still owed (`OPEN-ITEMS.md` R-168).
|
||||
- Testing doctrine (non-hollow tests, red-proofs, seams): use the `felhom-testing` skill.
|
||||
- **Logging**: the slog logger fans out to journald (configured level) + the always-DEBUG `applog.Ring`
|
||||
(remote pulls) — English, keys-never-values, durations on outcomes; full rules in
|
||||
|
||||
@@ -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:]))
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user