diff --git a/CHANGELOG.md b/CHANGELOG.md index eeffb58..e5a205a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,6 +113,8 @@ to fail** — three first ran INERT or did not compile and were fixed before the ## v0.236.0 — "delete my data too" deletes the data, or says that it could not (2026-09-13, R-442) +**MinAgent: 0.129.0** (unchanged) — backfilled 2026-09-13 (R-470): read from v0.232.0's header; `internal/agentapi/` has no commit since 2026-09-01 + **The defect.** A customer removes an app and ticks *also delete my data*. The box says it worked; **the data is still on the disk** — measured 2026-09-01 on demo-hp: `HTTP 200` with `hdd_paths_removed: null, hdd_paths_preserved: null` and 128 MB of Nextcloud left at @@ -162,6 +164,8 @@ metrics) — left alone; not deleted here. ## v0.235.0 — freeze the version, keep the fixes flowing (2026-09-06, update arc slice 3) +**MinAgent: 0.129.0** (unchanged) — backfilled 2026-09-13 (R-470): read from v0.232.0's header; `internal/agentapi/` has no commit since 2026-09-01 + **OPERATOR RULING, 2026-09-06 — Option 1.** R-447 was `BLOCKED` because R-438 established that `RestartStack`'s use of `up -d` to pick up template changes was **chosen** and written down in its own comment; reversing a chosen behaviour is a decision, not a bug fix. The decision is made and this @@ -273,6 +277,8 @@ catalog. `TestRenderTable_EmptyStoredDefinitionIsTreatedAsAbsent`. ## v0.234.0 — the label now appears on an app nobody has touched (2026-09-03, update arc slice 1b) +**MinAgent: 0.129.0** (unchanged) — backfilled 2026-09-13 (R-470): read from v0.232.0's header; `internal/agentapi/` has no commit since 2026-09-01 + **Found by the operator on demo-felhom the morning after v0.233.0, and it is a real gap, not a misunderstanding:** OpenGist had been up 15 hours, was running exactly what the catalog pins, and showed **no badge at all**. v0.233.0 wrote the record only from the four bring-up paths, so an app @@ -326,6 +332,8 @@ a backfill nothing invokes seeds nothing, and a `strings.Contains` would match a ## v0.233.0 — the box writes down what it installed, and one label says whether it is current (2026-09-02, update arc slices 1 & 2) +**MinAgent: 0.129.0** (unchanged) — backfilled 2026-09-13 (R-470): read from v0.232.0's header; `internal/agentapi/` has no commit since 2026-09-01 + **Neither slice changes any behaviour.** The Frissítés button, the restart path, the sync and the boot reconciler are byte-identical. This adds a RECORD and a LABEL, because the behaviour work (slice 3) is easier to judge once the fleet's real state is visible. Opened by diff --git a/controller/scripts/controller_gates.py b/controller/scripts/controller_gates.py index 56458e0..5b49649 100644 --- a/controller/scripts/controller_gates.py +++ b/controller/scripts/controller_gates.py @@ -84,6 +84,8 @@ GATES = [ ("instructions", SHARED_INSTRUCTIONS, [REPO], True, True), # R-389 — a REPORT.md observation with no register row behind it. Fast: stdlib file reads. ("observations", SHARED_OBSERVATIONS, [REPO], True, True), + # R-470/R-472 — the newest release header states its MinAgent; the hub's floor reads it. Fast. + ("minagent-header", os.path.join(SCRIPTS, "minagent_header_gate.py"), [], True, True), # R-404 — ADVISORY. Reports the golden debt where it is created; never refuses. ("golden-notice", GOLDEN_NOTICE, [REPO], True, False), ] diff --git a/controller/scripts/minagent_header_gate.py b/controller/scripts/minagent_header_gate.py new file mode 100644 index 0000000..2d25665 --- /dev/null +++ b/controller/scripts/minagent_header_gate.py @@ -0,0 +1,75 @@ +#!/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()) diff --git a/controller/scripts/test_controller_gates.py b/controller/scripts/test_controller_gates.py index 277736c..5236e12 100644 --- a/controller/scripts/test_controller_gates.py +++ b/controller/scripts/test_controller_gates.py @@ -32,6 +32,7 @@ FINGERPRINTS = [ ("docker-v", "docker -v gate OK"), ("debug-routes", "debug route gate OK"), ("reuse-refs", "cited paths — exact"), + ("minagent-header", "minagent header gate OK"), ] diff --git a/controller/scripts/test_gate_decoys.py b/controller/scripts/test_gate_decoys.py index 9764299..fddcb04 100644 --- a/controller/scripts/test_gate_decoys.py +++ b/controller/scripts/test_gate_decoys.py @@ -43,6 +43,7 @@ COVERS = { "mojibake": "CONTROL: already walked; proves the planted file is really reachable", "debug-routes": "a live dispatcher case commented out - the button survives, the handler dies", "golden-notice": "R-410 in the other direction: an empty dir must not count as a bake", + "minagent-header": "the word MinAgent in prose / a code span, not the header line (test_minagent_header_gate.py)", } fails = [] diff --git a/controller/scripts/test_minagent_header_gate.py b/controller/scripts/test_minagent_header_gate.py new file mode 100644 index 0000000..e4c0cb3 --- /dev/null +++ b/controller/scripts/test_minagent_header_gate.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Tests for minagent_header_gate.py (R-470/R-472) — each case writes a CHANGELOG and runs the REAL gate. + +Run from controller/: python3 scripts/test_minagent_header_gate.py +""" +import os +import subprocess +import sys +import tempfile +import unittest + +SCRIPTS = os.path.dirname(os.path.abspath(__file__)) +GATE = os.path.join(SCRIPTS, "minagent_header_gate.py") + + +def run(body): + with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False, encoding="utf-8") as f: + f.write(body) + path = f.name + try: + env = dict(os.environ, MINAGENT_GATE_CHANGELOG=path) + p = subprocess.run([sys.executable, GATE], capture_output=True, text=True, env=env) + return p.returncode, p.stdout + p.stderr + finally: + os.unlink(path) + + +class MinAgentHeaderGateTest(unittest.TestCase): + def test_header_line_passes(self): + rc, out = run("## v0.239.0 — x (2026-09-13)\n\n**MinAgent: 0.129.0** (unchanged)\n\nbody\n\n## v0.238.1 — y\n") + self.assertEqual(rc, 0, out) + self.assertIn("v0.239.0 declares MinAgent 0.129.0", out) + + def test_missing_line_fails_naming_the_header(self): + rc, out = run("## v0.239.0 — x (2026-09-13)\n\nbody\n\n## v0.238.1 — y\n\n**MinAgent: 0.129.0** (unchanged)\n") + self.assertEqual(rc, 1, out) + self.assertIn("v0.239.0", out) + + def test_decoy_body_mention_is_not_the_line(self): + """R-421: the word in prose, a code span, or a non-bold form is the LABEL without the FACT. + + COMPANION RED-PROOF F (REPORT.md): relax MINAGENT_LINE_RE to a plain search for 'MinAgent'. + This test then passes the decoy and fails.""" + decoy = ("## v0.239.0 — x (2026-09-13)\n\n" + "MinAgent is unchanged from the previous release, see `**MinAgent: 0.129.0**` there.\n" + "The agent requirement (MinAgent: 0.129.0) did not move.\n\n## v0.238.1 — y\n") + rc, out = run(decoy) + self.assertEqual(rc, 1, out) + + def test_no_header_is_inconclusive(self): + rc, out = run("# CHANGELOG\n\nnothing released\n") + self.assertEqual(rc, 2, out) + + +if __name__ == "__main__": + unittest.main()