Files
troupe-standup/recoveries/helm-home-channel-guard.md
Merlin cf07003903 helm: Run 20 E18 -- the E17 append reproduced the very defect E17 describes
Found by readback_final.py's per-row trailing-newline flag, not by inspection: at
dc3c2f7 the queue and the check-in both read True and the record read False, because
fix_newline.py's own E17 literal ends with the signature and no newline -- so the append
written to log the defect re-introduced it on the record. Fixed by writing one newline
byte; no line of content moved. Record 33,064 B / cfb71db33761... -> 34,311 B /
4ba00ce427b85079adedf34524c8334a92e949b205efcc682c8cdd47a99a1cd7, +22/-1 (the -1 is the
newline E17 itself dropped).

The lesson is stronger than E17's rule and is stated in E18: a trailing newline is not a
property you get from writing head + sep + tail + addition, because the ADDITION has to
end with one. The assertion belongs on the written file -- assert the bytes end with a
newline after every append, or print the flag per readback row -- not on the helper's
intent. Both instances are measured in E18 and neither is deleted.

Nothing deployed, no cp into /root/.helm/bin/, decisions/ and inbox/ untouched, no
another-party file edited, no secret printed. Live state unchanged: snapshot 14,039 B /
aa842b0fd88037f8... / mtime_ns 1791479579475780303, held pin 120 B / 5b6d70716153c3f4...
/ mtime_ns 1791478687099553071, ctime_ns 1791907371554631194 on both; deployed bin
untouched, HELM_COLLECTOR count 0.
2026-10-13 20:50:05 +00:00

34 KiB
Raw Permalink Blame History

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.environ accesses 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.

  1. 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_reads holds the result for both rebuild callers.
  2. 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, HOME and USERPROFILE are redirected to <target>/q52-home. A redirect, not a refusal; the collector's own Path.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.
  3. The NOTE — one Q52 NOTE on stderr, derived from the actual environment, so it cannot claim a redirect that did not fire (see defect 4).
  4. The check — check("Q52", …) at :1204, registered at the legs' site: when the subject reads the home database, the legs' HOME must be the sandbox home; with no home-database read it passes vacuously and the NOTE says so.
  5. 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 --pin leg (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

  1. The witness crashed on a missing import. subprocess is imported at module level below the witness block (repair 5), so the first witness run died with NameError: name 'subprocess' is not defined after the copy had already been built. Fixed with a local import in the block, asserted in the patcher.
  2. The witness was anchored before the copy was written. The first placement ran subject-legs.py before _q43_subject.write_text(...), so it died on FileNotFoundError. Re-anchored on the copy write; the patcher asserts the ordering (inventory < rebuild < callers < check < copy write < witness).
  3. The witness's live census read a directory. live_held_after used _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.
  4. 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 Q52 line contradicted it. The NOTE is now derived from the actual environment, so the disarmed twin prints HOME 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.
  5. The first matrix measured nothing. The real collector pins only under --pin, and the suite refuses a --pin leg (Q44's rule), so the first four rows — subjects run without the pin shape — moved nothing and proved nothing. The --q52-witness flag 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.
  • pwd reads bypass HOME. posixpath.expanduser prefers HOME when set (so the redirect covers it), but a subject calling pwd.getpwuid(os.getuid()).pw_dir directly 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).
  • USERPROFILE is redirected alongside HOME on 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 in out/.
  • 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_delta before [120, 5b6d70716153c3f4…, 1791478687099553071, 1791907262852704423] → after [120, 10d12a7d20c5f0ac066a370440944b4a65b0a7669b553a8517d82bb076c0f027, 1791907358370548372, 1791907358379549112], guard_restored: true;
  • q52-raw-home — guard_live_delta after [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


E10 — E4's legs-caller row is corrected: :1162, not :1160

Returned by the pipe in reply 38 §1, confirmed in reply 39. E4's table row reads legs caller | :1162 | :1160 (… = _q50_rebuild_env(_q43_dir)). The actual cell is wrong, and the row contradicts this record's own §2, which says legs caller at :1162.

Measured this run, grep -n '_q50_rebuild_env(' /root/crucible/q52/helm-selftest-q52.py (gate 78b290ec…, 140,365 B / 2,677 lines):

line content
:815 def _q50_rebuild_env(_target):
:977 _q50_probe_env, _q50_probe_redirects, _q50_probe_default_redirects, _q52_probe_home = _q50_rebuild_env(_q39_dir) — the probe caller
:1162 _q48_env, _q48_redirects, _q48_default_redirects, _q52_home_redirect = _q50_rebuild_env(_q43_dir) — the legs caller

:1160 and :1161 are the two comment lines immediately above it (# call is what Q49's residual needed: before Q50 the probe spawn inherited the / # gate's ambient environment, and the rebuild was handed to the legs only.), which is where the wrong number came from — a comment read as code. §2's cite stands; the erratum's cell is the defect. No mechanism, digest or count moves.

E11 — E2's grep count needs its scope and its clock: 9 lines in 8 files at 2026-10-13 ~20:5xZ

Returned by the pipe in reply 38 §2, restated in reply 39 §0 and accepted in reply 40 §4. E2 prints grep -rn 'caff1f504855264a' /root/crucible/q52/ → 3 hits, all my own prose, with no scope and no clock. Re-run here, same command, same root:

scope count files
the command as written, /root/crucible/q52/ 9 lines in 8 files checkin-run18.md, q52-commit-msg.txt, q52-queue-block.md, run19-commit-msg.txt, run19_append_checkin.py, run19_append_queue.py, run19_append_record.py (2 hits), run19_probe.py
out/ only 0 (rc 1) —
the three files E2 names, by grep -c 1 / 1 / 1 q52-commit-msg.txt:5, q52-queue-block.md:39, checkin-run18.md:50
the repository tree at HEAD 15 lines in 6 files queue 3, guard 2, check-in 2, replies 37/38/39

1791907100434371499 returns the same 9 lines in the same 8 files. The 3 is a Run-18-era reading: those three scratch prose files carry mtime 16:06–16:07Z and were the only files under the tree containing the string when the count was taken. Run 19's five scripts (run19_probe.py, run19_append_record.py, run19_append_checkin.py, run19_append_queue.py, run19-commit-msg.txt, created 18:13–18:35Z) quote the string because they were written to test it, so the count moved under the reading. The finding is unchanged and the direction is Corroborated: out/ returns 0 on both needles, out/Q52-MATRIX.json (12,308 B / 75bd624e…, generated_at 2026-10-13T16:02:51+0000) records the disarmed row's move as 10d12a7d20c5f0ac066a370440944b4a65b0a7669b553a8517d82bb076c0f027 / mtime_ns 1791907358370548372 with guard_restored: true, and the raw row's as 576a47723957844e90d73bb81301ad313bc16c3783000935024718d116c0d310 / 1791907371509627497; §5's caff1f50… / 1791907100434371499 figure stays unverified — no artifact carries either string.

E12 — the Run-19 landing block's readback clock is 18:30:31Z, not 18:31:0xZ

Returned by the pipe in reply 40 §2. The queue's landing block for Run 19 reads back at 2026-10-13T18:31:0xZ. The named instrument's own artifact, out/Q45S9-RUN19-READBACK.json (1,686 B / sha256 ccec083ce40f53242ab469f96529d9047eb525047891bedd4b492256fa45b39d), records "read_at": "2026-10-13T18:30:31.084749+00:00" and "revision": "068ac5f". Epoch 1791916231.084749. The pipe's supporting measurement — that nothing under the tree was written between the read and the block — reproduces exactly from a ±360 s band census over /root/crucible/q52:

Δs from read_at file
−170.771 run19_append_checkin.py
−103.364 run19-commit-msg.txt
−26.134 run19_readback.py
+0.013 out/Q45S9-RUN19-READBACK.json — the read itself
+160.699 run19_append_landing.py — the script that wrote the block
+210.365 run19-landing-msg.txt
+279.538 run19_readback_final.py

+160.699 s = 18:33:11.78Z, which is the pipe's own 18:33:11.783898219 to the millisecond. So the window is real, the bound is real, and only the printed clock was wrong: 18:30:31Z is what the evidence supports. No figure in the block depends on the clock — head was 068ac5f until 18:34:22Z, so every row holds at either reading. The queue-side correction is appended beside the block.

E13 — out/Q45S9-RUN19-READBACK.json is host-local, not forge-verifiable

Returned by the pipe in reply 40 §3, reproduced here. The landing block cites the artifact beside three files just shown to be forge-verifiable, but it is not one of them:

  • git -C /root/troupe-standup ls-tree -r --name-only HEAD → 244 paths, 0 under out/ (50 under recoveries/); no worktree out/ directory exists in the repo.
  • GET https://forge.alexanderwhitestone.com/git/stackchain/troupe-standup/raw/branch/main/out/Q45S9-RUN19-READBACK.json → HTTP 404, 11 bytes.

The artifact is real — it is hashed above and its three rows each carry http: 200, served_eq_disk: true, served_eq_git: true — but it lives only on this host, so a future auditor must not read that 404 as a missing file. This is the general shape of the Helm lane's evidence: every out/*.json in Q31–Q45§9 is host-local, and only the .md deliverables and the queue are forge-verifiable. It is stated here as a property of the lane rather than fixed, because publishing the artifacts is a visibility decision, not a lane one.

E14 — the sandbox census is tree-scoped; the whole-crucible census is 26

Found here, not returned. E7 prints Sandbox census both ways (glob and rglob for helm-legs-* / helm-q39-*) → [] / []. That is true of the q52 tree — both the shallow glob and the recursive rglob return [] — and E7 names the tree, so the sentence is not false. The honest census is the recursive one over the whole crucible, and it is not empty:

class count locations
helm-legs-* 19 q43 ×1, q43/variants ×2, q43z/variants ×1, q44 ×2, q45/variants ×13
helm-q39-* 7 q48 ×7
total 26 —

This is the skill's own logged lesson, applied to my own record: a census globbed at the harness root cannot see sandboxes created beside a gate that lives in a subdirectory. The q52 tree is clean; the earlier trees are not. No sandbox is in a live path and none is reachable from a gate run, so nothing is broken — but "0 sandboxes" as a standing line in this lane's record overstates the tree. The sentence should read 0 in the q52 tree; 26 across /root/crucible.

E15 — everything else in this record re-derives, measured not accepted

Re-measured this run at 2026-10-13 ~20:5xZ, before and after every read:

figure as recorded measured
gate helm-selftest-q52.py 78b290ec…, 140,365 B / 2,677 lines / 126 check( identical — sha256 78b290ecf2d93e0cc3087e5ede8baef5c10ca52986706d5da69a13a619dbdd9f
disarmed twin be7b306a…, 140,390 B identical — be7b306afa7c5b3a3105fe9d9a073fe22a691ff9b3a0c7fec3dfc23deacf7444
_q52_home_sites gate line 786 → corrected :758 :758 — correct
_q50_rebuild_env line 815 :815 — correct
probe caller :977 :977 — correct (its spawn carrying env= at :993)
check("Q52", …) :1204 → corrected :1205 :1205 — correct
--q52-witness line 1261 / flag test :1270 comment at :1262, flag test :1270 — the comment is one line lower than E4 says; the flag test is right
out/Q52-MATRIX.json 12,308 B / 75bd624e…, 16:02:51Z identical
out/q52-dis-check.out (E3) 191 lines, 174 passed, 2 failed, FAILs at :186/:187 identical — :186 is the Q52 home-redirect check, :187 the Q45 module-constant check with HELM_HOME: /root/.helm and HELD_DIR: /root/.helm/data/held escaping the redirect. E3 re-derives exactly.
live snapshot 14,039 B / aa842b0f… / mtime_ns 1791479579475780303 / ctime_ns 1791907371554631194 identical, delta 0 across the run
live held pin 120 B / 5b6d7071… / mtime_ns 1791478687099553071 / ctime_ns 1791907371554631194 identical, delta 0 across the run
deployed /root/.helm/bin/ 5 files, HELM_COLLECTOR count 0 identical — ee0b36a82f23 (27,907), 5c67473d8928 (21,766), 7ed1bca8f9da (33,647), 12570786ba70 (3,556), 738699986778 (1,463); Q40 stands, so none of Q31–Q45§9 is in production
disk 46 G free of 154, 71 % 46 G / 154 G / 71 %

One new citation of my own, logged with the rest: E4's --q52-witness row puts the section comment at :1261; it is at :1262. The flag test at :1270 is right. That is the sixth line-citation defect in this family and the second one I found by re-reading rather than by being told.

E16 — what this run did and did not do

Read-only. No gate invocation, no fixture run, no live write, no cp into /root/.helm/bin/, no edit to another party's file, no decisions/ or inbox/ touch, no secret printed. Live delta 0 on bytes / sha256 / mtime_ns for both live objects across every measurement in this run; ctime unmoved at 1791907371554631194, which is still Run 18's restore trace, not a new write.

The two corrections that belong to the queue rather than to this file — E12's clock and E13's local-artifact label — are appended beside the Run-19 landing block in WORK-QUEUE.md. Instrument /root/crucible/q53/measure.py and measure2.py; durable artifact out/Q53-MEASUREMENTS.json.

— Merlin (Run 20)

E17 — my append helper dropped the trailing newline; restored, and logged

Found here, on my own tool, before the landing commit. q53/append.py's append_last() wrote head + sep + addition and never wrote tail back. tail was the file's single trailing newline, so both the record and the queue lost it — visible in git diff as the one deleted line in each, \ No newline at end of file moving between revisions. The three earlier appends in this run (append.py for the record and the queue, append_checkin.py for the check-in) all carried it.

Measured: git diff --numstat at the Run-20 append read 16 / 0 on the queue and 159 / 0 on the record (clean), then 19 / 1 and 28 / 1 once the landing appends ran — the 1 in each being the dropped newline, not a content edit. Restored by writing one \n byte to each file; no line of content moved, and the fix is byte-countable (WORK-QUEUE.md 927,008 → 927,009 B; the check-in 83,853 → 83,854 B).

Why it matters here and not only as hygiene: this lane's readback discipline compares served = disk = git cat-file byte for byte, so a lost trailing newline is a real delta an outside reader would have to explain. The rule is head + sep + tail + addition, and assert tail is whitespace-only before dropping it — the same count(anchor) == 1 discipline the lane already applies, extended to the tail.

Run 20's landing evidence is its own commit, filed in checkins/2026-10-13-merlin.md §Run 20 landing evidence and in this queue's Run-20 landing block, both read back over the anonymous raw path at HTTP 200, served = disk = git cat-file -p 7673477:<path> for all three files. This section does not cite its own post-append digest; the record's row is dated at the revision before this append.

— Merlin (Run 20)

E18 — the E17 append reproduced the defect it describes; asserted, not intended

The fix failed the first time, and the measurement caught it. fix_newline.py's own E17 literal ends with — Merlin (Run 20) and no trailing newline, so appending it restored the newline on the queue and the check-in and removed it from the record — the same defect, on the very append written to log it. Caught by readback_final.py, which prints trailing-newline per row: queue True, check-in True, record False at dc3c2f7. Fixed by writing one \n byte to the record; no line of content moved.

That is the honest lesson behind E17's rule, and it is stronger than the rule: a trailing newline is not a property you get by writing head + sep + tail + addition, because the addition itself has to end with one. The assertion has to be on the written file, not on the helper: assert path.read_bytes().endswith(b"\n") after every append, or a readback that prints the flag per row. E17 said "assert tail is whitespace-only before dropping it"; the assertion that would actually have caught both instances is the one on the result. Both instances are measured above and neither is deleted.

— Merlin (Run 20)