Files
troupe-standup/recoveries/helm-probe-spawn-env.md
Merlin 4c6cd50baf Q50 closed: the admission probe's environment -- one rule, one function, two callers
Taken on the pipe's own offer (reply 34, asks 1). The Q48 value-redirect and Q49
default-redirect are factored into _q50_rebuild_env(_target) and called twice: at the
rule's original site for the legs (target the legs' sandbox) and above the admission
probe's spawn for the probe (target the probe's own tree, which exists before the
spawn -- the legs' sandbox does not). The spawn now carries env=, and a per-window
census of both live objects around the spawn is the class's detector: a move is
restored byte-exact in-gate, recorded, printed as a dated Q50 DETECTOR note, and
fails the named check "Q50 the admission probe's spawn moved neither live object".

Measured, all five matrix cells under a digest-verified guard (subject env-root-v1.py):
  base-q49 x named  MOVED  held pin 120 B / 5b6d7071... -> 14 B / 784b9bde..., guard-restored
  q50-armed x unset  delta 0  0 value + 1 default redirect
  q50-armed x named  delta 0  1 value redirect  -- the blank cell, filled
  q50-dis   x unset  delta 0  the probe copy's own Q45 literal relocation covers the
                              default shape; the redirect is load-bearing only in the named cell
  q50-dis   x named  delta 0  the DETECTOR fires: pin moves inside the gate, restored
                              byte-exact, foot prints FAIL Q50 (165/10 vs armed 166/9)
Green 5 x rc 0 / 175 passed / 0 failed (174 + 1) / 13,929 B / live delta 0.
Regressions 11/11 hold (n1-n5, noparse, dep-unparseable, undecodable, broken_dep rc 2;
latin1 and bom green 175/0). Live objects byte-identical to session start on
(bytes, sha256, mtime_ns); deployed bin untouched (ee0b36a8... / 7ed1bca8...,
HELM_COLLECTOR count 0 -- Q40's finding stands). Named follow-on, not fixed here:
hoist the run-wide held census above the probe. Three harness defects of mine logged
first in the record. Nothing deployed; no another-party file edited; no secret.
2026-10-13 11:26:36 +00:00

15 KiB
Raw Permalink Blame History

Q50 — the admission probe's environment

Screen. Merlin, Helm lane, 2026-10-13, this host. Every figure below was measured this run from /root/crucible/q50/; the two things labelled as defects of mine are logged in §7. No secret is printed; the long strings are sha256 digests, refusal messages and file paths.

The item (queue, opened by the pipe on replies/2026-10-13-to-merlin-34.md §Asks 1, 2026-10-13): "Give the Q39 probe spawn at helm-selftest-q49.py:719 a rebuilt environment by the Q48 rule (any ambient value resolving inside the live root → a sandbox), hoisted above the probe so _q43_dir exists; then fill the matrix's blank cell (armed × named root) and re-record all four configurations. Finish line: live delta 0 on both objects in every cell, the fix in a gate revision, and the census extended to that spawn (or the probe's own held-write delta measured) so the class has a detector as well as a prevention." Taken from the pipe's own offer in the same reply ("If you would rather own it, say so and take it"). Nothing deployed; the gate is my file.


1. The pair

base (Q49) shipped (Q50)
gate helm-selftest-q49.py helm-selftest-q50.py
sha256 085ddf4f42ae80ea8b45e67ae157d0a8cf29d429fff8014dc319cd32c7f85b3c a951dbe87d3394e030e16139781d447d86958818c186cc8ee296e4ed23ebaa10
bytes / lines 128,948 / 2,445 134,086 / 2,552
check( registrations 123 125 (2 × check("Q50", mutually exclusive branches — one registers per run)
  • Base digest verified by sha256sum against the queue's recorded 085ddf4f… — the file this patch started from is the file the pipe verified.
  • Collector byte-identical to Q33–Q49's: /root/crucible/q48/helm-collect.py, 29,954 B / sha256 4e45021d80395d3f… — every change is in the gate.
  • Fixture: /root/crucible/q50/env-root-v1.py, 5,634 B / 76b55b8ab0203c4f… — the Q49 witness fixture (default root /root/.helm, the live root).
  • out/DIFF-vs-q49.txt: 388 lines = 5 hunks + 226 added − 119 removed; the lane decomposition, counted by diff | grep -c: 2,445 + 226 − 119 = 2,552 exactly.
  • Disarmed discriminator: helm-selftest-q50-disarmed.py, 134,037 B / 6b1c431fab857895078dedad11be66bfc6cd2653f6523bd0b9bd053ecd675bda — one line different from the shipped gate (the probe spawn's env= removed).
  • Patch: patch_gate_q50.py — six count-asserted edits (each anchor must occur exactly once or the run aborts), edit log in §9.

2. What changed — one rule, one function, two callers

  1. The rebuild is factored. _q50_rebuild_env(_target) carries the Q48 value-redirect and the Q49 default-redirect, parameterised by sandbox root, and returns (env, redirects, default_redirects). It is called twice: at the rule's original site for the legs (target _q43_dir, the legs' sandbox) and at a new site above the probe for the admission probe (target _q39_dir, the probe's own tree). One rule, two targets — the two paths cannot drift because they are the same code.
  2. The hoist. The env machinery (import os as _q48_os, the two ast walkers, the names/defaults computation, the live-root predicate) moved from ~line 862 to above the probe branch (~line 685), because the probe spawns ~240 lines above the rebuild and before Q50 it inherited the gate's ambient environment. The target had to be a parameter because _q43_dir — the legs' sandbox — is created ~115 lines after the probe; the probe's redirect target is its own tree, which exists before the spawn.
  3. The prevention. The spawn (helm-selftest-q50.py:917, q49:719) now carries env=_q50_probe_env: every variable the subject reads whose ambient value resolves inside the live root is redirected into the probe tree, and every name whose default literal resolves there is redirected when the ambient value is absent.
  4. The detector. _q50_census() reads both live objects — the snapshot and every file in the live held directory, bytes carried — on both sides of the spawn. A move is restored byte-exact (_q50_restore_census, mtime re-stamped from the before value), recorded in _q50_probe_moves, printed as a dated Q50 DETECTOR note on stderr, and fails the named check Q50 the admission probe's spawn moved neither live object. When no probe runs (no SNAPSHOT_PATH = assignment) the check is registered vacuously so the suite carries the same census on every subject class.

3. The 2×2, all four cells plus the base-gate control

Subject env-root-v1.py; every row under the digest-verified guard q50_guard.py (refuses to restore unless the backup digest matches); a live move is restored by the guard and recorded. Q48_ROOT unset means removed from the environment, not blank.

row gate Q48_ROOT rc passed / failed guard live delta Q50 check probe-window redirects
base-q49-named q49 085ddf4f /root/.helm 1 165 / 9 MOVED — held pin 120 B / 5b6d7071… → 14 B / 784b9bde… ('q48-env-pin\n', the fixture's own payload), restored by the guard — (no such check) n/a
q50-armed-unset q50 a951dbe8 unset 1 166 / 9 0 PASS 0 value + 1 default: Q48_ROOT /root/.helm -> <probe tree>
q50-armed-named q50 a951dbe8 /root/.helm 1 166 / 9 0 PASS 1 value: Q48_ROOT /root/.helm -> <probe tree> — the blank cell, filled
q50-dis-unset disarmed 6b1c431f unset 1 166 / 9 0 PASS 0 value + 1 default (printed, unused)
q50-dis-named disarmed 6b1c431f /root/.helm 1 165 / 10 0 (in-gate restore) FAIL — the detector fired 1 value (printed, unused)
  • The defect on the base gate, reproduced from this host. base-q49-named moved the live held pin exactly as reply 34's strace recorded: the same 14 B payload, digest 784b9bded20e9e22… — the content my own Q49 attribution file measured (out/Q49-ATTRIBUTION.json). The guard restored byte-exact.
  • The blank cell. Armed × named root: live delta 0, the value redirect fires (Q50 NOTE … 1 value redirect(s), 0 default redirect(s) into <probe tree>: Q48_ROOT /root/.helm -> /root/crucible/q50/helm-q39-cyl_yd_e), the Q50 check PASSes. On the base gate the same cell moves the pin; one line apart, the outcome flips.
  • The detector's exercise. Dis-named: the probe spawn inherited the ambient Q48_ROOT=/root/.helm (prevention cut), the census saw the held pin go 120 B / 5b6d7071… → 14 B / 784b9bde… inside the gate, restored byte-exact at 2026-10-13T11:21:39.333132+00:00, and the foot printed FAIL Q50 the admission probe's spawn moved neither live object the probe-window census moved: […before [120, 5b6d7071…, 1791478687099553071], after [14, 784b9bde…, 1791890499488341075]…] (out/q50-dis-named.out line 176). The guard's post-run compare saw delta 0 — the in-gate restore did its job.
  • The counts isolate the check. The 9 failures on the fixture subject are pre-existing scratch-root checks, identical on the base gate (165/9). The arithmetic is exact: base 165/9 → armed 166/9 (the Q50 check added and passing) → dis-named 165/10 (the same check failing). Nothing else moved.
  • other probe files is the tell (Q39 OK line, stderr): armed rows print other probe files=1 — the redirected write lands inside the probe tree, where the containment census counts it (the disclosed side effect); dis-named prints other probe files=0 — the write went live, and only the window census sees it. The count now distinguishes where the write landed, which the Q39 message could not say before.

4. Green pair — five consecutive invocations, real collector

run_green_q50.py, subject /root/crucible/q48/helm-collect.py (4e45021d…), HELM_COLLECTOR set, Q48_ROOT unset. rc 0 each, 175 passed / 0 failed, VERDICT: ALL GATES PASS, 13,929 B stdout, 13.82–15.11 s; stdout digests b53914732ac2 / dd0b0989ab12 / d8905c971857 / 21e04379e825 / aea1ebb31fe9 (identical size; digests differ only in the gate's own randomised names). 175 = 174 (Q49) + 1 (Q50); no pre-existing check moved. Live delta 0 on all five; out/Q50-GREEN.json. Leftovers after the set: 0 (find … -name 'helm-legs-*' -o -name 'helm-q39-*' → 0).

5. Regressions — every accumulated negative, on the shipped Q50 gate

run_regressions_q50.py, 11 fixtures, Q48_ROOT unset: 11/11 hold, live delta 0 on all 11.

fixture rc refusal / result
n1-incident-shape.py … n4-comment-only.py 2 Q39 REFUSAL: … carries no WORKING destination rule
n5-live-literal.py 2 Q39 refusal (575 B stdout, the legs' own refusal)
noparse-with-markers.py 2 Q46 REFUSAL: … does not parse - invalid syntax
dep-unparseable.py 2 Q39 REFUSAL — repair 3 (the dependency's SyntaxError raises with a traceback)
collector-undecodable.py 2 Q47 REFUSAL: … cannot be read the way CPython reads a source file
broken_dep.py 2 Q35 REFUSAL: … carries no destination rule
collector-latin1.py 0 175 / 0, ALL GATES PASS
collector-bom.py 0 175 / 0, ALL GATES PASS

6. Live state, delta 0 across the whole run

27 invocations under census (5 green + 5 matrix + 11 regressions + the guard's own reads). Session start = final, on (bytes, sha256, mtime_ns):

  • snapshot /root/.helm/data/snapshot.json — 14,039 B / aa842b0fd88037f8… / mtime_ns 1791479579475780303
  • held pin /root/.helm/data/held/stackchain-home.html.sha256 — 120 B / 5b6d70716153c3f4… / mtime_ns 1791478687099553071

ctime_ns moved on both objects (to 1791890492529766997 / 1791890499523343963) — the two in-gate detector restores' traces plus the guard restore of the base row. A restore cannot undo ctime (Q43); the traces are the durable record and are disclosed, not hidden. Deployed bin untouched: /root/.helm/bin/ — ee0b36a82f23fcf3… (helm-collect.py), 7ed1bca8f9da624f… (helm-selftest.py), 12570786ba703787…, 7386999867789694…; grep -c HELM_COLLECTOR on the deployed gate = 0, so Q40's finding stands and none of Q31–Q50 is in production. Scratch tree /root/crucible/q50/ is not committed; everything re-runs from patch_gate_q50.py, run_green_q50.py, run_matrix_q50.py, run_regressions_q50.py, figures_q50.py.

7. Findings and limits

  1. The unset shape is covered twice. Dis-unset (prevention cut) still holds delta 0: the probe copy's own Q45 literal relocation covers the default literal, so the Q50 redirect is load-bearing only in the named cell. Both arms measured — the prevention is not redundant, it is the only cover for the shape that actually moved the pin.
  2. The detector is per-window, not per-run. This run's once-per-run held census (_q44_held_before, q49:1281 → q50:1388) is taken after the spawn and structurally cannot see a probe write — the ordering defect that made the class invisible. The permanent fix is to hoist that census above the probe, or to keep the window census as the instrument. Named, not fixed: out of Q50's one-line scope, and the next Helm-side follow-on.
  3. Citation shift from the hoist. The probe spawn is q49:719 → q50:917; the in-process --deploy branch is q49:2292–2325 → q50:2399–2432 (_rc = hcg.main() at q50:2432; the # --dry-run at the live destination is NOT gated comment at q50:2390, q49:2283); the legs' env hand-off q49:1117 → q50:1310. Reply 34's corrected citations describe the same regions.
  4. What is not covered (carried from Q48/Q49's limits, unchanged): a variable name the ast inventory cannot see (dynamically constructed), an argv-composed root passed to the probe (the probe passes no argv), and a subject that reaches the live path through a mechanism neither environment nor literals express. The parse, read-path and redirect classes are untouched: 11/11 regressions hold.
  5. Harness defects of mine, logged first. (a) The green runner's first attempt used the deployed collector /root/.helm/bin/helm-collect.py (ee0b36a8… — the pre-Q33 collector, Q40's finding) and was refused by Q35's source precondition; re-run with the Q33–Q48 subject (4e45021d…). (b) The matrix/regression extractors matched lines starting with Q50 while the foot prints PASS Q50 …; the Q50 check lines were re-derived from the saved logs by figures_q50.py — no invocation re-run. (c) The disarmed variant still prints the Q50 NOTE although the environment it names is not the one the spawn receives — a scratch discriminator's known inaccuracy, disclosed.
  6. Shared live objects, re-measured. The queue's standing note (the live objects and scratch trees need a lock or separate roots) stands: this session's ctime traces are this run's own restores, and no foreign writer appeared in this tree's window. The fixture's default live root means any unguarded invocation of env-root-v1.py writes live; every row here either set Q48_ROOT explicitly or ran under the guard.

8. Verdict

Q50 DONE. The class the pipe attributed to the probe spawn is closed for both shapes: prevention (the spawn's rebuilt environment) measured delta 0 in the blank cell and in every other armed configuration, the detector measured firing and restoring in the one configuration where prevention is cut, and the 2×2 now isolates the two mechanisms row by row. The adoption pair the pipe made conditional on this item is now collector 4e45021d… + gate a951dbe8….

9. Evidence

claim command / artifact output
base gate identity sha256sum /root/crucible/q49/helm-selftest-q49.py 085ddf4f42ae80ea… (matches the queue)
new gate identity sha256sum helm-selftest-q50.py; wc -l a951dbe87d3394e0…, 134,086 B / 2,552 lines
diff decomposition diff -u q49 q50 | grep -c 388 = 5 hunks + 226 − 119; 2,445+226−119 = 2,552
patch integrity python3 patch_gate_q50.py 6 edits, each anchor count 1; py_compile rc 0 ×2; 2 × check("Q50"
green python3 run_green_q50.py 5 × rc 0, 175/0, 13,929 B; VERDICT: ALL GREEN
matrix python3 run_matrix_q50.py 5 rows; base MOVED 120→14 B restored; armed cells delta 0; dis-named Q50 FAIL, in-gate restore
regressions python3 run_regressions_q50.py 11/11 rc as expected, delta 0; VERDICT: ALL REGRESSIONS HOLD
live state python3 q50_guard.py snapshot / compare / restore session start = final on bytes/sha/mtime; FINAL delta {}
deployed bin sha256sum /root/.helm/bin/*; grep -c HELM_COLLECTOR helm-selftest.py ee0b36a8… / 7ed1bca8… / 12570786… / 73869998…; count 0
artifacts out/Q50-{GREEN,MATRIX,REGRESSIONS,FIGURES}.json, out/DIFF-vs-q49.txt, per-row .out/.err on disk, re-runnable