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

588 lines
34 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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**:
```python
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)