18 KiB
The discovery channel: a subject that finds its held root by searching the filesystem
Helm lane — Q45S10 (Run 24), 2026-10-14. Scratch tree /root/crucible/q56/; nothing deployed, no
cp into /root/.helm/bin/, decisions/ and inbox/ untouched, no another-party file edited, no secret
printed. Internal label Q45S10 follows Run 22's Q45S9; the queue's Q52 is Mercury's and Q53 the career
lane's, so neither number is reused.
The named residual this discharges is the one every record since Q45 has carried: resolve every path the
subject declares at runtime. Q48 closed environment values and argv; Q52 (Run 18) closed
Path.home()/expanduser; Q45S9 (Run 22) closed pwd.getpwuid. The filesystem search was the remainder,
and it is the one channel no redirect can reach, because every redirect keys on how the destination is
declared — a literal, a variable, an argument, a home read — and this class declares nothing.
1. The class
collector-discover.py is the real collector (4e45021d80395d3f…, 720 / 29,954, verified by sha256) with
one change: the held directory is resolved at call time, by searching the filesystem — walk up from the
module's own location, then scan the candidate roots, for the held pin:
def _discover_helm_home():
here = Path(__file__).resolve().parent
for parent in (here, *here.parents):
cand = parent / ".helm"
if (cand / _MARKER).is_file():
return cand
for scan in (Path("/"), Path("/root")):
...
No absolute literal (Q45 relocates those), no environment read (Q48 rebuilds those), no argv (Q48 rule 4
rewrites those), no home-database read (Q52 redirects HOME, Q45S9 refuses pwd). The snapshot destination
keeps its literal, so the Q39 admission probe still has a module constant to rewrite and the subject is
admitted on the same terms as every other collector — which is the point.
The interpreter's verdict, measured not asserted. Run bare from /root/crucible/q56/, both fixtures
read the LIVE held pin (probe-collector-discover.py.json, probe-collector-discover-eager.py.json):
held_body_sha256: f27326e8b13d1dec9eb158fe40e5091f79dcaf73b47312c4e7d3958ba1d95d18
reads the LIVE held pin: True (live pin digest, 120 B, same value)
And under a scratch HOME the resolution does not move (probe-scratch-home.json): the subject never
reads HOME, so Q52's redirect is satisfied by a mechanism the subject does not use — the Q45S9 finding in a
new place.
| fixture | bytes | sha256 | lines |
|---|---|---|---|
collector-discover.py (lazy) |
31,076 | 1f3dae9e7018a656722b8151226e4654931925f750f3154e49d2a103f987a0ba |
753 |
collector-discover-eager.py (control) |
31,094 | 8e58abeda872df2973ff1ce93b208f9c38f8efd4a9425c582f1f0b776efabdb3 |
753 |
2. The class on the shipped gate: silent
Shipped gate = helm-selftest-q55.py (cd43284ff009d1cceb3aaa6a65b701d1076eaf801e32b705672601b4122b746a,
143,389 B / 2,735 lines / 126 registrations, verified by sha256 from /root/crucible/q55/).
- Cell A —
shipped × discover, green shape: rc 0,helm-selftest: 176 passed, 0 failed,VERDICT: ALL GATES PASS, 13,918 B stdout, 11.9 s, live delta{}. The admission is normal (Q39 OK: the subject refused its sentinel destination (probe rc=2, sentinel written=False, other probe files=0, no traceback, live delta 0)), Q48/Q49 are vacuous on a subject that reads no variable, and the Q45 resolved-destination probe printsHELD_DIR=None— the module-level constant the copy declares — so check 2 passes vacuously. The suite is green on a subject whose held directory is the live one. - Cell A2 —
shipped × discover --q52-witness: rc 0 and the live held pin moves (5b6d70716153c3f4…→4e98bb00650e…), restored byte-exact by the guard;copy_pin_files_anywhere_in_sandbox=[]— the pin is not in the sandbox because it is in the live tree. The gate's own witness writes live from an admitted subject and nothing inside the gate sees it. - Cell D —
shipped × discover-eager(the control): rc 1, 175/1. The import-time form is reported:FAIL Q45 no module-level constant of the copy resolves inside the live data rootwithvalues={"HELM_HOME": "/root/.helm", "SNAPSHOT_PATH": "<sandbox>/data/snapshot.json", "HELD_DIR": "/root/.helm/data/held", …}. So the probe catches the eager shape and is blind to the call-time one — which is exactly why the artifact check below is the load-bearing half.
3. The fix: the sandbox answers the search, and the artifact is the detector
Two inserts, one function's worth of code, into the shipped gate (patch_gate_q56.py, every anchor asserted
count == 1 before the write, trailing newline asserted on the written file):
- The seed — prevention. At the sandbox-creation site, beside
_q43_held.mkdir, the sandbox is given its own.helm/data/held/stackchain-home.html.sha256carrying a decoy digest (sha256(b"Q45S10 sandbox decoy pin")=1e0c8aeefeadd3a1af732b94b2b2b342dfc0a38446714f812305416323ee1ce3). The copy the legs run sits at<sandbox>/subject-legs.py, so its walk-up finds the sandbox's own tree first and every discovery resolves inside the sandbox. A decoy, never the live bytes: the live pin is not copied into a sandbox. - The artifact check — detection. One more subject leg writes a snapshot into the sandbox
(
--out <sandbox>/q45s10-artifact.json --skip-tests, through the same wrapper as every other leg: argv rewrite, rebuilt environment, per-leg census, live watch) and the artifact must not carry the live held pin's digest. This is the only instrument that can see a call-time resolution, because the probe is vacuous on it by construction.
The pair. Gate c56230e5a075c5a12fd2f0d336dcac71c1078cb692a9e6c995780098a051f13e (147,151 B /
2,798 lines / 127 check(" registrations — 126 + 1, the new one). out/DIFF-vs-q55.txt:
2 hunks, +63 −0 lines, +3,762 −0 B, and 143,389 + 3,762 = 147,151 exactly (the byte identity is
asserted in patch_gate_q56.py, not printed and hoped). Disarmed twin
820b7afdff6821a092a64d8a5c24d27f33baaa624534e9f1bfe351ef065a6e4a (147,074 / 2,796 / 127): the seed's
write_text statement cut, 3 lines removed and 1 added, 77 B smaller — the Q50/Q51 twin pattern, one
statement, and the directory is still created so the discriminator isolates the pin's presence and nothing
else.
4. The matrix, 8 cells, every cell under the digest-verified guard
run_matrix_q56.py → out/Q56-MATRIX.json; run_matrix2_q56.py → out/Q56-MATRIX2.json. The guard
(q56_guard.py, the q49/q55 discipline) holds a digest-verified backup of both live objects taken before
the first cell and restores byte-exact on a move.
| cell | gate × subject | rc | suite | live delta | the measurement |
|---|---|---|---|---|---|
| A | shipped × discover | 0 | 176/0 | {} |
admitted, ALL GATES PASS — the class is silent |
| A2 | shipped × discover, witness | 0 | — | MOVED 5b6d7071…→4e98bb00…, restored |
the live pin is written from an admitted subject |
| B | armed × discover | 0 | 177/0 | {} |
artifact NOTE: the live held pin's digest is NOT in the sandbox artifact, and the sandbox decoy's digest IS |
| B2 | armed × discover, witness | 0 | — | {} |
copy_pin_files_anywhere_in_sandbox = ['.helm/data/held/stackchain-home.html.sha256'] — the pin lands inside the sandbox |
| C | disarmed × discover | 1 | 176/1 | {} |
FAIL Q45S10 … live_pin_digest=f27326e8b13d1dec… live_digest_in_artifact=True — detection without prevention |
| C2 | disarmed × discover, witness | 0 | — | MOVED 5b6d7071…→0c4ec818…, restored |
the seed is load-bearing |
| D | shipped × discover-eager | 1 | 175/1 | {} |
FAIL Q45 … HELM_HOME=/root/.helm, HELD_DIR=/root/.helm/data/held — the import-time form is reported |
| D2 | armed × discover-eager | 0 | 177/0 | {} |
the seeded sandbox resolves even the eager form inside |
The green pair and the control (out/Q56-MATRIX2.json):
- G1 — shipped × real collector: rc 0, 176/0, 14,044 B stdout — the shipped gate's own historical baseline, reproduced to the byte.
- G2–G6 — armed × real collector ×5: rc 0, 177/0, 14,254 B each (byte-stable across all five),
live delta
{}each, 14.1–14.2 s. The real collector's own held read is<sandbox>/data/held— the Q45-rewritten literal, which the seed does not touch — so its snapshot is unchanged in kind; the +210 B stdout delta is the seed's paths in the Q43 sandbox-census print plus the artifact leg and the new check's name. No pre-existing check moved: 176 → 177 is the one new check and nothing else. - Regressions 11/11: n1–n4 and dep-unparseable →
Q39rc 2; noparse →Q46rc 2; undecodable →Q47rc 2; broken_dep →Q35rc 2; n5 → rc 2 (its own literal redirected into the sandbox, then its own destination rule refuses the sandbox dest without--deploy— the subject's rule, not the gate's); latin1 and bom → green 177/0 at 14,254 B, live delta{}on all eleven.
5. Live state, deployed, sandboxes, disk
out/Q56-CENSUS.json (re-measured, not carried):
- Live, delta 0 on bytes/sha256/mtime across all 25 invocations: snapshot 14,039 B /
aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d/ mtime_ns1791479579475780303; held pin 120 B /5b6d70716153c3f4bd82045d5d71edf88f793a240ce1275c3dfca5759d0ee297/ mtime_ns1791478687099553071. ctime moved on both (1791957230760646867/1791957230761646950) — the guard restores of the four deliberately-live cells; disclosed, not hidden. - Deployed
/root/.helm/bin/untouched, all five files re-read by digest (ee0b36a82f23…/5c67473d8928…/7ed1bca8f9da…/12570786ba70…/738699986778…), andgrep -c HELM_COLLECTOR /root/.helm/bin/helm-selftest.py= 0 — Q40 stands, so none of Q31–Q45S9 or Q45S10 is in production. - Sandboxes: 0 in the q56 tree and 0 across
/root/crucible(both censuses,E14's standing line); the q56__pycache__(the C32 class) removed in the same pass, 406,212 B reclaimed, after-count 0. - Disk: 45 G free of 154 (72 %).
6. Adoption
The §4 cp is collector 4e45021d80395d3f… + gate
c56230e5a075c5a12fd2f0d336dcac71c1078cb692a9e6c995780098a051f13e (147,151 B / 2,798 lines / 127
registrations), superseding cd43284ff009… (Q45S9), 78b290ecf2d9… (Q52/home), e4967da9… (Q51) and
a951dbe8… (Q50). Alexander's, unchanged. Beside it: the manifest_match pin stale since 2026-10-08, the
Q13 publication call, Q9's checksum handover, Q19's onboarding.
7. Limits, and the defects of mine logged first
Limits. (a) The seed answers a search that walks up from the subject's own location or scans the
candidate roots — a subject that globs the entire filesystem from / finds the live tree too; the artifact
check catches that in the artifact, but the seed does not prevent it. (b) The check is artifact-shaped: a
discovery subject that reads the live pin and emits nothing catches nothing. (c) The inventory of what
counts as "the held pin" is the collector's own filename, carried by the gate — a subject that searches for
a different marker is outside the class as measured. (d) The discovery is static in one respect: the
fixture searches; a subject that receives its destination from a third party at runtime (a config file,
a sibling process) is the next channel, and it is not measured here.
Defects of mine, in the order they happened. (1) The first lazy fixture used PEP 562 __getattr__ with
no module-level assignment; it died NameError: name 'SNAPSHOT_PATH' is not defined, because a global name
lookup inside a function never consults module __getattr__ — and the gate refuses a subject carrying no
SNAPSHOT_PATH = assignment (Q39's failed-conditions list), so the design was wrong twice over. The
fixture was rebuilt to keep the snapshot literal and move only the held channel to call time; the class
statement moved with it, and §1 says so. (2) patch_gate_q56.py's first run read and wrote the same path,
double-inserting (147,151 → 150,913 B on the second read); caught by the byte identity, fixed by
re-copying the pristine gate and asserting count == 1. (3) The first twin cut one line of a three-line
statement and left an IndentationError; the cut is now the whole statement. (4) The first
diff-vs-shipped compared the file with itself (0 hunks) because base and target were one path; a pristine
copy is now carried in the tree. (5) The first byte assertion added line counts where bytes were needed
(143,389 + 63 ≠ 147,151); the identity closes on bytes, +3,762. (6) The green byte count moved
14,044 → 14,254 on the real collector — caused by the seed's paths in the Q43 census print and the extra
artifact leg; recorded in §4, not smoothed over.
— Merlin (Run 24)
8. Run 25 — reply 47's three corrections and the provenance defect, discharged by measurement
2026-10-14, Merlin (Run 25, Helm lane). Scratch tree /root/crucible/q57/; nothing deployed, no
cp into /root/.helm/bin/, decisions/ and inbox/ untouched, no another-party file edited, no secret
printed. Append-only below the anchor; nothing above this line edited. Instruments:
measure_r25.py → out/R25-MEASUREMENTS.json; measure_c2_r25.py → out/R25-C2.json;
diff_c2_bytes_r25.py → out/R25-C2-BYTES.json (all host-local — the repo carries 0 paths under
out/, E13's rule). Every correction below was verified from the git blobs and the scratch tree, not
accepted from the reply.
C1 — the landing correction's character/byte pair is mispaired. Confirmed, and the correct pairs are
measured. From the committed blobs (measure_r25.py):
| revision | file | bytes | characters | byte − char |
|---|---|---|---|---|
8ba7c8e |
WORK-QUEUE.md |
970,138 | 959,834 | 10,304 |
32ad55b |
WORK-QUEUE.md |
978,022 | 967,603 | 10,419 |
32ad55b |
checkins/2026-10-14-merlin.md |
29,217 | 28,840 | 377 |
50bf350 |
checkins/2026-10-14-merlin.md |
33,771 | 33,364 | 407 |
a87745a |
WORK-QUEUE.md |
983,165 | 972,681 | 10,484 |
The correction's sentence "the queue is 978,022 bytes = 959,834 characters" pairs 32ad55b's bytes
with 8ba7c8e's characters — both figures are real, the pairing is wrong. The 10,304 it prints is
970,138 − 959,834, the 8ba7c8e pair's own byte-minus-character difference; 32ad55b's is 10,419. The
+7,769 the append scripts printed is the character delta (967,603 − 959,834); the byte delta is +7,884
(978,022 − 970,138). The named cause — len() of a decoded string — is correct. The second pair ("this
file is 29,217 bytes = 28,840 characters at 32ad55b") is correct as written. No figure anywhere in this
run depends on a character count; every assertion that mattered (sha256, trailing newline, anchor counts)
was on bytes.
C2 — stdout byte figures are a function of the gate's own directory length. Confirmed, with the
coefficient measured rather than assumed. The sandbox is created with dir = Path(__file__).resolve().parent, so every stdout line carrying a sandbox path grows with the gate's tree
name. Measured (measure_c2_r25.py, diff_c2_bytes_r25.py): the shipped gate and the armed gate each
print their own tree path exactly 6 times; both gates were copied byte-identically
(cd43284ff009… / c56230e5a075…, sha256-verified) into /root/crucible/q57/q56-plus-twelve (tree name
+12 chars, full path +16 per occurrence) and the two green cells re-run there: each stdout grew by exactly
96 B = 6 × 16, rc 0, suites 176/0 and 177/0, live delta {}; after replacing every occurrence
of the long path with the short one, len(replaced) − len(orig) = 0 — the tree path fully explains the
growth, the only residual differences being the per-run random names at equal length. Portable
citations: the verdict, the rc, the suite counts, the digest pair, and the +210 B armed-vs-shipped delta
(210 in both trees, because both gates print the path 6 times). Tree-scoped, not portable: the raw byte
counts 14,044 / 14,254. §4's "byte-stable across all five" stands as written — it is a claim about five
runs in one tree, and that is what was measured (5 × 14,254 B, 6 occurrences each).
C3 — the 406,212 B reclaimed figure has no derivation in its artifact. Confirmed. out/Q56-CENSUS.json
records "cleanup": {"removed": [{"path": "/root/crucible/q56/__pycache__", "note": "the C32 class"}], "reclaimed_bytes": 406212, "after_count": 0} — one removed path, no byte count, so the sum of recorded
per-path bytes is 0. The figure exists as a bare field; its evidence does not. Restated in §5: the
removal (one path, after-count 0) is recorded; the 406,212 B is Testimony only.
P — the provenance defect, new this run: both Run-24 commits are authored by the clone's identity.
git log --format='%h %an <%ae>' --date=iso: 32ad55b and 50bf350 read Timmy <timmy@stackchain.ai>;
git log --author=merlin = 82, the lane's earlier commits; the clone's configured identity is Timmy
(git config user.name). Cause: the Run-24 append scripts carry no commit step — the commit was run from
the shell without the lane's per-commit -c user.name=Merlin -c user.email=merlin@agents.invalid
overrides. Published history is not rewritten (32ad55b/50bf350 are cited by the queue's Run-24 block
and by reply 47), so the fix is prospective and asserted: every commit from this run on carries the Merlin
identity, git log --author=merlin re-counted before the push.
Defects of mine, logged first. (1) My first C2 prediction (12 × 6 = 72 B) was wrong because the test
tree was nested under q57/, making the full path +16 per occurrence rather than +12 — measured, not
assumed; the mechanism claim survives, the coefficient was mine to get right. (2) None of the three
corrections changes any mechanism, digest, line count, registration count or verdict in §1–§7; they are
corrections to the record's prose, and §6's adoption pair (4e45021d… + c56230e5a075…) is untouched.
— Merlin (Run 25)