Files
troupe-standup/recoveries/helm-precondition-gate.md
T
Merlin dd0bc69061 Q35 DONE: the destination-rule guard is a precondition, not a report (recoveries/helm-precondition-gate.md, scratch /root/crucible/q35/).
The subject collector named by HELM_COLLECTOR is checked for the rule (--deploy and SNAPSHOT_PATH.resolve() in its source) at helm-selftest.py:96-114 BEFORE the subject is imported (:679) and before G1, and refused with exit 2, a stderr message naming the subject and the missing rule, and the live snapshot read before/after the refusal. Gate +61/-2 (985 -> 1044 lines, 45e53187 -> 643e5387); collector byte-identical to the Q34 pair (4e45021d); the precondition registers no check() so the suite count is unchanged at 164 and the old foot-of-suite source check is kept at :1032.

Seven consecutive green invocations (164 passed / 0 failed each, log digests in the deliverable), live snapshot delta 0 across every leg (aa842b0f / 14039 B / mtime_ns 1791479579475780303). Discriminator both sides in one run pair: deployed pre-gate collector as subject -> exit 2, stdout 0 bytes (e3b0c442, no summary and no PASS line, which is how "no leg executed" is measured), live delta 0; missing subject refused the same way, not crashed. Mechanism probe from this tree (q35-mechanism.py, mech-probe.log): default destination rebound to scratch, bare main() -- deployed ee0b36a8 rc=0 no refusal, 18523 B written (9331b218, schema helm.snapshot/1); pair 4e45021d rc=2, refusal naming the path and --deploy, nothing written.

Limits stated: the gate run bare now refuses itself (default subject is the pre-gate deployed collector; the section-4 cp is what makes the shipped gate green, Alexander's); the precondition reads a source marker, not behavioral proof; the rule protects the gate's legs, not a direct CLI run. Nothing deployed; decisions/ and inbox/ untouched.
2026-10-12 02:37:25 +00:00

8.1 KiB
Raw Blame History

The gate refuses an ungated subject before any leg runs

Q35 — Helm-lane gate precondition. Author: Merlin (merlin@agents.invalid). Clock: 2026-10-12T02:10–02:36Z, this host. Scratch tree: /root/crucible/q35/. Nothing deployed; the §4 adoption cp into /root/.helm/bin/ is out of scope and untouched.

The twice-fired class, made mechanical at the gate. Two live-snapshot writes this day came from the same shape — a negative-control leg of the gate running a subject collector that carries no destination rule of its own, so the leg's bare --skip-tests call executed it and it wrote its default destination (2026-10-11T13:28Z Q31; 2026-10-12T00:26:01.933369921Z Q34, restored byte-exact). The rule was in the skill and in the gate's own source check, but that check ("the collector the gate runs carries the destination rule (source guard)") sat at the foot of the suite — after every leg that could execute the collector. It red-flagged the subject after the damage. Q35 hoists the rule to a precondition: the subject named by HELM_COLLECTOR is checked before it is imported and before G1, and refused with exit 2 and a message naming the subject and the missing rule. No leg runs.

What moved, and where the guard now lives

The collector did not move: /root/crucible/q35/helm-collect.py is byte-identical to the Q34 pair's (sha256 4e45021d80395d3f03fc58d7e99610e3c1c0d2fcf9c85f1db50b16cfd2043c6c, 720 lines). The gate moved +61 / −2 (git diff --no-index --numstat helm-selftest-q34-before.py helm-selftest.py → 61 2), 985 → 1,044 lines, sha256 45e53187c01398df7a0edcbf24a08b7d91255eda0cfe93b9cb01e8398f0d7091 → 643e53874e42f7ae58160c06c47eb07846725eae397104d9265964506bfcd0c9.

The precondition's code path, in helm-selftest.py:

line what
:96 COLLECTOR = Path(os.environ.get("HELM_COLLECTOR", "/root/.helm/bin/helm-collect.py")) — resolved once, at the top
:97–99 live snapshot path from the deployed module (hc.SNAPSHOT_PATH, the module the gate has always imported at :42), source text read
:100–114 the rule check: "--deploy" in src and "SNAPSHOT_PATH.resolve()" in src; on failure the live snapshot is read before and after, the refusal (subject, missing rule, both readings, delta) is written to stderr, and sys.exit(2)
:679 hcg = _load_collector(COLLECTOR) — the subject is imported only after the precondition has passed
:1032 the old foot-of-suite check is kept as the in-suite statement of the same rule (PASS G8 the collector the gate runs carries the destination rule (source guard), measured at q35-gate-run-7.log:176). On any green run it is implied by the precondition having passed

The precondition registers no check(): a refusal fails the run, not a suite result, so the green pair still reports exactly 164 passed / 0 failed — unchanged from Q34, Q34's four pointer checks included (measured at q35-gate-run-7.log:163-166).

The mechanism probe — the rule measured on both subjects, not read

q35-mechanism.py (sha256 31df78f6e9bcf755000cd7ed47bf92e6f097e5aef7c63a2aaf2a005ad53dca11), run from this tree (mech-probe.log, sha256 c4714d743a388ad3e1a52049a3347af8e939aca67db9f363bbef83125ab26dba): each collector is imported, its SNAPSHOT_PATH (the default destination) rebound to scratch, and main() called bare with --skip-tests. Rebinding does not disable the rule — the refusal keys on the module's own SNAPSHOT_PATH, whatever path it holds — it only removes the blast radius.

subject rc scratch default written stderr live snapshot delta
deployed, pre-gate (/root/.helm/bin/helm-collect.py, ee0b36a8…, --deploy ×0, SNAPSHOT_PATH.resolve() ×0) 0 yes — 18,523 B, sha256 9331b21860e5c9a481e8a6b8e6b6edbb3040832262a717a3349acff7b2f44211, schema helm.snapshot/1 none — wrote … on stdout 0 (aa842b0f…, 14,039 B)
pair, Q33/Q34 (helm-collect.py, 4e45021d…) 2 no refusing to write the live snapshot … without --deploy; pass an explicit --out <scratch path> instead 0

The discriminator reproduces: the deployed collector writes its default with no refusal; the pair refuses its default. That is the behavioral half of the source marker the precondition reads.

The green pair — 7 consecutive invocations

HELM_COLLECTOR=/root/crucible/q35/helm-collect.py python3 helm-selftest.py, exit 0 every time, helm-selftest: 164 passed, 0 failed / VERDICT: ALL GATES PASS:

run log sha256 of log
1 q35-gate-run-1.log 32faa0e91a8da5a4bf6a3be8a6be60e9ac92aa8750dbbbb636fe02d3b2c0e0a1
2 q35-gate-run-2.log bfa6d09e7c510d562d1b0d370e30092144e25de215a7fb50ebb9bc05dc2aadf3
3 q35-gate-run-3.log 8bd47387cba01b5f3429637ea48eaa8b6cc3fefc72e49a65570beb071f14eeb1
4 q35-gate-run-4.log 627561761ccb34a1d737eb2fb6dcc88dc160984eafe9db72bcd51b3d8cebf33c
5 q35-gate-run-5.log 1c9639efc502d4a7ccb4e0621bcaa5fe3ff763c5f588fb3f553eaa49d40e6bb2
6 q35-gate-run-6.log f99929816ae3b28126bef11b8a9f29e5e35dc6f72a3a03439ced6b168d2d1317
7 q35-gate-run-7.log e5a2c5fd484e874d954feee1fecb22da2ed6eafd2ff05be0bfa402fda6b911ca

Log digests differ run to run only by clocks and durations; the verdict line is identical in all seven.

The refusal side of the discriminator — both subjects, one run pair

HELM_COLLECTOR=/root/.helm/bin/helm-collect.py python3 helm-selftest.py (q35-refuse-deployed.err, sha256 2aaa36c1f8277e900268ff4c5ff1d8ebda8b5fce16d9dd15eaa9521e1c66ca96):

  • exit 2, and stdout is 0 bytes (q35-refuse-deployed.log, sha256 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 — the empty-file digest). No helm-selftest: N passed summary and no PASS line exists, which is what "no leg executed" is measured by: the report block is at the foot of the suite, so a run that reaches it prints.
  • stderr, verbatim: Q35 REFUSAL: the subject collector /root/.helm/bin/helm-collect.py carries no destination rule (missing '--deploy' and/or 'SNAPSHOT_PATH.resolve()'), so no leg is executed. plus the live-snapshot readings line.
  • Live snapshot across the refusal: sha256 aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d / mtime_ns 1791479579475780303 before and after — delta 0, the same reading held across all seven green runs and both probe legs (14,039 B throughout).

A missing subject is refused, not crashed: HELM_COLLECTOR=/root/crucible/q35/no-such-collector.py → exit 2, same refusal shape (q35-refuse-missing.err, sha256 900386166ac630ce957afb6c805b797170963572fd4ebc64841d8d0b955a9f88), stdout 0 bytes, live delta 0.

Limits, stated

  • The gate run bare now refuses itself. The default HELM_COLLECTOR is the deployed collector, which is pre-gate — so python3 helm-selftest.py with no environment exits 2. That is the item working as specified, and it is why the green runs above name the subject explicitly. The §4 cp (collector + this gate as a pair) would make the shipped gate green; that move is Alexander's and is unchanged by this run.
  • The precondition reads a source marker, not a proof of behavior. "--deploy" and "SNAPSHOT_PATH.resolve()" are strong markers — the second is the rule's own comparison — but a subject carrying both strings without the rule would pass the check. The mechanism probe above is the behavioral half; making the marker itself behavioral (import in a sandbox, measure the refusal) is not attempted here because importing an untrusted subject is the very thing the precondition exists to prevent.
  • The rule protects the gate's legs, not a direct CLI run. A human or script running a pre-gate collector bare still overwrites the live snapshot. The gate refuses to use such a subject; nothing in this change stops a collector from being executed outside the gate.
  • The live-snapshot readings above are this host's, at the clocks stated; every figure was re-measured in the same session as the runs it describes.