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.
This commit is contained in:
Merlin committed 2026-10-12 02:37:25 +00:00
1 parent 7e306dc2cd
commit dd0bc69061
3 files changed
+206

No files matched your search

+29
View File
@@ -4678,3 +4678,32 @@ both sides in one run pair (deployed collector as subject → refused, no leg ex
readings (sha256 and mtime_ns), and the mechanism probe repeated from the pipe's tree. **Out of scope:** the §4 readings (sha256 and mtime_ns), and the mechanism probe repeated from the pipe's tree. **Out of scope:** the §4
`cp` into `/root/.helm/bin/` — the shipped gate travelling with the collector is still Alexander's, and this item `cp` into `/root/.helm/bin/` — the shipped gate travelling with the collector is still Alexander's, and this item
does not move it. does not move it.
**Status: DONE — `recoveries/helm-precondition-gate.md` (8,261 B / sha256
`a5d3ad6af205f3c1c8caf4f4baa2ee01548b5a89697ff5478e3fc52a9a61656b`), Merlin, 2026-10-12T02:10–02:36Z, this host.**
- The guard is now a **precondition**: 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** (`_load_collector` at `:679`) and before G1; a subject without the rule is refused with **exit 2**, a
stderr message naming the subject and the missing rule, and the live snapshot read before/after the refusal.
Gate moved **+61/−2** (985 → 1,044 lines), sha256 `45e53187…` → `643e5387…`; the collector is byte-identical to
the Q34 pair's (`4e45021d…`). The old foot-of-suite source check is **kept** at `:1032` (it passes on every green
run, implied by the precondition), so the suite count is unchanged: the precondition registers no `check()`.
- **Green pair, 7 consecutive invocations**, exit 0 each, `164 passed, 0 failed` / `VERDICT: ALL GATES PASS`
(log digests in the deliverable). Live snapshot **delta 0 across every leg** — sha256
`aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d` / mtime_ns `1791479579475780303` /
14,039 B throughout.
- **Discriminator, both sides in one run pair.** Deployed collector as subject (`HELM_COLLECTOR=/root/.helm/bin/
helm-collect.py`): **exit 2, stdout 0 bytes** (the empty-file digest `e3b0c442…` — no `helm-selftest:` summary,
no PASS line, which is how "no leg executed" is measured, since the report block is at the suite's foot), live
delta 0. A missing subject is refused the same way, not crashed. Pair as subject: unchanged green, above.
- **Mechanism probe from the pipe's tree** (`q35-mechanism.py`, `mech-probe.log`): each collector imported, its
default destination (`SNAPSHOT_PATH`) rebound to scratch, `main()` called bare — deployed `ee0b36a8…` **rc=0,
no refusal, 18,523 B written** (sha256 `9331b218…`, schema `helm.snapshot/1`); pair `4e45021d…` **rc=2, refusal
naming the path and `--deploy`, nothing written**. Rebinding does not disable the rule — it keys on the module's
own `SNAPSHOT_PATH` — it only removes the blast radius.
- **Limits stated in the deliverable:** the gate run bare now refuses itself (the default subject is the pre-gate
deployed collector — the §4 `cp` is what makes the shipped gate green, still Alexander's); the precondition
reads a source marker, not behavioral proof (the probe is the behavioral half; sandboxing an untrusted import
is the very thing the precondition prevents); and the rule protects the gate's legs, not a direct CLI run of a
pre-gate collector. Nothing deployed; `decisions/` and `inbox/` untouched.
+69
View File
@@ -116,3 +116,72 @@ no secret is printed in this check-in or in the files it lands — the long stri
names, snapshot JSON keys, file paths and sha256 digests. names, snapshot JSON keys, file paths and sha256 digests.
— Merlin — Merlin
---
## Run 2 — Q35: the destination-rule guard is now a precondition
Claimed and finished **Q35** (Helm lane, opened by the pipe this day after Q34 closed) — the twice-fired
class from Q34's incident, made mechanical at the gate instead of at the operator. Deliverable:
`recoveries/helm-precondition-gate.md` (**8,261 B / sha256
`a5d3ad6af205f3c1c8caf4f4baa2ee01548b5a89697ff5478e3fc52a9a61656b`**, this host). Scratch tree
`/root/crucible/q35/`; nothing deployed; the §4 `cp` is untouched.
**What changed.** The gate's source check — *"the collector the gate runs carries the destination rule"* —
used to sit at the **foot of the suite**, after every leg that could execute the subject collector. Q34's
incident is what that costs: the bare `--skip-tests` refusal leg executed an ungated subject and wrote the
live snapshot, and the check red-flagged it only afterwards. The check is now a **precondition** at
`helm-selftest.py:96–114`: the subject named by `HELM_COLLECTOR` is verified to carry the rule
(`--deploy` and `SNAPSHOT_PATH.resolve()` in its source) **before the subject is imported** (`:679`) and
before G1, and refused with **exit 2** and a stderr message naming the subject and the missing rule, with
the live snapshot read before and after the refusal. Gate moved **+61/−2** (985 → 1,044 lines), sha256
`45e53187…` → `643e53874e42f7ae58160c06c47eb07846725eae397104d9265964506bfcd0c9`; the collector is
byte-identical to Q34's (`4e45021d…`). The precondition registers **no `check()`**, so the suite count is
unchanged — the old foot-of-suite check is kept at `:1032` and passes on every green run.
**Green pair, 7 consecutive invocations**, exit 0 each, `helm-selftest: 164 passed, 0 failed` /
`VERDICT: ALL GATES PASS` (`HELM_COLLECTOR=/root/crucible/q35/helm-collect.py python3 helm-selftest.py`;
log digests `32faa0e9…` / `bfa6d09e…` / `8bd47387…` / `62756176…` / `1c9639ef…` / `f9992981…` / `e5a2c5fd…`).
**Discriminator, both sides in one run pair.** Deployed collector as subject
(`HELM_COLLECTOR=/root/.helm/bin/helm-collect.py`, pre-gate `ee0b36a8…`): **exit 2, stdout 0 bytes**
(`q35-refuse-deployed.log` sha256 `e3b0c442…`, the empty-file digest — no `helm-selftest:` summary and no
PASS line exists, which is how "no leg executed" is measured, since the report block is at the suite's
foot), stderr the refusal with live-snapshot readings, **live snapshot delta 0** (sha256
`aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d` / mtime_ns `1791479579475780303` /
14,039 B — identical before and after, and across all seven green runs). A missing subject is refused the
same way, not crashed (`q35-refuse-missing.err`, sha256 `90038616…`).
**Mechanism probe from the pipe's tree** (`q35-mechanism.py` / `mech-probe.log`, sha256
`31df78f6…` / `c4714d74…`): each collector imported, its default destination (`SNAPSHOT_PATH`) rebound to
scratch, `main()` called bare — deployed `ee0b36a8…` **rc=0, no refusal, 18,523 B written** (sha256
`9331b218…`, schema `helm.snapshot/1`); pair `4e45021d…` **rc=2, refusal naming the path and `--deploy`,
nothing written**. Rebinding does not disable the rule — it keys on the module's own `SNAPSHOT_PATH` — it
only removes the blast radius.
**Limits stated, not buried:** the gate run bare now refuses itself, because the default subject is the
pre-gate deployed collector — that is the item working as specified, and the §4 `cp` is what makes the
shipped gate green; the precondition reads a source marker, not behavioral proof (the probe is the
behavioral half, and sandboxing an untrusted import is the very thing the precondition prevents); and the
rule protects the gate's legs, not a direct CLI run of a pre-gate collector.
## Blocked
Nothing in my lane. Same three gates as Run 1 (Q13/Q19 on Alexander, Q9 on the audio handover, Q20 is
zephyrus's).
## Next
Q7's never-dry obligation: the unclaimed executable set is **zero** after this run — Q30–Q35 are closed,
Q9/Q13/Q19/Q21 wait on humans or the counterparty, Q20 is zephyrus's active sweep. The next item I can
see is a Helm-lane one, and it is the §4 adoption itself: the gate now green-refuses its own default
subject, so the shipped pair is a two-file `cp` (collector `4e45021d…` + gate `643e5387…`) that makes
`python3 helm-selftest.py` green with no environment. That is Alexander's move; I have not touched
`/root/.helm/bin/`. If he approves, the natural follow-on item is a verification run of the *deployed*
pair — same discriminator, one host change.
## Ask
Alexander: unchanged from Run 1 — nothing blocking. The two standing items are the §4 adoption (now a
cleaner ask: the pair travels together or the shipped gate stays self-refusing) and the `manifest_match`
pin decision (stale since 2026-10-08; a static render cannot say so).
+108
View File
@@ -0,0 +1,108 @@
# 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.