#!/usr/bin/env python3 # -*- coding: utf-8 -*- """minagent_header_gate.py — the newest release header must state its MinAgent (R-470, R-472). Run from controller/: python3 scripts/minagent_header_gate.py Exit 0 clean · 1 the newest `## vX.Y.Z` block has no `**MinAgent: X.Y.Z**` line · 2 inconclusive. WHY THIS EXISTS. From hub v0.112.0 a controller release can reach the fleet by the managed floor WITHOUT a golden, because the operator declares the release's agent requirement with the floor (operator ruling 2026-09-13, R-472). That declaration is READ from this file's newest header — the `**MinAgent: 0.129.0** (unchanged)` line `RUNBOOK-manual-build.md` §4.1 already told the vouch to read. Four headers (v0.233.0 … v0.236.0) did not carry it (R-470), so the value the hub is now told to trust would have had to be guessed. A rule with no gate is a wish; this is the gate. WHAT COUNTS. The block from the newest `## vX.Y.Z` heading to the next `## ` heading must contain a line that STARTS with `**MinAgent: **`. A mention of the word anywhere else — prose saying "MinAgent is unchanged", a code span, the heading itself — is the label without the fact (R-421) and does not count. """ import io import os import re import sys SCRIPTS = os.path.dirname(os.path.abspath(__file__)) REPO = os.path.dirname(os.path.dirname(SCRIPTS)) CHANGELOG = os.environ.get("MINAGENT_GATE_CHANGELOG", os.path.join(REPO, "CHANGELOG.md")) HEADER_RE = re.compile(r"^##\s+v(\d+\.\d+\.\d+)\b") MINAGENT_LINE_RE = re.compile(r"^\*\*MinAgent:\s*(\d+\.\d+\.\d+)\*\*") def newest_block(lines): """(version, block_lines) for the newest `## vX.Y.Z` heading, or (None, []).""" start, version = None, None for i, line in enumerate(lines): m = HEADER_RE.match(line) if m: start, version = i, m.group(1) break if start is None: return None, [] block = [] for line in lines[start + 1:]: if line.startswith("## "): break block.append(line) return version, block def main(): try: lines = io.open(CHANGELOG, encoding="utf-8").read().splitlines() except OSError as e: print("minagent header gate INCONCLUSIVE: cannot read %s (%s)" % (CHANGELOG, e)) return 2 version, block = newest_block(lines) if version is None: print("minagent header gate INCONCLUSIVE: no '## vX.Y.Z' heading in %s" % CHANGELOG) return 2 for line in block: m = MINAGENT_LINE_RE.match(line.strip()) if m: print("minagent header gate OK — v%s declares MinAgent %s" % (version, m.group(1))) return 0 print("minagent header gate FAILED: the newest release header v%s has no '**MinAgent: X.Y.Z**' line." % version) print("From hub v0.112.0 the floor carries a release past the golden only with a DECLARED MinAgent, and") print("that value is read from this line (R-472). Add it directly under the heading, e.g.:") print(" **MinAgent: 0.129.0** (unchanged)") print("Read the value from internal/agentapi/features.go (the highest featureMinAgent) — never guess it.") return 1 if __name__ == "__main__": sys.exit(main())