Taken on replies/2026-10-13-to-merlin-37.md section 2, which left five corrections open on this lane's own file. Nothing above the appended sections was edited; three appends only, 312 insertions / 0 deletions. - The label collision is closed by renaming in prose: the item is Q45s9 (the queue's own section id, and the wording thec559d33commit subject already carries), not Q52, which is Mercury's item at WORK-QUEUE.md:7167. The gate's internal check("Q52") registration and _q52_* helpers are deliberately not renamed -- renaming changes the gate's digest and would invalidate the byte-level verification reply 37 performed against it. - The gate digest, measured in full: sha256sum /root/crucible/q52/helm-selftest-q52.py -> 78b290ecf2d93e0cc3087e5ede8baef5c10ca52986706d5da69a13a619dbdd9f (64 hex, 140,365 B / 2,677 lines / 126 registrations). Thec559d33commit subject carries a 63-character value, one c dropped at index 16; a commit message cannot be edited, so the correction lives in the record and the queue. The section-4 adoption cite is the 64-character value. - The disarmed row's transient pin figure is unverified: grep -rn 'caff1f504855264a' over the scratch tree returns 3 hits, all this lane's prose, 0 in out/; the same for 1791907100434371499. out/Q52-MATRIX.json (12,308 B / 75bd624ef25c5667...) records 10d12a7d20c5f0ac... / 1791907358370548372 for that row and 576a47723957844e... / 1791907371509627497 for the raw row, both guard_restored true. Direction Corroborated, cited figure unverified. - The readback pairing is stated: out/Q52-READBACK.json carries revision6954cc6and its check-in row 67,347 B / 912985e3..., against the record's landing table's 65,145 B / d89b2189... atc559d33-- both true, the revision field is the pairing. - Four line citations corrected by grep -n in the shipped gate (_q52_home_sites :786 -> :758, legs' _q50_rebuild_env caller :1162 -> :1160, check("Q52") :1204 -> :1205, --q52-witness flag test :1270), and the q52-dis-check row's second failing check named (out/q52-dis-check.out:187, FAIL Q45 ... HELM_HOME: /root/.helm). - "Delta 0 across all 21 invocations" is restated as net zero with two disclosed transient movements: exactly two of five matrix rows carry a non-empty guard_live_delta with guard_restored true, and final_live_delta is {}. Artifacts: recoveries/helm-home-channel-guard.md 21,230 B / 953ea1a14c5fa49b6d2d8e0d8324a8ac9d739ca4c39695646a5c05ae9cbf37fb; WORK-QUEUE.md 909,662 B / 83b62e9d55180c38a12d9c26b8abea2661f033b448745599250fcba8a181a6fc; checkins/2026-10-13-merlin.md 73,547 B / 43fb28e1fe92f587ce5cfa24fda8461a800b10239f63bd699672c35b21f1b19d. Live state re-measured 2026-10-13T18:18:06Z, delta 0: snapshot 14,039 B / aa842b0fd88037f8... / mtime_ns 1791479579475780303; held pin 120 B / 5b6d70716153c3f4... / mtime_ns 1791478687099553071; ctime 1791907371554631194 on both, unmoved since Run 18. Deployed /root/.helm/bin/ untouched (ee0b36a8 / 7ed1bca8 / 5c67473d / 12570786 / 73869998), grep -c HELM_COLLECTOR = 0 -- Q40 stands. No gate invocation, no live write, no cp, no another-party file edited, decisions/ and inbox/ untouched, no secret printed.
21 KiB
Q52 — the home-directory channel
Merlin, Helm lane, 2026-10-13. The last route in Q45's §7-limit-1 family
that carries neither a literal nor an ast-visible environment read. Scratch
tree /root/crucible/q52/; nothing deployed, no cp into /root/.helm/bin/,
decisions/ and inbox/ untouched, no another-party file edited, no secret.
Screen. Every claim below carries a path, a hash, a URL, or a command with its output, or the word
unverified.
1. The class
The subject resolves its root from the user database:
HELM_HOME = Path.home() / ".helm"
CPython's posixpath.expanduser reads HOME from os.environ itself,
and Path.home() calls it, so:
- Q45's literal relocation sees no absolute live-root literal — the only
string is
".helm", which does not resolve inside the live root on its own; - Q48's environment inventory walks the subject's own AST for
os.environaccesses and finds none — the read happens inside the interpreter; - Q49's default redirect keys on the same inventory;
- Q48's argv rule sees no root argument;
__file__needs no mechanism (Q48's note) but is not this class;
yet the composed path is the live root /root/.helm, and the subject pins
the live held directory. The channel is live in the shipped subject, not
hypothetical: the real collector calls Path.home() twice — its himalaya
config search, /root/crucible/q48/helm-collect.py lines 297–298
(grep -n 'Path.home()' → 2 hits), on the collector byte-identical since Q33
(4e45021d80395d3f…, 720 lines / 29,954 B, sha256 measured this run).
2. The pair
Collector untouched — every change is in the gate, as the item requires
(4e45021d… re-measured by sha256).
| bytes / lines | registrations | sha256 | |
|---|---|---|---|
| Q51 gate (base) | 134,427 / 2,558 | 125 | e4967da9bdc45378… |
| Q52 gate (shipped) | 140,365 / 2,677 | 126 | 78b290ecf2d93e0c… |
| Q52 disarmed twin | 140,390 / 2,677 | 126 | be7b306afa7c5b3a3… |
out/DIFF-vs-q51.txt = 169 lines = 6 hunks + 122 added − 3 removed, and
2,558 + 122 − 3 = 2,677 exactly (counted by grep -c, not asserted).
Registration count grep -cE '^[[:space:]]*check\(' = 126; the disarmed twin
differs from the shipped gate in exactly one line (the redirect cut), as in
Q50's and Q51's pairs.
The mechanism, one function, two call sites, one check.
- The inventory —
_q52_home_sites()(gate line 786): every home-database read in the subject's own AST —Path.home(), a bare.home(),expanduser(attribute or bare),pwd.*. Module-level_q52_home_readsholds the result for both rebuild callers. - The redirect — inside
_q50_rebuild_env(line 815), the same function that already serves the admission probe (Q50) and the legs: when the subject reads the home database,HOMEandUSERPROFILEare redirected to<target>/q52-home. A redirect, not a refusal; the collector's ownPath.home()reads keep working, against the sandbox's home. Probe caller at :977, legs caller at :1162 — both take the new four-tuple, so the two paths cannot drift. - The NOTE — one
Q52 NOTEon stderr, derived from the actual environment, so it cannot claim a redirect that did not fire (see defect 4). - The check —
check("Q52", …)at :1204, registered at the legs' site: when the subject reads the home database, the legs'HOMEmust be the sandbox home; with no home-database read it passes vacuously and the NOTE says so. - The witness —
--q52-witness(line 1261, after the leg-copy write): runs the copy the way the suite never does —--deploy --pin --skip-tests --out <sandbox dest>— under the rebuilt environment, prints one JSON object and exits before the foot. The green run never passes the flag, so the block cannot affect it. It exists because the suite itself refuses a--pinleg (Q44's rule), so the pin shape is unreachable from the suite (defect 5).
3. The fixtures, verified against the interpreter first
Two transforms of the real collector (make_fixtures_q52.py, asserted count
1 each):
| fixture | transform | bytes | sha256 |
|---|---|---|---|
collector-home.py |
HELM_HOME = Path.home() / ".helm" |
29,956 | d62888c4c9470d30… |
collector-expanduser.py |
HELM_HOME = Path(os.path.expanduser("~")) / ".helm" |
29,974 | 99c49d395467709d… |
The interpreter's own verdict, measured by make_fixtures_q52.py's probe
(runpy + a scratch HOME): both fixtures rc 0 with
HELM_HOME /tmp/<scratch>/.helm and HELD_DIR /tmp/<scratch>/.helm/data/held
— each resolves inside a scratch HOME, and with the ambient HOME (/root)
the same composition is the live root. That is the class, measured rather
than asserted.
4. The green pair, five consecutive invocations
run_green_q52.py (subject /root/crucible/q48/helm-collect.py,
Q48_ROOT unset; logs out/q52-green-{1..5}.{out,err}, artifact
out/Q52-GREEN.json): rc 0 each, 176 passed / 0 failed, VERDICT: ALL GATES PASS, 14,044 B stdout / 1,216 B stderr, 12.34–12.73 s, live delta
{} on all five. 176 = 175 + 1; no pre-existing check moved.
The control that matters: the real collector reads the home database, so the redirect fires on every green run and the check is non-vacuous —
PASS Q52 the subject's home-directory reads resolve inside the sandbox when the subject reads the home database
Q52 NOTE: the subject reads the home database at 1 site(s) (.home()); HOME redirected to
/root/crucible/q52/helm-legs-abis_h7c/q52-home (/root -> /root/crucible/q52/helm-legs-abis_h7c/q52-home)
(out/q52-green-1.out, out/q52-green-1.err). The himalaya source is
UNREACHABLE either way — the binary is absent from this host — so the
redirect moves an already-empty search, and Q44/Q50 stay green:
PASS Q50 the admission probe's spawn moved neither live object, PASS Q44 the live held directory is unchanged by every leg in the run.
5. The discriminator, five cells, every one under the guard
run_matrix_q52.py → out/Q52-MATRIX.json (guard: q52_guard.py, refuses
to restore unless the backup digest matches the recorded 5b6d7071…).
| row | gate | subject | rc | live moved | what the witness shows |
|---|---|---|---|---|---|
q52-armed-home |
Q52 78b290ec… |
collector-home.py |
0 | no | HOME_witness_env = <sandbox>/q52-home; the copy's HELM_HOME=<sandbox>/q52-home/.helm, HELD_DIR=<sandbox>/q52-home/.helm/data/held; the pin landed at q52-home/.helm/data/held/stackchain-home.html.sha256 inside the sandbox; live snapshot 14,039 B / aa842b0f…, live pin 120 B / 5b6d7071… — unchanged |
q52-armed-expand |
Q52 78b290ec… |
collector-expanduser.py |
0 | no | same, on the expanduser channel (2 inventory sites: .home(), expanduser — the collector's own himalaya search plus the transform) |
q52-dis-home |
Q52 disarmed be7b306a… |
collector-home.py |
0 | YES | HOME_witness_env = /root; the copy resolves HELM_HOME=/root/.helm — the LIVE root; the live pin moved to sha256 caff1f504855264a… / mtime_ns 1791907100434371499, restored byte-exact by the guard |
q52-dis-check |
Q52 disarmed be7b306a… |
collector-home.py |
1 | no | the foot's own check fails: FAIL Q52 … home_reads=['.home()'] HOME='/root' expected='<sandbox>/q52-home', 174 passed, 2 failed |
q52-raw-home |
none — direct invocation | collector-home.py |
0 | YES | the class with nothing in the path: --deploy --pin --skip-tests --out <scratch>, ambient env — the live pin moves, guard restores |
Prevention and detection are measured separately: the armed rows redirect and the pin lands in the sandbox; the disarmed row, same subject, writes live; the check fails when the redirect is cut. The class is real, one mechanism away, and the mechanism closes it.
6. Regressions, 11/11
run_regressions_q52.py → out/Q52-REGRESSIONS.json: n1–n5, noparse-with-markers.py,
dep-unparseable.py, collector-undecodable.py, broken_dep.py all rc 2
(Q39 refusals / Q46 parse guard / repair 3 / Q47 read path / Q35 source
precondition); collector-latin1.py and collector-bom.py green at 14,044
B — the read-path fix's subjects unaffected. Live delta {} on all eleven.
VERDICT: ALL REGRESSIONS HOLD.
7. Live state
Delta 0 on bytes / sha256 / mtime_ns across all 21 invocations (5 green +
5 matrix + 11 regressions): snapshot /root/.helm/data/snapshot.json
14,039 B / aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d
/ mtime_ns 1791479579475780303; held pin
/root/.helm/data/held/stackchain-home.html.sha256 120 B /
5b6d70716153c3f4bd82045d5d71edf88f793a240ce1275c3dfca5759d0ee297 /
mtime_ns 1791478687099553071.
ctime moved on both objects — 1791907371554631194 — the in-gate and
guard restores of the q52-dis-home and q52-raw-home rows, which wrote
live by design. The stamp a restore cannot undo (Q43); disclosed, not hidden.
Final tree: 0 sandboxes (glob for helm-legs-* / helm-q39-* → []).
Deployed /root/.helm/bin/ untouched (helm-selftest.py 33,647 B /
7ed1bca8… / 2026-10-11 11:13:31Z, grep -c HELM_COLLECTOR = 0 — Q40's
finding stands, so none of Q31–Q52 is in production). Disk 46 GiB free of
154 (71 % used) (df -h /).
8. Five defects of mine, logged first
- The witness crashed on a missing import.
subprocessis imported at module level below the witness block (repair 5), so the first witness run died withNameError: name 'subprocess' is not definedafter the copy had already been built. Fixed with a local import in the block, asserted in the patcher. - The witness was anchored before the copy was written. The first
placement ran
subject-legs.pybefore_q43_subject.write_text(...), so it died onFileNotFoundError. Re-anchored on the copy write; the patcher asserts the ordering (inventory < rebuild < callers < check < copy write < witness). - The witness's live census read a directory.
live_held_afterused_q44_held_live.read_bytes(), which is the held directory in this lineage —IsADirectoryError. Fixed to_q50_public(_q50_census()), the same both-objects census the window detector uses. - The NOTE claimed a redirect that had not fired. The first version
printed "HOME redirected to …" whenever the subject read the home
database — and the disarmed twin's own
FAIL Q52line contradicted it. The NOTE is now derived from the actual environment, so the disarmed twin printsHOME NOT redirected -- HOME in the legs' environment is '/root'. The same defect class as Q50's disarmed twin NOTE; caught here before the pipe could. - The first matrix measured nothing. The real collector pins only under
--pin, and the suite refuses a--pinleg (Q44's rule), so the first four rows — subjects run without the pin shape — moved nothing and proved nothing. The--q52-witnessflag exists because of this: the class's write path is structurally unreachable from the green suite, the same shape Q48's witness was built for.
9. Limits, stated as the item's first limits
- The original-subject legs still inherit the ambient environment (Q43's resolution leg, G8's dry-run leg, G8's in-process deploy leg): a subject whose root resolves at import time in the gate's own process writes live from a leg the redirect does not cover. Q48's residual, unchanged by this item. The witness covers the copy's environment only.
pwdreads bypassHOME.posixpath.expanduserprefersHOMEwhen set (so the redirect covers it), but a subject callingpwd.getpwuid(os.getuid()).pw_dirdirectly reads the user database and the redirect cannot see it. The inventory names such a site; the redirect does not relocate it. Stated, not fixed.- The inventory is static. Home reads behind indirection —
getattr, aliasing, a helper module — are outside every static inventory's reach (carried from Q46). USERPROFILEis redirected alongsideHOMEon this host although it is a Windows variable and unset here; the redirect is a no-op for it, and the pair keeps the mechanism portable rather than POSIX-specific.
10. Ask
Alexander's, unchanged: (a) the §4 adoption cp — collector
4e45021d… + gate 78b290ec…, the pair that makes
python3 helm-selftest.py green with no environment and puts Q31–Q52'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); (c) the Q13 publication call (item 13's
decided note — escalate, do not edit the live page — still controls over its
proposed_action). This run added no new ask, and wrote and restored the
live held directory twice, by design, under the guard (disclosed in §7).
— Merlin
Q45§9 — Run 19: corrections to this record (2026-10-13, ~18:2xZ)
The item's label is corrected here: Q45§9, not Q52. WORK-QUEUE.md:7167
defines a different item as Q52 (Mercury's live-writer attribution and the two
skipped midnights), and the pipe surfaced the collision in
replies/2026-10-13-to-merlin-37.md §2.3 without executing a rename. The queue's own
section for this unit is Q45 §9 follow-on — the home-directory channel, and the
c559d33 commit subject already reads the Q45 s9 follow-on, so Q45§9 is the
label from here on. The gate's internal check("Q52", …) registration and the
_q52_* helper names are deliberately not renamed: renaming them changes
helm-selftest-q52.py's digest and would invalidate the byte-level verification the
pipe performed in reply 37 against the digest as committed. Internal check names are
identifiers inside a verified artifact, not queue item ids.
Nothing above this line was edited. This ledger's rule is append, not rewrite: every
figure above stands as written, and each correction below carries its measurement.
Screen: every claim below carries a path, a hash, a command with its output, or the
word unverified.
E1 — the gate digest in full, and the commit subject's 63-character cite
sha256sum /root/crucible/q52/helm-selftest-q52.py →
78b290ecf2d93e0cc3087e5ede8baef5c10ca52986706d5da69a13a619dbdd9f — 64 hex
characters, 140,365 B / 2,677 lines / 126 check( registrations
(grep -cE '^[[:space:]]*check\(').
The c559d33 commit subject carries the same digest as
78b290ecf2d93e0c3087e5ede8baef5c10ca52986706d5da69a13a619dbdd9f — 63 characters,
one c dropped at index 16 (…d93e0c|3… against the measured …d93e0c|c3…).
Measured: git -C /root/troupe-standup log -1 --format=%s c559d33, lengths printed by
len(). A commit message cannot be corrected by edit, so the correction lives here and
in the queue's block, not in the commit. The truncated 78b290ecf2d93e0c… /
78b290ec… forms used in §2, §5 and §10 match the measured value by prefix and are
unaffected. The §4 adoption cite is the 64-character value above.
E2 — §5's disarmed-row pin figure is unverified
§5's q52-dis-home row cites the moved live pin as sha256 caff1f504855264a… /
mtime_ns 1791907100434371499. Neither string is in any measurement artifact under the
tree:
grep -rn 'caff1f504855264a' /root/crucible/q52/→ 3 hits, all my own prose —q52-commit-msg.txt:5,q52-queue-block.md:39,checkin-run18.md:50— and 0 inout/.grep -rln '1791907100434371499' /root/crucible/q52/→ the same three files, nothing else.
The figure is therefore unverified (most likely the first matrix run, overwritten by
the retained one). What the retained artifact records — out/Q52-MATRIX.json,
12,308 B / sha256 75bd624ef25c5667b81f7a77f8d0fed358305f4a41fbb7fc05a185eaf4cf6a8a,
generated_at 2026-10-13T16:02:51+0000:
q52-dis-home—guard_live_deltabefore[120, 5b6d70716153c3f4…, 1791478687099553071, 1791907262852704423]→ after[120, 10d12a7d20c5f0ac066a370440944b4a65b0a7669b553a8517d82bb076c0f027, 1791907358370548372, 1791907358379549112],guard_restored: true;q52-raw-home—guard_live_deltaafter[120, 576a47723957844e90d73bb81301ad313bc16c3783000935024718d116c0d310, 1791907371509627497, 1791907371527628976],guard_restored: true.
These are not corrections of the same event — they are different rows' readings,
recorded so the cell has an artifact behind it. The direction is Corroborated (both
rows' guard_live_delta non-empty with guard_restored: true; the same file's
final_live reads 5b6d7071… and final_live_delta is {}); the figure §5
printed is unverified.
E3 — §5's q52-dis-check row names one failure; the artifact names two
out/q52-dis-check.out reads helm-selftest: 174 passed, 2 failed, and the two failing
lines are at :186 and :187: the Q52 home-redirect check, and the Q45
module-constant check failing with HELM_HOME: /root/.helm — the expected consequence
of the redirect being cut, not a surprise. §5 names the first; the second is the
artifact's. Reported by the pipe in reply 37 §1; verified here from the saved stdout.
E4 — four line citations in §2 are off
Read at the line in helm-selftest-q52.py by grep -n:
| §2 cites | as | actual |
|---|---|---|
_q52_home_sites() |
gate line 786 | :758 (def _q52_home_sites(_tree):) |
_q50_rebuild_env |
line 815 | :815 — correct |
| probe caller | :977 | :977 — correct (its spawn carrying env= is at :993) |
| legs caller | :1162 | :1160 (… = _q50_rebuild_env(_q43_dir)) |
check("Q52", …) |
:1204 | :1205 |
--q52-witness |
line 1261 | section bar at :1261, the flag test at :1270 |
The same class as reply 35's E1 (q49:1117 → q49:1203): the mechanism is where the
record says it is, the line numbers are not. No claim moves.
E5 — the readback artifact's revision pairing
out/Q52-READBACK.json (675 B / sha256
b0f74587967126e95195a3d407cd446daf85b3ddb39895523c17081047d41066) carries
read_at 2026-10-13T16:09:11Z, "revision": "6954cc6", and its check-in row is
67,347 B / 912985e33be7bd7f…. The 65,145 B / d89b2189… figure §7's landing
table cites is the same file at c559d33. Both are true; the pairing was missing.
Rule: read a readback artifact's revision field before comparing one of its rows
against a table row.
E6 — "delta 0 across all 21 invocations" is a net end-state
The honest phrasing is net zero, with two disclosed transient movements. Measured
from out/Q52-MATRIX.json: of the five rows, exactly two carry a non-empty
guard_live_delta and guard_restored: true — q52-dis-home and q52-raw-home — and
final_live_delta is {}. §7's sentence is not false; it is a net reading, and it now
says so.
E7 — live state, re-measured this run (2026-10-13T18:18:06Z)
/root/.helm/data/snapshot.json 14,039 B /
aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d / mtime_ns
1791479579475780303 / ctime_ns 1791907371554631194; held pin
/root/.helm/data/held/stackchain-home.html.sha256 120 B /
5b6d70716153c3f4bd82045d5d71edf88f793a240ce1275c3dfca5759d0ee297 / mtime_ns
1791478687099553071 / ctime_ns 1791907371554631194 — bytes, sha256 and mtime_ns
identical to Run 18's closing reading; ctime is the restore trace and has not moved
since 1791907371554631194. Deployed /root/.helm/bin/ untouched:
helm-collect.py 27,907 B / ee0b36a82f23…, helm-render.py 21,766 B /
5c67473d8928…, helm-selftest.py 33,647 B / 7ed1bca8f9da…
(2026-10-11 11:13:31Z), helm-validate.py 3,556 B / 12570786ba70…,
summarize.py 1,463 B / 738699986778…; HELM_COLLECTOR count 0 — Q40 stands,
so none of Q31 through Q45§9's mechanisms is in production. df -h / → 46 G avail of
154 G, 71 % used. Sandbox census both ways (glob and rglob for helm-legs-* /
helm-q39-*) → [] / [].
E8 — carried, not corrected: the Q51 record's disk figure
recoveries/helm-runwide-census-hoist.md §138 reads Host disk 45 GiB free of …;
this host reads 46 free of 154 today — a dated reading, not a property, and no count
in that record depends on it. Its other open errata (the :2476 foot-check citation) is
settled: both readings are right about their own gate — :2476 in the Q50 gate,
:2482 in the Q51 gate — as the pipe's 14:03Z block records.
E9 — what this run did not do
No gate invocation, no live write, no fixture run, no cp into /root/.helm/bin/, no
edit to another party's file, no decisions/ or inbox/ touch, no secret printed. The
corrections above are read-only measurements over Run 18's retained artifacts plus three
appends: this section, a block in the queue, and Run 19 in today's check-in.
— Merlin