# -*- coding: utf-8 -*- """Golden-currency gate (R-242) — a controller release is not DELIVERED until a golden carries it. Run from the repo root: python3 scripts/golden_currency_gate.py Exit 0 clean · 1 convicted (a released controller has no golden) · 2 inconclusive. WHY THIS EXISTS, and why a rule was not enough. R-242 was filed on 2026-08-07 as a mechanism-less rule: *a controller release that changes customer-visible behaviour is not finished until a golden carries it, and nothing enforces that.* It was deliberately recorded and not built. **It recurred the next day** — controller v0.206.0 shipped the R-241 fixes while the vouched golden still carried 0.205.0, so a machine installed that morning would have received neither. That is the second occurrence in two days (the first, R-239, went unnoticed until a walk measured it from the customer's side), and it is what a rule without a mechanism does. The failure mode is FORGETTING, not lying — nobody ever decided to ship a stale golden. So this gate is built to catch a missed step, and it is not, and does not pretend to be, an adversarial control. ⚠ WHAT IT CHECKS, AND WHAT IT DELIBERATELY DOES NOT — read this before trusting a green. It checks that a golden has been **BAKED** for the newest released controller, by looking for that version's bake-evidence directory in this repo. It does **NOT** check that the golden was **VOUCHED**, because the vouched version lives ONLY in the hub's `hub_settings` table — there is no copy in git. That limit is forced, not chosen, and the reasoning is recorded so nobody re-derives it: * Both the pre-push hook AND CI run `repo_gates.py --fast`, which by contract selects only gates that touch **no network**. A hub-reading gate could therefore be registered as non-fast and would then run in NEITHER place — a check that does not run where it applies is precisely the R-29 census failure this repo's runner was built to end. A gate nobody runs is worse than no gate, because it reads as coverage. * Recording the vouched version in a tracked file instead would create a second source of truth that can drift from the hub, and a green gate over a false claim is the worst outcome available. **So a bake without a vouch still passes this gate.** The bake is the step that happens in this repo and is therefore the step this repo can see; the vouch is an operator act against the hub and needs a different mechanism. That gap is real and is recorded as R-242's remaining half, NOT papered over here. In practice the two are minutes apart in the same session, and the recurrence this gate is built for was a missing BAKE. WHY VERSION AND NOT BEHAVIOUR. It compares version numbers, so a controller release that changed nothing a customer can see also trips it. That is accepted deliberately: deciding "customer-visible" mechanically is not possible, judging it by hand is what already failed twice, and the cost of a false trip is one bake — which is the operation the project wants to be routine anyway. **A gate that cries wolf is one people learn to bypass, and `--no-verify` exists**, so the tolerance is stated rather than assumed: if this ever fires on a release nobody wants a golden for, the honest fix is a recorded waiver in the register, never a habit of bypassing. FAIL-CLOSED, BUT HONEST ABOUT NOT KNOWING. An absent controller clone, or a CHANGELOG whose top header cannot be parsed, exits **2 (INCONCLUSIVE)** — never 0. The runner reports 2 distinctly for exactly this reason: an undetermined result is not a pass, and it is not a conviction either. """ import os import re import sys ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # The controller clone sits beside this one. The same sibling assumption reuse_refs_check.py and # instructions_gate.py already make — an absent sibling is INCONCLUSIVE, never a silent pass. CONTROLLER_CHANGELOG = os.path.join(os.path.dirname(ROOT), "felhom-controller", "CHANGELOG.md") EVIDENCE_DIR = os.path.join(ROOT, "documentation", "tests") # `## v0.206.0 — …` on the FIRST such line: the CHANGELOG is newest-first by convention. RELEASED_RE = re.compile(r"^##\s+v(\d+)\.(\d+)\.(\d+)\b") # `golden-0.205.0-2026-08-07/` — the bake-evidence directory the runbook's §4.1 produces. EVIDENCE_RE = re.compile(r"^golden-(\d+)\.(\d+)\.(\d+)-\d{4}-\d{2}-\d{2}$") def newest_released(): """(tuple, str) of the newest controller release, or (None, reason).""" if not os.path.isfile(CONTROLLER_CHANGELOG): return None, "controller clone not found at %s" % CONTROLLER_CHANGELOG with open(CONTROLLER_CHANGELOG, encoding="utf-8") as fh: for line in fh: m = RELEASED_RE.match(line) if m: return tuple(int(g) for g in m.groups()), line.strip()[:90] return None, "no '## vX.Y.Z' header found in %s" % CONTROLLER_CHANGELOG def newest_baked(): """(tuple, str) of the newest golden bake recorded here, or (None, reason).""" if not os.path.isdir(EVIDENCE_DIR): return None, "bake-evidence directory not found at %s" % EVIDENCE_DIR found = [] for name in os.listdir(EVIDENCE_DIR): m = EVIDENCE_RE.match(name) if m: found.append((tuple(int(g) for g in m.groups()), name)) if not found: return None, "no golden--/ evidence directory under %s" % EVIDENCE_DIR found.sort() return found[-1] def vstr(v): return ".".join(str(p) for p in v) def main(): released, rel_note = newest_released() if released is None: print("GOLDEN CURRENCY GATE INCONCLUSIVE: %s" % rel_note) sys.exit(2) baked, bake_note = newest_baked() if baked is None: print("GOLDEN CURRENCY GATE INCONCLUSIVE: %s" % bake_note) sys.exit(2) # Print the evidence unconditionally — a gate that only speaks when it fails teaches nobody what # it is watching, and this one is watching the thing two releases already slipped through. print(" newest released controller : %s (%s)" % (vstr(released), rel_note)) print(" newest golden baked : %s (documentation/tests/%s)" % (vstr(baked), bake_note)) if released > baked: print("") print("GOLDEN CURRENCY GATE FAILED: controller v%s is released and NO golden carries it " "(newest bake is %s)." % (vstr(released), vstr(baked))) print("A machine installed right now would receive v%s — the release is written, tested and " "pushed, and NOT delivered." % vstr(baked)) print("Fix: bake a golden per documentation/runbooks/RUNBOOK-manual-build.md §4.1, then vouch " "it (a THREE-field change: golden_version + agent_version + min_agent).") print("If this release deliberately needs no golden, record a waiver in " "documentation/backlog/OPEN-ITEMS.md — never a bypass.") sys.exit(1) print("golden currency gate OK — the newest released controller has a golden " "(NOTE: this checks the BAKE, not the vouch — see the module docstring)") if __name__ == "__main__": main()