From 55868cb3e0f0f5f1e33f9e488523055dba010941 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Sat, 17 Oct 2026 11:01:40 +0200 Subject: [PATCH] The decoy suite is a gate of the full run (R-1116); no code change controller/scripts/test_gate_decoys.py is now controller_gates.py's `gate-decoys` row: blocking, full run only (~22 s, 53 cases, and it edits real files in the tree for a moment, so never the hook). Declared in its own COVERS; test_controller_gates.py pins it. Red-proved: debug-routes/comment flipped to expect accept -> CONVICTED: gate-decoys; restored, green. gates.md's "--fast is all of them" corrected. Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS --- .claude/rules/gates.md | 4 +++- CHANGELOG.md | 11 +++++++++++ controller/scripts/controller_gates.py | 10 ++++++++++ controller/scripts/test_controller_gates.py | 14 ++++++++++++++ controller/scripts/test_gate_decoys.py | 3 +++ 5 files changed, 41 insertions(+), 1 deletion(-) diff --git a/.claude/rules/gates.md b/.claude/rules/gates.md index c886a47..c9a065b 100644 --- a/.claude/rules/gates.md +++ b/.claude/rules/gates.md @@ -14,7 +14,9 @@ one, `golden-notice`, is ADVISORY and never refuses. **The `GATES` table is the pointer to it, not a second copy** — it drifted twice (it said "seven" while nine were registered, then named ten local gates while fourteen were). -- `--fast` selects the gates that touch no network and no container runtime; today that is all of them. +- `--fast` selects the gates that touch no network and no container runtime; today that is all of them + except `gate-decoys` (the decoy suite, ~22 s, edits real files in this tree for a moment; R-1116), which + runs in the full run only — so the hook and CI do not run it. - **A missing gate script is a FAILURE, never a skip.** - **The shared `reuse_refs_check.py`, `instructions_gate.py` and `observations_gate.py` live in `felhom.eu/scripts/` and are never copied here** — a copy would recreate the drift they detect; an absent sibling clone FAILS. diff --git a/CHANGELOG.md b/CHANGELOG.md index bc06a65..61e5b88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## Unreleased (2026-10-17) — the decoy suite is a gate of the full run (R-1116); no code change + +- **`gate-decoys`** (`controller/scripts/test_gate_decoys.py`) is now a row of `controller_gates.py`: the full run, + blocking, NOT `--fast`. It was in no runner — the shape that let the catalog's R-1115 fail unseen for a night. Not + `--fast` because it takes ~22 s (measured: 53 cases) and edits real files in this tree for a moment (it swaps and + restores `handler_debug.go`, `update.go`, the locale bundles, and plants `.go` files under `internal/` and `cmd/`) — + in the pre-push hook that would race a session editing the same tree. It does not call the runner (no recursion). + Declared in its own `COVERS`. `test_controller_gates.py` pins it (registered once, full run only, blocking, named + by `--fast`'s SKIPPED line). Red-proved: the `debug-routes/comment` case flipped to expect `accept` turned the full + run red (`CONVICTED: gate-decoys`); restored, green. `.claude/rules/gates.md`'s „`--fast` is all of them" corrected. + ## v0.325.0 — parallel password tries capped on two more doors; family sessions stored as fingerprints; samba renames apply; path guard widened; gates in a worktree (2026-10-17) **MinAgent: 0.158.1** — the floor's, carried. Nothing here calls the agent. No golden (`09` §3 decision 250). Task: diff --git a/controller/scripts/controller_gates.py b/controller/scripts/controller_gates.py index 6125410..4f741f8 100644 --- a/controller/scripts/controller_gates.py +++ b/controller/scripts/controller_gates.py @@ -18,6 +18,9 @@ Gates, in order (all must pass; **non-zero exit on any failure**): 8. debug-routes every debug-page control resolves to a handler, and back (R-400) 9. reuse-refs every path cited by this repo's REUSE.md still resolves … gofmt every Go file is gofmt-clean (R-454) + … gate-decoys the decoy suite (test_gate_decoys.py): every gate above still convicts its + decoys (R-1116). ~22 s and it edits real files in this tree for a moment, so + the full run only, never --fast (never the hook, never CI) WHY THIS FILE EXISTS (2026-08-02, closing R-29 leg (a) and half of leg (b)). @@ -104,6 +107,13 @@ GATES = [ ("gofmt", os.path.join(SCRIPTS, "gofmt_gate.py"), [], True, True), # R-404 — ADVISORY. Reports the golden debt where it is created; never refuses. ("golden-notice", GOLDEN_NOTICE, [REPO], True, False), + # R-1116 (2026-10-17) — the gates' OWN decoy suite: every gate above, red-proved against its decoys. It was + # in no runner, the shape that let the catalog's R-1115 fail unseen for a night. NOT --fast, for two + # reasons: it takes ~22 s (measured, 53 cases), and it EDITS REAL FILES in this tree and restores them + # (handler_debug.go, update.go, the locale bundles; plants .go files under internal/ and cmd/) — in the + # pre-push hook that would race any session editing the same tree. Run the full run in a quiet tree. + # It does not call this runner, so there is no recursion. + ("gate-decoys", os.path.join(SCRIPTS, "test_gate_decoys.py"), [], False, True), ] VERDICT = {0: "OK", 1: "FAILED", 2: "INCONCLUSIVE"} diff --git a/controller/scripts/test_controller_gates.py b/controller/scripts/test_controller_gates.py index 5236e12..201ba87 100644 --- a/controller/scripts/test_controller_gates.py +++ b/controller/scripts/test_controller_gates.py @@ -60,6 +60,20 @@ class ControllerGatesTest(unittest.TestCase): self.assertFalse(os.path.exists(os.path.join(SCRIPTS, "reuse_refs_check.py")), "reuse_refs_check.py must NOT be copied into this repo") + def test_decoy_suite_is_a_blocking_full_run_gate(self): + """R-1116: the decoy suite is a gate of the FULL run (blocking), and --fast names it as skipped. + Not --fast: ~22 s, and it edits real files in this tree for a moment (a hook would race other sessions).""" + sys.path.insert(0, SCRIPTS) + import controller_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") + _label, path, _args, fast, blocking = rows[0] + self.assertEqual(os.path.basename(path), "test_gate_decoys.py") + self.assertFalse(fast, "gate-decoys must not be --fast") + self.assertTrue(blocking, "a decoy that stops convicting must fail the run") + self.assertEqual([g[0] for g in mod.GATES if not g[3]], ["gate-decoys"]) + self.assertIn("--fast SKIPPED (deliberate periodic runs, never in a hook): gate-decoys", self.out) + def test_unknown_argument_is_rejected(self): p = subprocess.run([sys.executable, ENTRY, "--nope"], cwd=CTRL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) diff --git a/controller/scripts/test_gate_decoys.py b/controller/scripts/test_gate_decoys.py index aa8908a..62c5df9 100644 --- a/controller/scripts/test_gate_decoys.py +++ b/controller/scripts/test_gate_decoys.py @@ -60,6 +60,9 @@ COVERS = { "same sentence inside an HTML comment", "observations": "R-426: R-419 in THIS repo's REPORT.md - an Observations note SAYING it carries no " "marker - vs the two genuine markers", + "gate-decoys": "this suite, run as a gate of controller_gates.py's full run (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 the debug-routes/comment case to expect accept", } fails = []