diff --git a/CHANGELOG.md b/CHANGELOG.md index 622d956..3fd6600 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## scripts only — the decoy suite is a gate of the full run (R-1116, 2026-10-17) + +No agent release; no binary changes. `scripts/test_gate_decoys.py` is now the `gate-decoys` row of `agent_gates.py`: +the full run (which CI runs on every push), not `--fast` — it takes ~20 s in the workspace (measured: 30 cases; ~5 s in a +CI-shaped shallow clone, also green). It was in no runner, the shape that let the catalog's R-1115 fail unseen for a +night. It plants only in scratch clones and asks a fake Gitea on 127.0.0.1; it does not call the runner (no +recursion). Declared in its own `COVERS`; `test_agent_gates.py` pins it (registered once, full run only, named by +`--fast`'s SKIPPED line). Red-proved: a reuse-refs FACT case flipped to expect rc 0 turned the full run red +(`CONVICTED: gate-decoys`); restored, green. CLAUDE.md's `--fast` sentence names it. + ## unreleased — Go 1.27.2 and golang.org/x/crypto v0.58.0 (R-972, 2026-10-16) **Not released, not deployed** — agent releases wait for the operator's vouch; the next vouched release carries this. diff --git a/CLAUDE.md b/CLAUDE.md index adca3fe..7f9aa9c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,7 +56,8 @@ This is in the core because breaching it is how this component stops being audit gate in its `GATES` table (that table is the list); the shared ones — `reuse_refs_check`, `instructions_gate`, `observations_gate` — are the copies in `felhom.eu/scripts/`, never copied into this repo (a copy recreates the drift they detect; an absent sibling clone FAILS). `--fast` selects the -gates touching no network and no container runtime, and skips `published` (network), naming it. **A missing gate is a FAILURE, never a skip.** +gates touching no network and no container runtime, and skips `published` (network) and `gate-decoys` (the decoy +suite, ~20 s; R-1116), naming them — CI runs the full set. **A missing gate is a FAILURE, never a skip.** **The pre-push hook** (`.githooks/pre-push`) runs it with `--fast` and refuses a failing push. It is **per-clone** — switch it on once with `git config core.hooksPath .githooks`, and a manual run WARNS diff --git a/scripts/agent_gates.py b/scripts/agent_gates.py index fb9d45d..84dfddc 100644 --- a/scripts/agent_gates.py +++ b/scripts/agent_gates.py @@ -15,6 +15,9 @@ Gates (all must pass; **non-zero exit on any failure**): 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. + … gate-decoys the decoy suite (test_gate_decoys.py): every gate here still convicts its decoys + (R-1116). ~20 s in the workspace, so the full run only: not the hook, but CI + runs the full set on every push. Scratch clones and a local fake Gitea only. WHY THIS FILE EXISTS, WITH ONE GATE (2026-08-02, R-29 leg (b)). @@ -58,6 +61,12 @@ GATES = [ ("release-complete", os.path.join(ROOT, "scripts", "check-release-complete.py"), [], True), # R-389 — a REPORT.md observation with no register row behind it. Fast: stdlib file reads. ("observations", SHARED_OBSERVATIONS, [ROOT], True), + # R-1116 (2026-10-17) — the gates' OWN decoy suite. It was in no runner, the shape that let the catalog's + # R-1115 fail unseen for a night. Full run only: ~20 s in the workspace (measured, 30 cases; ~5 s in a + # CI-shaped clone, where the shared gates have fewer siblings to search), too slow for the hook — and CI + # runs the FULL set, so every push is still judged by it. It plants only in scratch clones and asks a fake + # Gitea on 127.0.0.1 (never the network, never the real tree); it does not call this runner (no recursion). + ("gate-decoys", os.path.join(ROOT, "scripts", "test_gate_decoys.py"), [], False), ] VERDICT = {0: "OK", 1: "FAILED", 2: "INCONCLUSIVE"} diff --git a/scripts/test_agent_gates.py b/scripts/test_agent_gates.py index 69faf11..9c840f4 100644 --- a/scripts/test_agent_gates.py +++ b/scripts/test_agent_gates.py @@ -57,6 +57,20 @@ class AgentGatesTest(unittest.TestCase): finally: shutil.rmtree(tmp, ignore_errors=True) + def test_decoy_suite_is_a_full_run_gate(self): + """R-1116: the decoy suite is a gate of the FULL run (which CI runs on every push), and --fast + names it as skipped (~20 s, too slow for the hook).""" + sys.dont_write_bytecode = True # no __pycache__ left in scripts/ (not gitignored here) + sys.path.insert(0, os.path.join(ROOT, "scripts")) + import agent_gates as mod + rows = [g for g in mod.GATES if g[0] == "gate-decoys"] + self.assertEqual(len(rows), 1, "gate-decoys is not registered exactly once") + self.assertEqual(os.path.basename(rows[0][1]), "test_gate_decoys.py") + self.assertFalse(rows[0][3], "gate-decoys must not be --fast") + self.assertEqual([g[0] for g in mod.GATES if not g[3]], ["published", "gate-decoys"]) + self.assertIn("--fast SKIPPED (deliberate periodic runs, never in a hook): published, gate-decoys", + self.out) + def test_unknown_argument_is_rejected(self): p = subprocess.run([sys.executable, ENTRY, "--nope"], cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) diff --git a/scripts/test_gate_decoys.py b/scripts/test_gate_decoys.py index f49c510..003411c 100644 --- a/scripts/test_gate_decoys.py +++ b/scripts/test_gate_decoys.py @@ -66,6 +66,9 @@ COVERS = { "inside an HTML comment (R-426)", "observations": "R-419 in THIS repo's REPORT.md: an Observations note SAYING it carries no marker - vs the " "two genuine markers (R-426)", + "gate-decoys": "this suite, run as a gate of agent_gates.py's full run, which CI runs on every push (R-1116): " + "every case above is a decoy of its own gate, and ONE case that stops convicting turns the run " + "red - red-proved 2026-10-17 by flipping a reuse-refs FACT case to expect rc 0", } fails = []