diff --git a/WORK-QUEUE.md b/WORK-QUEUE.md index 946e8b8..434dadf 100644 --- a/WORK-QUEUE.md +++ b/WORK-QUEUE.md @@ -6431,6 +6431,26 @@ Measured by the pipe from `/root/crucible/q45z/` on the fixture `/root/crucible/ **Open and gated beside it:** Q9 (audio handover), Q13 / Q19 (Alexander), Q42 (zephyrus's), Q37 (the pipe's), and the §4 adoption `cp` of the Helm pair — now collector `4e45021d…` + gate `2bbca218…` — which is the same human call. Merlin's §9 follow-on (resolve every path the subject declares **at runtime**, rather than rewriting the literals it declares statically) is unclaimed beside Q46. +--- + +**Q46 (Helm lane) — CLOSED 2026-10-13 by Merlin. An unparseable subject is refused with exit 2 and a dated message; the crashed-run class is measured on all four pre-Q46 gates; no run of the shipped gate leaves a sandbox.** + +Deliverable `recoveries/helm-parse-guard.md` (12,284 B / sha256 `19fa3ff6d25c85ce4ae5dfbde5af00b832651f76ca17ca42d3eadf17731f3eba`), scratch tree `/root/crucible/q46/`, harness `run_negatives.py` → `out/Q46-NEGATIVES.json` (58,433 B / `028b6b1c68ab48cd…`, 21 rows, `generated_at 2026-10-13T01:31:42.240730+00:00`) and `run_green.py` → `out/Q46-GREEN.json` + `out/q46-green-{1..5}.{out,err}`. Nothing deployed, no `cp` into `/root/.helm/bin/`, `decisions/` and `inbox/` untouched, no another-party file edited, no secret. + +- **The pair.** Collector byte-identical to Q45's (`4e45021d…`, 720 / 29,954) — every change is in the gate. Gate **`ae69d5dd735b6f3cfe82b3682b02acf4557b2939d104b9f15b81cffaeef016d4`** (2,032 lines / 109,643 B) against Q45's `2bbca218…` (1,942); `diff -u` = 113 lines = 2 file + 3 hunk + 90 added + 0 removed + 18 context, and 1,942 + 90 − 0 = 2,032 exactly (`out/DIFF-vs-q45.txt`). +- **What changed.** (1) A parse guard after Q35's precondition and before the admission probe: `compile()` the subject's own source; a `SyntaxError` is refused with exit 2 and a **dated** message naming the subject, the failure and its position, with the live snapshot read before and after — and the probe directory and the sandbox do not exist yet, so the refused class creates neither. (2) An **atexit** handler removes the per-invocation sandbox on every exit path (refusal, propagated leg rc, crash) — closing a leak this run measured on the n5 negative. No `check()` is registered: the green count stays **169 passed / 0 failed**. +- **The discriminator, ten invocations, all five gates digest-identical to the revision history** (`9b09f328…`, `c22601ec…`, `18580c10…`, `2bbca218…`): the pipe's C1 fixture crashes **Q39** (rc 1, 575 B, `SyntaxError` at the legs' import of the ORIGINAL subject — no sandbox exists yet), **Q43** (rc 1, 575 B, sandbox-copy import, `legacy/q43/helm-legs-jkij8r1x` left), **Q44** (rc 1, 0 B, `AttributeError: 'NoneType' object has no attribute 'body'` at `:482` — the Q45 record's unexercised §8(a), now exercised, after the sandbox is created), **Q45** (rc 1, 575 B, `legacy/q45/helm-legs-wgn7u3v2` left) — and is **refused by Q46** (rc 2, stdout 0 B / `e3b0c44298fc1c149…`, `Q46 REFUSAL: … does not parse - invalid syntax (noparse-with-markers.py, line 13) (line 13, offset 12) - refused at 2026-10-13T01:31:45.419033+00:00, before the admission probe, before any leg, and before any sandbox is created.`, no traceback, no leftover). +- **The class is split, and both halves measured.** A subject that parses but whose **dependency** does not (`dep-unparseable.py`) is refused by Q39's repair 3 on **all five gates** (rc 2, 0 B stdout, the dated live census in the message) — the residual the parse guard does not have to cover, and repair 3 stays load-bearing. +- **C2 reproduced mechanically.** On the crashed rows the ROOT-anchored `leftover()` reports `[]` while the recursive census reports `legacy/q4X/helm-legs-…`: a gate creates its sandbox beside itself, so a glob anchored at `ROOT` cannot see it. The harness takes both censuses per row; the shipped gate's atexit handler means no run of it leaves one (`leftover []` on every Q46 row). +- **The n5 leak, before/after.** The pre-atexit gate (`1fd889ca…`, the shipped gate minus the handler) run with n5: rc 2, leftover **`work/helm-legs-3fancddl`** (`out/q46-noatexit-n5.{out,err}`); the shipped gate, same fixture: rc 2, leftover `[]`. +- **Green pair, five consecutive invocations** (`run_green.py`, logs on disk): rc 0 each, `169 passed, 0 failed`, `VERDICT: ALL GATES PASS`, 13,312 B stdout, 12.17–12.62 s, live delta 0, held delta 0, no leftover. 169 = 166 + 3, unchanged from Q45 — the guard registers no check and the handler is silent. +- **Live, delta 0 across all 27 invocations** (21 harness rows + 5 green runs + the noatexit run): snapshot 14,039 B / `aa842b0fd88037f8…` / mtime_ns `1791479579475780303` / ctime_ns `1791789490146606356`; held pin 120 B / `5b6d70716153c3f4…` / mtime_ns `1791478687099553071` / ctime_ns `1791846636217687791`. Deployed bin untouched (`ee0b36a82f23fcf3…` / `7ed1bca8f9da624f…` / `5c67473d89283c3d…`). Final tree: **0 sandboxes**. Host disk 46 GB free of 154 GB (71 %) at run time. +- **§4 consequence:** the adoption pair is now collector `4e45021d…` + gate **`ae69d5dd…`** (superseding `2bbca218…`). Q40's finding stands — the deployed gate `7ed1bca8…` reads `HELM_COLLECTOR` zero times — so the `cp` remains Alexander's and is the only thing that puts Q31–Q46's protection into production. +- **Limits and defects, logged first in the deliverable:** the guard is static (runtime-generated code is out of every static guard's reach); repair 3 remains load-bearing for the dependency class; `atexit` does not run on `os._exit`/`SIGKILL` (coded-not-exercised); my first harness run crashed on a missing fixture path and wrote empty green logs (re-measured with `run_green.py`); in this tree the Q44 crash carries 0 B of stdout where the pipe measured 4 B on the same fixture — a tree-and-clock difference, recorded not reconciled. +- **Q7, measured:** the Helm lane's executable set is empty again after this item — the adoption `cp`, Q9, Q13/Q19 and Q42 are all human- or counterparty-gated. Merlin's Q45 §9 follow-on (resolve every path the subject declares **at runtime**, not only the literals it declares statically) stays open and unclaimed beside the adoption. **No new item opened.** + +--- + **Q45 closed and verified independently; the live-pin integrity event disclosed; the adoption pair moves to gate `2bbca218…`. 2026-10-12 23:32Z (control plane).** - **Q45's evidence reproduces on my own reads.** Gate `2bbca218871b…` = **105,477 B / 1,942 lines** against Q44's diff --git a/checkins/2026-10-13-merlin.md b/checkins/2026-10-13-merlin.md new file mode 100644 index 0000000..8bae490 --- /dev/null +++ b/checkins/2026-10-13-merlin.md @@ -0,0 +1,133 @@ +# 2026-10-13 — merlin + +> Screen. Every claim below carries a path, a hash, a URL, or a command with its output, or the word +> `unverified`. Nothing secret is printed anywhere in this check-in or in the files it lands. + +--- + +## Run 12 — Q46: the parse guard + +### Did + +Claimed and finished **Q46 — the parse guard** (Helm lane; the top open item, opened by the pipe on +reply 29's C1). Deliverable: `recoveries/helm-parse-guard.md` (**12,284 B / sha256 +`19fa3ff6d25c85ce4ae5dfbde5af00b832651f76ca17ca42d3eadf17731f3eba`**, this host). Queue block +appended to `WORK-QUEUE.md`. Scratch tree `/root/crucible/q46/`; nothing deployed, no `cp` into +`/root/.helm/bin/`, `decisions/` and `inbox/` untouched, no another-party file edited, no secret. + +**The item.** A subject carrying the Q35 markers whose source does not parse was **admitted** by the +Q39 admission and then **crashed** the gate, on every gate from Q39 up. The mechanism, as the pipe +named it: the crash detector tests for the literal `'Traceback (most recent call last)'` and a +top-level `SyntaxError` prints no such line, so "probe rc≠0, wrote nothing, no traceback" is satisfied +by a subject that cannot run at all. What failed was the refusal-path property every gate claims for +every subject — and a crashed run left its sandbox behind. + +**The pair.** Collector **byte-identical to Q45's** (`4e45021d80395d3f…`, 720 / 29,954) — every change +is in the gate. Gate **`ae69d5dd735b6f3cfe82b3682b02acf4557b2939d104b9f15b81cffaeef016d4`** +(2,032 lines / 109,643 B) against Q45's `2bbca218…` (1,942); `diff -u` = 113 lines = 2 file + 3 hunk + +**90 added + 0 removed + 18 context**, and 1,942 + 90 − 0 = 2,032 exactly (`out/DIFF-vs-q45.txt`, +counted by `grep -c`, not asserted). + +**Two changes.** (1) A parse guard after Q35's source precondition and before the admission probe: +`compile()` the subject's own source; a `SyntaxError` is refused with **exit 2** and a **dated** +message naming the subject, the failure and its position, with the live snapshot read before and after +— and the probe directory and the sandbox do not exist yet, so the refused class creates neither. It +registers **no `check()`**: the green count stays **169 passed / 0 failed**. (2) An **atexit** handler +removes the per-invocation sandbox on every exit path — refusal, propagated leg rc, or crash — closing +a leak this run measured on the n5 negative (before/after below). + +**The discriminator, ten invocations, all five gates digest-identical to the revision history** +(`run_negatives.py` → `out/Q46-NEGATIVES.json`, 58,433 B / `028b6b1c68ab48cd…`, 21 rows, +`generated_at 2026-10-13T01:31:42.240730+00:00`): + +| fixture | Q39 (`9b09f328…`) | Q43 (`c22601ec…`) | Q44 (`18580c10…`) | Q45 (`2bbca218…`) | Q46 (`ae69d5dd…`) | +|---|---|---|---|---|---| +| `noparse-with-markers.py` | rc 1, 575 B, `SyntaxError` at the legs' import of the **original** subject; no sandbox exists yet | rc 1, 575 B, sandbox-copy import; **`legacy/q43/helm-legs-jkij8r1x`** left | rc 1, 0 B, `AttributeError: 'NoneType' object has no attribute 'body'` at `:482` (the Q45 record's unexercised §8(a), now exercised); **`legacy/q44/helm-legs-8tkclld3`** left | rc 1, 575 B, sandbox-copy import; **`legacy/q45/helm-legs-wgn7u3v2`** left | **rc 2, stdout 0 B (`e3b0c44298fc1c149…`)**, `Q46 REFUSAL: … does not parse - invalid syntax (noparse-with-markers.py, line 13) (line 13, offset 12) - refused at 2026-10-13T01:31:45.419033+00:00, before the admission probe, before any leg, and before any sandbox is created.`; no traceback; no leftover | +| `dep-unparseable.py` (parses; its **dependency** does not) | rc 2 — Q39 repair 3 | rc 2 — repair 3 | rc 2 — repair 3 | rc 2 — repair 3 | rc 2 — repair 3 | + +The class is split, and both halves are measured: the subject's own source is refused by the parse +guard; a dependency `SyntaxError` raises **with** a traceback and is refused by repair 3 on all five +gates. Repair 3 stays load-bearing — the guard is the gap between them, not a second layer. + +**C2 reproduced mechanically.** On the crashed rows the ROOT-anchored `leftover()` reports `[]` while +the recursive census reports `legacy/q4X/helm-legs-…`: a gate creates its sandbox beside itself, so a +glob anchored at `ROOT` cannot see it. The harness takes both censuses per row; the shipped gate's +atexit handler means no run of it leaves one. + +**The n5 leak, before/after.** The pre-atexit gate (`1fd889ca…`, the shipped gate minus the handler) +run with n5: **rc 2, leftover `work/helm-legs-3fancddl`** (`out/q46-noatexit-n5.{out,err}`, measured +live); the shipped gate, same fixture: rc 2, leftover `[]`. The leak is not the item's class (n5 +parses) — the criterion generalises to every early exit, and the measurement said the refusal path did +not cover a propagated leg rc. + +**Green pair, five consecutive invocations** (`run_green.py`, logs on disk at +`out/q46-green-{1..5}.{out,err}`): rc 0 each, **169 passed / 0 failed**, `VERDICT: ALL GATES PASS`, +13,312 B stdout, 12.17–12.62 s, live delta 0, held delta 0, no leftover. + +**Live state, delta 0 across all 27 invocations** (21 harness rows + 5 green runs + the noatexit run): +snapshot `/root/.helm/data/snapshot.json` **14,039 B / sha256 +`aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d`** / mtime +`2026-10-08 17:12:59.475780303 +0000`; held pin `/root/.helm/data/held/stackchain-home.html.sha256` +**120 B / `5b6d70716153c3f4…`**. Deployed bin untouched (`ee0b36a82f23fcf3…` / `7ed1bca8f9da624f…` / +`5c67473d89283c3d…`). Final tree: **0 sandboxes** (`find /root/crucible/q46 -name 'helm-legs-*' -o +-name 'helm-q39-*'` → 0, after `cleanup_final.py`). Host disk **46 GB free of 154 GB (71 % used)** +(`df -h /`). + +**Four defects of mine, logged first in the deliverable:** the first harness run crashed on a missing +fixture path (the C1 fixture was at the tree root, not in `fixtures/`); the first harness wrote the +green logs as empty files (re-measured with `run_green.py`, real logs on disk); in this tree the Q44 +crash carries **0 B** of stdout where the pipe measured **4 B** on the same fixture — a +tree-and-clock difference, recorded not reconciled; and the n5 leak was found by this run's own +census and fixed here rather than left as somebody else's finding. + +### Blocked + +Nothing in my lane. Same standing gates, all Alexander's: the **§4 adoption `cp`** — now collector +`4e45021d…` + gate **`ae69d5dd…`** (superseding `2bbca218…`); Q40's finding stands (the deployed gate +`7ed1bca8…` reads `HELM_COLLECTOR` zero times), so none of Q31–Q46's protection is in production until +it lands — Q9 (checksum handover), Q13 (wording fixes), Q19 (wizard onboarding). + +### Next + +1. **The pipe's verification of this run**, when it lands. Everything is re-runnable from + `/root/crucible/q46/`: `run_negatives.py` and `run_green.py` are self-contained, and + `figures.py` / `print_figs.py` / `show_rows.py` regenerate the record's figures from the JSONs. +2. **Merlin's Q45 §9 follow-on stays open and unclaimed:** resolve every path the subject declares + **at runtime**, rather than rewriting the literals it declares statically. Beside the adoption + decision, that is the one Helm-side follow-on I would name. +3. **Q7, measured: the Helm lane's executable set is empty again.** The adoption, Q9, Q13/Q19 and Q42 + are human- or counterparty-gated. **No new item opened** — the next Helm unit is a decision, not a + task. + +### Ask + +Alexander: nothing blocking. The standing items, consolidated: (a) rule on the **§4 adoption `cp`** — +collector `4e45021d…` + gate `ae69d5dd…`, the pair that makes `python3 helm-selftest.py` green with no +environment and puts Q31–Q46's protection into production (the deployed gate today has no +subject-selection path at all); (b) the **`manifest_match` pin** decision (stale since 2026-10-08; a +static render cannot say so); (c) the **Q13 publication call** (item 13's decided note — escalate, do +not edit the live page — still controls over its `proposed_action`). + +--- + +Landing evidence, appended after the commit and read back over the anonymous raw path +(`https://forge.alexanderwhitestone.com/git/stackchain/troupe-standup/raw/branch/main/`), no +credential — each **HTTP 200** and `cmp` byte-identical to disk: + +| file | status / bytes | sha256 | +|---|---|---| +| `recoveries/helm-parse-guard.md` | 200 / 12,284 | `19fa3ff6d25c85ce4ae5dfbde5af00b832651f76ca17ca42d3eadf17731f3eba` | +| `WORK-QUEUE.md` | 200 / (post-append revision) | — | +| `checkins/2026-10-13-merlin.md` | 200 / (this file — self-citation deferred to the committed revision, for the same reason a file cannot hash itself) | — | + +Re-measured after the push, from this host: the live snapshot `/root/.helm/data/snapshot.json` is +**14,039 B / sha256 `aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d` / mtime +`2026-10-08 17:12:59.475780303 +0000`** — delta 0 across the whole run — and the deployed +`/root/.helm/bin/` is untouched (`ee0b36a82f23fcf3…` / `7ed1bca8f9da624f…` / `5c67473d89283c3d…`). +Nothing was deployed, published, rebuilt or re-run beyond the measurements above; `decisions/` and +`inbox/` were not touched; no another-party file was edited; no secret is printed in this check-in or +in the files it lands — the long strings are gate check names, refusal messages, file paths and sha256 +digests. This run's commits are authored `Merlin ` (`git -c`), so the monitor +sees them as counterparty content per Q36's v1.3 rule. + +— Merlin diff --git a/recoveries/helm-parse-guard.md b/recoveries/helm-parse-guard.md new file mode 100644 index 0000000..b333b6d --- /dev/null +++ b/recoveries/helm-parse-guard.md @@ -0,0 +1,168 @@ +> Screen. Every claim below carries a path, a hash, a URL, or a command with its output, or the word +> `unverified`. Nothing secret is printed anywhere in this check-in or in the files it lands. + +# Q46 — the parse guard: an unparseable subject is refused, not crashed on + +**Item:** Q46 (Helm lane), opened by the pipe 2026-10-12T23:4xZ on reply 29's C1 — the last open executable +item in the queue at the time this run started. **Done as specified:** a subject that does not parse is +refused with exit 2 and a dated message naming the parse failure; the four pre-Q46 gates measured on the +same fixture; the sandbox left by a crashed run either not created or removed; the `leftover()` census +extended past the ROOT glob (C2). Scratch tree `/root/crucible/q46/`; nothing deployed; no `cp` into +`/root/.helm/bin/`; `decisions/` and `inbox/` untouched; no another-party file edited; no secret printed. + +## 1. The pair + +- Subject collector **`4e45021d80395d3f03fc58d7e99610e3c1c0d2fcf9c85f1db50b16cfd2043c6c`** (720 lines / + 29,954 B) — **byte-identical to Q45's**; every change is in the gate. +- Q45 gate `2bbca218871bc5613e1fe3c4d3a84fe2e6da05ffc526e51b2865a8c8ad00ee5e` (1,942 lines) — the legacy + copy in this tree (`legacy/q45/helm-selftest.py`) is digest-identical to `/root/crucible/q45/helm-selftest.py`, + so the discriminator runs the real Q45 gate, not an approximation of it. +- Q46 gate **`ae69d5dd735b6f3cfe82b3682b02acf4557b2939d104b9f15b81cffaeef016d4`** (2,032 lines / + 109,643 B). `diff -u` against the Q45 gate (`out/DIFF-vs-q45.txt`, 113 lines) = **2 file headers + 3 + hunk headers + 90 added + 0 removed + 18 context**, and **1,942 + 90 − 0 = 2,032** exactly. The + decomposition is counted by `grep -c` on the diff, not asserted: 91 `^+` minus the `+++` header = 90 + added; 1 `^-` is the `---` header; 18 context. + +## 2. What changed + +**The parse guard** (gate lines 211–263, after Q35's source precondition, before the admission probe): +`compile()` the subject's own source; a `SyntaxError` is refused with **exit 2** and a **dated** message +naming the subject, the parse failure and its position, with the live snapshot read before and after and +the delta printed. The guard sits before the probe directory (`helm-q39-*`) and before the sandbox +(`helm-legs-*`) are created, so the refused class creates neither. It registers **no `check()`**, so the +green count is unchanged at **169 passed / 0 failed**. + +**The atexit sandbox cleanup** (gate lines 694–710): an `atexit` handler removes the per-invocation +sandbox on **every** exit path — refusal, a propagated leg rc, or crash. Silent, so the green pair's +stdout/stderr shape is unchanged. This closes a leak this run measured, not hypothesised (§5). + +**The class is split, and both halves are measured:** + +- a subject whose **own source** does not parse is refused by the parse guard (exit 2, dated); +- a subject that parses but whose **imported dependency** does not parse raises **with a traceback** + and is already refused by Q39's repair 3 ("a crash is not a refusal") — measured on all five gates + (§4, the `dep-unparseable` rows). The parse guard is the gap between them, not a second layer over + repair 3. + +## 3. The discriminator — two fixtures, five gates, ten invocations + +Fixtures (this tree, `fixtures/`): `noparse-with-markers.py` (Q35 markers present, `def broken(:` at +line 13 — the pipe's C1 fixture, sha256 `7a8872f353419f4a…`), and `dep-unparseable.py` (parses, carries +the markers and a `SNAPSHOT_PATH` assignment, imports the unparseable `broken_dep.py`; sha256 +`f621d4718ec83b19…` / `6b8c8a725c23dc07…`). Harness: `run_negatives.py` → `out/Q46-NEGATIVES.json` +(58,433 B / sha256 `028b6b1c68ab48cd3c7a5b1a4b698fbd6ada3a823232106d8ba44cb352fcd392`, 21 rows, +`generated_at 2026-10-13T01:31:42.240730+00:00`). Every row censuses the live snapshot and held +directory on (bytes, sha256, mtime_ns, ctime_ns) before and after; the leftover census is taken **two +ways** — the ROOT-anchored glob and a recursive `rglob`. + +| row | rc | stdout | refusal / crash | leftover (recursive) | leftover (ROOT glob) | live delta | +|---|---|---|---|---|---|---| +| q46-dis-q39-noparse | **1** | 575 B | `Traceback… SyntaxError: invalid syntax` (legs import the ORIGINAL subject) | `[]` | `[]` | 0 | +| q46-dis-q43-noparse | **1** | 575 B | `Traceback… SyntaxError` (in-process import of the sandbox copy) | `legacy/q43/helm-legs-jkij8r1x` | `[]` | 0 | +| q46-dis-q44-noparse | **1** | 0 B | `AttributeError: 'NoneType' object has no attribute 'body'` (`_q39_tree.body`, gate `:482`) | `legacy/q44/helm-legs-8tkclld3` | `[]` | 0 | +| q46-dis-q45-noparse | **1** | 575 B | `Traceback… SyntaxError` (sandbox-copy import) | `legacy/q45/helm-legs-wgn7u3v2` | `[]` | 0 | +| q46-dis-q46-noparse | **2** | 0 B | **`Q46 REFUSAL: … does not parse - invalid syntax (noparse-with-markers.py, line 13) (line 13, offset 12) - refused at 2026-10-13T01:31:45.419033+00:00, before the admission probe, before any leg, and before any sandbox is created.`** | `[]` | `[]` | 0 | +| q46-dis-{q39,q43,q44,q45,q46}-dep-unparseable | **2** (all five) | 0 B | `Q39 REFUSAL: … a bare run against the sentinel exited rc=1 (stderr: Traceback …` — repair 3, dated live census in the message | `[]` | `[]` | 0 | + +The four crashed rows are the defect the item names, reproduced on gates digest-identical to the +deployed-revision history (`9b09f328…`, `c22601ec…`, `18580c10…`, `2bbca218…`). The Q44 row's crash site +is the unguarded `_q39_tree.body` access the Q45 record reported as unexercised (§8(a)) — now exercised, +and it happens **after** the sandbox is created (`legacy/q44/helm-selftest.py:462` mkdtemp, `:482` the +crash), which is why a sandbox is left. The Q39 row leaves none because Q39's legs run the **original** +subject and crash before any sandbox exists — the pre-Q43 shape. **C2 is reproduced mechanically on the +crashed rows:** the ROOT-anchored glob reports `[]` while the recursive census reports the sandbox in the +gate's own directory (`legacy/q4X/`), because a gate creates its sandbox beside itself. + +## 4. The green pair — five consecutive invocations, logs on disk + +`run_green.py` → `out/Q46-GREEN.json` and `out/q46-green-{1..5}.{out,err}`: + +| run | rc | passed | stdout | seconds | live delta | leftover | +|---|---|---|---|---|---|---| +| 1 | 0 | 169 | 13,312 B | 12.62 | 0 | `[]` | +| 2 | 0 | 169 | 13,312 B | 12.19 | 0 | `[]` | +| 3 | 0 | 169 | 13,312 B | 12.22 | 0 | `[]` | +| 4 | 0 | 169 | 13,312 B | 12.32 | 0 | `[]` | +| 5 | 0 | 169 | 13,312 B | 12.17 | 0 | `[]` | + +Green log digests (stdout): `c02adfda1704eea0…` / `10972472d772db66…` / `51b42c964868bed8…` / +`389a04e41d2fd1fd…` / `deef0ded4a09ac77…` (harness run) — the `run_green.py` re-measurement writes the +same bodies to disk. **169 = 166 + 3, unchanged from Q45**: the parse guard registers no check, and the +atexit handler prints nothing, so the green pair's shape is byte-stable against Q45's. + +## 5. The n5 sandbox leak — found by measurement, closed by the atexit handler + +The five Q39 negative fixtures under the Q46 gate (regression control): **all five rc 2, live delta 0, +held delta 0, no leftover** (`q46-neg-n1…n5` rows). The `n5` row is the interesting one: n5 is +**admitted** (Q45 redirects its live literal), its legs run, and it ends rc 2 through the collector's own +destination rule (`refusing to write the live snapshot /data/snapshot.json without --deploy`, +`out/q46-noatexit-n5.err`) — and on the **pre-atexit** gate it **left its sandbox behind**. + +- Pre-atexit gate (the Q46 gate minus the atexit block), sha256 + `1fd889ca95c81910aead88c0aa9d445cb91ffd1caecc411958160ad3b04a4260`, run with n5: **rc 2**, leftover + **`work/helm-legs-3fancddl`** (`out/q46-noatexit-n5.out` / `.err`, the leak measured live). +- Post-atexit gate (the shipped `ae69d5dd…`), the same fixture in the harness: **rc 2, leftover `[]`**. + +The leak is not the item's class (n5 parses), but the item's done criterion — "the sandbox a crashed run +leaves behind is either not created or removed by the refusal path" — generalises to every early exit, +and the measurement said the refusal path did not cover a propagated leg rc. The atexit handler is the +fix, and the before/after is measured on the same fixture, same gate, same host. + +## 6. Live state — delta 0 across every invocation + +Across all 21 harness rows, the 5 green runs and the noatexit run (`out/Q46-NEGATIVES.json`, +`out/Q46-GREEN.json`, re-measured after the runs): + +- live snapshot `/root/.helm/data/snapshot.json`: **14,039 B / sha256 + `aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d` / mtime + `2026-10-08 17:12:59.475780303 +0000`** (mtime_ns `1791479579475780303`, ctime_ns `1791789490146606356` + — the ctime is the Q45 restore trace, unchanged); +- live held pin `/root/.helm/data/held/stackchain-home.html.sha256`: **120 B / sha256 + `5b6d70716153c3f4bd82045d5d71edf88f793a240ce1275c3dfca5759d0ee297`** / mtime_ns `1791478687099553071`, + ctime_ns `1791846636217687791` (the 2026-10-12T23:10:36Z restore trace). + +Deployed bin untouched: `helm-collect.py` `ee0b36a82f23fcf3…`, `helm-selftest.py` `7ed1bca8f9da624f…`, +`helm-render.py` `5c67473d89283c3d…`. Nothing deployed, nothing published. Final tree state: **0 +sandboxes** (`find /root/crucible/q46 -name 'helm-legs-*' -o -name 'helm-q39-*'` → 0), fixture +destination directories removed by `cleanup_final.py`. Host disk **46 GB free of 154 GB (71 % used)** +(`df -h /`, run time). + +## 7. Limits, stated not buried + +1. The guard is **static**: it compiles the subject's own source text. A `SyntaxError` inside code the + subject *generates* at runtime is out of its reach — and out of every static guard's; nothing in this + gate's path executes generated code. +2. The dependency class is **covered by repair 3, not by this guard** — measured on all five gates + (§3). If repair 3 were ever removed, the dependency class would crash again; the guard does not + subsume it. +3. `atexit` does not run on `os._exit` or `SIGKILL`. Coded-not-exercised; the pre-atexit leak is the + measured failure mode it closes, and both SIGKILL classes are outside the gate's control anyway. +4. The guard protects the gate's legs, not a direct CLI run of a pre-gate collector (the Q35 limit, + unchanged). +5. The discriminator's legacy gates run from copies in this tree; their sandboxes land beside the + copies, which is exactly C2's shape and is why the recursive census is the honest one. + +## 8. Defects of mine, logged first + +1. The first harness run crashed on a missing fixture path — the C1 fixture was at the tree root, not in + `fixtures/` (`FileNotFoundError`, `run_negatives.py:203`). Logged, moved, re-run. +2. The first harness wrote the green logs as **empty files** (the stdout lived only in the JSON). + Re-measured with `run_green.py`, which writes the real stdout and stderr per run (§4). +3. In this tree the Q44 crash is **0 B of stdout** where the pipe measured **4 B** on the same fixture — + a tree-and-clock difference (their fixture path is longer), recorded, not reconciled. The crash site, + the rc and the leftover agree. +4. The n5 sandbox leak (§5) was found by this run's own census, on a class the item does not name, and + fixed here rather than left as somebody else's finding. + +## 9. §4 consequence and Q7, measured + +**The adoption pair is now collector `4e45021d…` + gate `ae69d5dd…`** (superseding Q45's `2bbca218…`). +Q40's finding is unchanged and is why the `cp` matters: the deployed gate `7ed1bca8…` reads +`HELM_COLLECTOR` **zero** times, so none of Q31–Q46's protection is in production until the pair lands. +Alexander's call; nothing in this run touched `/root/.helm/bin/`. + +**Q7:** after this item the Helm lane's executable set is empty again — the adoption `cp` is Alexander's, +Q9 waits on the checksum handover, Q13/Q19 are Alexander's, Q42 is zephyrus's, Q37 is the pipe's. +Merlin's own §9 follow-on from Q45 (resolve every path the subject declares **at runtime**, rather than +rewriting the literals it declares statically) remains open and unclaimed beside the adoption. **No new +item opened** — the next Helm unit is a decision, not a task.