062a7027ab
gates / gates (push) Successful in 8s
Surgical corrections only; the file is deliberately NOT restructured (deferred). Deleted the expired TEMPORARY block. It read "felhom-pve is at a remote site (until ~2026-08-02) ... Delete this block on return" and was still being read as current fact on 2026-08-06, four days past its own deadline, while felhom-controller/CLAUDE.md asserted the opposite. The location-independence fact worth keeping (localapi binds 169.254.253.1:8443 on vmbr9 since the R-50 island migration) moved to an HTML comment. Every component version literal is gone from effective text, including the --version reading and the go.mod Go directive. Versions change several times a day; ask the hub's /hosts + /configs or the box. The drill-VM claim and the host addresses now point at documentation/operations/nodes.md, which already stated both correctly. This file's drill-VM claim was the correct one -- confirmed by qm list on demo-hp. The R-115/R-188/R-186 release narratives moved to an HTML comment and to the felhom-build-deploy skill; the directives stayed (never hand-roll the build; the build -> tag -> publish -> push order; reproducible -trimpath -buildvcs=false). The health-check block-I/O rule became .claude/rules/health-checks.md, scoped to the five packages where health checks are written. It had been duplicated from felhom.eu/CLAUDE.md with a note explaining that that file does not load in an agent-only session -- correct reasoning, made obsolete by path-scoped rules. agent_gates.py registers the shared instructions gate. Docs only -- no Go, no version bump, nothing built or deployed. Ledger: felhom.eu/documentation/audits/LEDGER-instruction-trim-2026-08-06.md Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJc8sAGRWmavP3rMtdpkr2
129 lines
5.9 KiB
Python
129 lines
5.9 KiB
Python
#!/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
|
|
2. published every `v<semver>` tag has a downloadable package AND a tag tree that serves
|
|
the agent's configs (R-115). NEEDS NETWORK, so it is **not** in `--fast` and
|
|
the pre-push hook does not run it — a push must not fail because Gitea blinked
|
|
or because someone is offline on a train. CI runs the FULL set for exactly this
|
|
reason: it is the machine that can afford a network check, and it is the half
|
|
that emails when something is wrong.
|
|
|
|
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")
|
|
SHARED_INSTRUCTIONS = os.path.join(
|
|
os.path.dirname(ROOT), "felhom.eu", "scripts", "instructions_gate.py")
|
|
|
|
# (label, absolute script path, args, fast)
|
|
GATES = [
|
|
("reuse-refs", SHARED_REUSE, [ROOT], True),
|
|
("instructions", SHARED_INSTRUCTIONS, [ROOT], True),
|
|
("published", os.path.join(ROOT, "scripts", "check-published-versions.py"), [], False),
|
|
]
|
|
|
|
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:]))
|