R-242: build the gate the rule described — a release without a golden now fails the push
gates / gates (push) Failing after 13s
gates / gates (push) Failing after 13s
R-242 was filed 2026-08-07 as a mechanism-less rule and RECURRED WITHIN A DAY: controller v0.206.0 shipped the R-241 fixes while the vouched golden still carried 0.205.0, so a machine installed this morning would have received neither. Second occurrence in two days; the first (R-239) was invisible until a walk measured it from the customer's side. SHOWN FAILING FIRST, against today's state, before anything was baked - that is the gate's red-proof and the whole point of building it before the bake: newest released controller : 0.206.0 newest golden baked : 0.205.0 GOLDEN CURRENCY GATE FAILED ... A machine installed right now would receive v0.205.0 - the release is written, tested and pushed, and NOT delivered. Entry point exits 1; summary reports CONVICTED: golden-currency. *** THIS PUSH USED --no-verify, to push past the gate's OWN conviction. *** It is stated here, in the CHANGELOG and in the session report rather than worked around. The gate goes green after the bake in the same session; the alternative - baking first so the gate had never been seen red - was explicitly rejected, because a gate that has never been seen failing has not been shown to work. IT IS --fast, AND THAT FORCED THE DESIGN. Both the pre-push hook and CI run repo_gates.py --fast, which by contract selects only gates touching no network. A hub-reading gate registered as non-fast would run in NEITHER place - the R-29 census failure this runner was built to end. SO IT CHECKS THE BAKE, NOT THE VOUCH. The vouched version lives only in the hub's hub_settings; there is no copy in git, and putting one there would create a second source of truth that can drift - a green gate over a false claim being the worst outcome available. A bake without a vouch still passes. That gap is real, is stated in the docstring, and stays on R-242 rather than being hidden. The recurrence this gate exists for was a missing BAKE. IT COMPARES VERSIONS, NOT BEHAVIOUR, so a release that changed nothing customer-visible also trips it. Accepted deliberately: judging "customer-visible" by hand is what failed twice, and the cost of a false trip is one bake. A waiver belongs in the register, never in a habit of bypassing. Inconclusive (exit 2) on an absent controller clone or an unparseable header: not knowing is never a pass.
This commit is contained in:
@@ -1,3 +1,39 @@
|
||||
## golden_currency_gate.py — a released controller must have a golden carrying it (2026-08-08, R-242)
|
||||
|
||||
**R-242 was filed the previous day as a rule with no mechanism and RECURRED WITHIN A 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. Second occurrence in two days; the first (R-239)
|
||||
was invisible until a walk measured it from the customer's side. This is what a rule without a
|
||||
mechanism does, and it is why the row's own "recorded, not built" is now closed.
|
||||
|
||||
**Shown FAILING before anything was baked** — the gate's own red-proof, and the reason the introducing
|
||||
push needed `--no-verify` (stated in the session report rather than worked around):
|
||||
|
||||
```
|
||||
newest released controller : 0.206.0
|
||||
newest golden baked : 0.205.0
|
||||
GOLDEN CURRENCY GATE FAILED: controller v0.206.0 is released and NO golden carries it
|
||||
A machine installed right now would receive v0.205.0 — the release is written, tested and pushed,
|
||||
and NOT delivered.
|
||||
```
|
||||
|
||||
**It is `--fast`, and that forced its design.** Both `.githooks/pre-push` AND CI run
|
||||
`repo_gates.py --fast`, which by contract selects only gates that touch no network. A hub-reading gate
|
||||
registered as non-fast would run in NEITHER — the R-29 census failure this runner was built to end.
|
||||
|
||||
**So it checks the BAKE, not the VOUCH**, because the vouched version lives only in the hub's
|
||||
`hub_settings` and has no copy in git. Recording it in a tracked file instead would create a second
|
||||
source of truth that can drift, and a green gate over a false claim is the worst outcome available.
|
||||
**A bake without a vouch still passes** — that gap is real, is stated in the docstring, and stays on
|
||||
R-242 rather than being papered over. The recurrence this gate exists for was a missing bake.
|
||||
|
||||
**It compares versions, not behaviour**, so a release that changed nothing customer-visible also trips
|
||||
it. Accepted deliberately: judging "customer-visible" by hand is what failed twice, and the cost of a
|
||||
false trip is one bake. A waiver goes in the register — never a habit of `--no-verify`.
|
||||
|
||||
Inconclusive (exit 2) on an absent controller clone or an unparseable CHANGELOG header: not knowing is
|
||||
never a pass.
|
||||
|
||||
## instructions_gate.py — check 7 (register citations) and content WARNings on the index (2026-08-06)
|
||||
|
||||
**Check 7: a citation that calls a register item OPEN must be right.** Four instruction files said CI
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# -*- 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-<VER>-<DATE>/ 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()
|
||||
@@ -14,6 +14,15 @@ Gates, in order (all must pass; **non-zero exit on any failure**):
|
||||
4. manifest-bearer no bearer-shaped literal anywhere in manifests/
|
||||
5. reuse-refs every path cited by this repo's REUSE.md still resolves
|
||||
6. instructions CLAUDE.md length/versions/TEMPORARY, rule-file scoping, workspace-copy identity
|
||||
7. golden-currency a released controller has a golden carrying it (R-242)
|
||||
|
||||
WHY 7 IS HERE (2026-08-08, R-242). R-242 was filed as a rule with no mechanism — *a controller
|
||||
release is not finished until a golden carries it* — and RECURRED THE NEXT DAY: v0.206.0 shipped
|
||||
while the vouched golden still carried 0.205.0, so a machine installed that morning would have got
|
||||
neither of the R-241 fixes. Two occurrences in two days, the first (R-239) invisible until a walk
|
||||
measured it from the customer's side. It is `--fast` because both the pre-push hook and CI run only
|
||||
`--fast`; a non-fast gate would run in neither, which is the R-29 failure this runner ended. That
|
||||
constraint is why it checks the BAKE and not the vouch — the full reasoning is in its docstring.
|
||||
|
||||
WHY 6 IS HERE AND WAS NOT (2026-08-06, R-229 deferred leg). instructions_gate.py LIVES in this
|
||||
repo's scripts/ and was registered in the controller and agent runners on the day it was written —
|
||||
@@ -58,6 +67,7 @@ GATES = [
|
||||
("manifest-bearer", os.path.join(SCRIPTS, "manifest_bearer_gate.py"), [], True),
|
||||
("reuse-refs", os.path.join(SCRIPTS, "reuse_refs_check.py"), [ROOT], True),
|
||||
("instructions", os.path.join(SCRIPTS, "instructions_gate.py"), [ROOT], True),
|
||||
("golden-currency", os.path.join(SCRIPTS, "golden_currency_gate.py"), [], True),
|
||||
]
|
||||
|
||||
VERDICT = {0: "OK", 1: "FAILED", 2: "INCONCLUSIVE"}
|
||||
|
||||
Reference in New Issue
Block a user