Q32 DONE: the G8 two-run agreement check is deterministic — declared scrub set covers every clock-derived field (age_seconds, last_active_at, last_checkin_at, the newest-file pointer), 6 consecutive green gate invocations, 8 paired runs with zero differing leaf paths; collector 8a477376 unchanged, adoption identity is now collector 8a477376 + selftest 9ce6a4e9

This commit is contained in:
Merlin committed 2026-10-11 19:59:42 +00:00
1 parent 4f5eecc71b
commit 7e12c82506
3 files changed
+234

No files matched your search

+22
View File
@@ -4314,3 +4314,25 @@ zero differing leaf paths across ≥5 pairs. Scratch tree: `/root/crucible/q31/`
`8a477376…` (collector) and `1d58b823…` (selftest) until Alexander's `cp`. `8a477376…` (collector) and `1d58b823…` (selftest) until Alexander's `cp`.
**Status: OPEN — named to the Helm lane**, ahead of the Q31 `cp` decision so that the gate that ships is the one that **Status: OPEN — named to the Helm lane**, ahead of the Q31 `cp` decision so that the gate that ships is the one that
runs green deterministically. runs green deterministically.
**Status: DONE 2026-10-11 (Merlin, Run 8) — `recoveries/helm-g8-scrub.md`. The gate is deterministic: 6 consecutive
green invocations (150 passed / 0 failed each), 8 paired runs with zero differing leaf paths, and the defect measured
wider than stated.** Root cause went beyond `age_seconds`: the pre-fix paired probe (age-only patch) was still 7 of 8
pairs red on `profiles[].last_active_at` (`helm-collect.py:208`, newest mtime over `sessions//cron/output//logs/` —
changes whenever a live session writes) and `profiles[].evidence.source_path` (`:200`/`:212` — not a citation but the
**pointer to that newest file**, volatile by construction), plus `members[].last_checkin_at` (`:493`, same class). The
fix is selftest-only, the collector untouched: `SCRUB_KEYS` is declared and every key is verified present as a literal
in the collector source; the pointer rule nulls `evidence.source_path` only inside dicts carrying a live mtime reading,
with a check proving 12 stable citations **survive**; every key carries its fresh-collect live count in the check name
and the pre-Q32 scrub runs as a control proving the old set kept what the new one nulls. Control measurement, same 8
pairs under the pre-Q32 scrub: 12 differing leaf paths, 4 of 8 pairs red. Gate: `HELM_COLLECTOR=/root/crucible/q31/
helm-collect.py python3 helm-selftest.py` → 6 runs, each exit 0 / `helm-selftest: 150 passed, 0 failed` / `VERDICT: ALL
GATES PASS` (137 standing + 13 new Q32 assertions; G8 now 31 checks), one log digest per run in the deliverable. Diff
`helm-selftest-q32-before.py` `1d58b823…` → `helm-selftest.py` `9ce6a4e971525fdd…` (+115/−5, `DIFF-Q32.txt`). Live
snapshot delta 0 throughout (`aa842b0f…`, 14,039 B, mtime_ns `1791479579475780303`); deployed bin unchanged
(`ee0b36a8…` collector, `7ed1bca8…` selftest — nothing deployed); collector artifact `8a477376…` unchanged. Limits
stated in the deliverable: the enumeration is source-verified, not proven complete (the gate fails closed on any new
volatile field); the `evidence.source_path`-as-mutable-pointer design smell is a collector-side follow-up, not made
here. **Adoption ask (Alexander, §4):** `cp` the collector `8a477376…` plus the selftest **`9ce6a4e9…`** (superseding
`1d58b823…`) into `/root/.helm/bin/` — Q31's adoption is unblocked behind it; deploy/schedule of collect+render still
his call.
+79
View File
@@ -1002,3 +1002,82 @@ edited; no secret printed.
— Merlin — Merlin
---
# Run 8 — Q32: the G8 two-run agreement check is deterministic (Merlin, 2026-10-11 ≈19:5x–20:2x UTC)
> Screen. Every claim below carries a path, a digest, or a command with its output. No secret is
> printed here or in the files this lands.
## Did
Claimed and finished **Q32 — the G8 two-run agreement check is nondeterministic** (the queue's only
OPEN item, named to the Helm lane; Q20 is zephyrus-sparkborn's, Q9 blocked on the audio handover,
Q13/Q19 gated on Alexander). Deliverable: `recoveries/helm-g8-scrub.md`, scratch tree
`/root/crucible/q31/` (re-runnable: `q32-probe.py`, `q32-paired.py`).
**Root cause, and it is wider than the item stated.** The scrub covered `observed_at`,
`rtt_seconds`, `generated_at` and `collector.duration_seconds` — and missed `age_seconds`
(`helm-collect.py:545`, `round(age, 1)` from the run-end clock, on every source block). But a
paired-run probe with an age-only patch was **still 7 of 8 pairs red**, on
`profiles[].last_active_at` (`:208` — newest mtime over `sessions//cron/output//logs/`, which
changes whenever a live session writes), `profiles[].evidence.source_path` (`:200`/`:212` — not a
citation but the **pointer to that newest file**, volatile by construction) and
`members[].last_checkin_at` (`:493`, same class).
**The fix, selftest-only — the collector artifact `8a477376…` is untouched.** `SCRUB_KEYS` is
declared and every key is verified present as a literal in the collector source; the pointer rule
nulls `evidence.source_path` only inside dicts carrying a live mtime reading, with a check proving
**12 stable citations survive** the scrub; every key prints its fresh-collect live count in the
check name and the pre-Q32 scrub runs alongside as a control proving the old set kept what the new
one nulls. Diff `1d58b823…` → `9ce6a4e971525fdd…`, +115/−5 (`DIFF-Q32.txt`).
**Results, measured:**
- **8 paired runs, 0 differing leaf paths** under the gate's own `_scrub` (extracted from the
selftest source by AST and exec'd — the paired script cannot drift from the gate). Control, the
same 8 pairs under the pre-Q32 scrub: **12 differing leaf paths, 4 of 8 pairs red**.
- **6 consecutive gate invocations**, `HELM_COLLECTOR=/root/crucible/q31/helm-collect.py python3
helm-selftest.py`, each exit 0 / `helm-selftest: 150 passed, 0 failed` / `VERDICT: ALL GATES
PASS` (137 standing + 13 new Q32 assertions; G8 now 31 checks). One log digest per run in the
deliverable: `bfcc35cd92484acb`, `0a6272e3f6d88a96`, `bcf24e51049e661e`, `b9740dbd33f9f494`,
`f69d56ef176b2eaf`, `4bf31912f396161d`.
**Nothing touched.** Live snapshot `/root/.helm/data/snapshot.json` delta 0 throughout
(`aa842b0f…`, 14,039 B, mtime_ns `1791479579475780303`, before and after every run); deployed bin
unchanged (`ee0b36a8…` collector, `7ed1bca8…` selftest — nothing deployed); live ledger index
`https://alexanderwhitestone.com/stackchain/blocks/` 200 / 34,488 B /
`4f764ef4dfb9c08b2654ff0783550d20ad1bb7a31cb0c32070974a5fe451557e`; `decisions/` and `inbox/`
untouched; nothing executed; no another-party file edited.
## Blocked
Nothing blocked the run. Limits, stated in the deliverable: the scrub enumeration is
source-verified, not proven complete (the gate fails closed on any *new* volatile field — a pair
goes red and names the leaf — but nothing proves the seven literals are all the clock-derived keys
the collector will ever emit); the `evidence.source_path`-as-mutable-pointer design smell is a
collector-side follow-up, not made here, because the collector is the artifact under adoption and
Q32 deliberately does not change it; the gate runs with `--skip-tests` (the standing Q31 caveat).
## Next
- **Alexander's call, §4:** adopt the gated collector `8a477376…` **plus the selftest
`9ce6a4e9…`** (superseding `1d58b823…`) into `/root/.helm/bin/` — Q31's adoption is unblocked
behind it — then the deploy/schedule decision for collect+render. Same gate as Q26–Q30.
- **Q7, measured:** after Q32 the queue's only OPEN item is none; Q20 is zephyrus-sparkborn's named
lane, Q9 blocked on the audio handover, Q13/Q19 gated on Alexander. The unclaimed executable set
is empty again, so a successor is owed; the natural candidate is the collector-side redesign of
the newest-file pointer (`evidence.source_path` as a stable citation plus a named
`latest_active_path` field) — this run's own residue, so per the standing pattern I open none and
name it instead.
- The deploy-or-retire ask for the honest Helm page (Run 2) and the adopt-or-hold asks on the
patched builder (Q26–Q29) all still stand unchanged.
## Ask
1. **Alexander — adopt the gated collector + the fixed selftest?** The identity pair is now
`8a477376…` (collector, unchanged) and `9ce6a4e9…` (selftest, the Q32 fix). It is the difference
between a gate whose red is a coin flip and one with 6 consecutive green invocations behind it.
Scratch until you say so.
2. **Nothing asked of zephyrus-sparkborn this tick.** His lane is Q20.
+133
View File
@@ -0,0 +1,133 @@
# Q32 — the G8 two-run agreement check is deterministic
> Screen. Every claim below carries a path, a digest, or a command with its output, measured this run
> (2026-10-11 ≈19:5x–20:2x UTC) from this host on the scratch tree `/root/crucible/q31/`. No secret is
> printed here or in the files this lands.
## The defect
G8 asserted "two CLI runs agree byte-for-byte once the clock fields are scrubbed", comparing two real
subprocess collects. The scrub covered `observed_at`, `rtt_seconds`, the top-level `generated_at` and
`collector.duration_seconds` — and nothing else. `helm-collect.py:545` stamps
`age_seconds = round(age, 1)` on **every source block** from the run-end clock, so any run pair
straddling a 0.1 s boundary in a source's age disagreed on that leaf alone. A gate whose red is a coin
flip cannot be cited as evidence that an artifact is safe, and the Q31 adoption ask rests on exactly
that citation.
## Root cause, and it is wider than `age_seconds`
Before patching, a paired-run probe ran with an `age_seconds`-only patch: **7 of 8 pairs still red**,
on two further volatile fields the scrub also missed:
- `profiles[].last_active_at` (`helm-collect.py:208`) — newest mtime across `sessions/`,
`cron/output/`, `logs/`. Changes whenever a live session writes a file, i.e. constantly on this host.
- `profiles[].evidence.source_path` (`helm-collect.py:200`, `:212`) — **not a citation but a pointer**:
the path of that newest file. Volatile by construction wherever a live mtime reading exists. Every
*other* `evidence.source_path` in the snapshot is a stable citation.
- `members[].last_checkin_at` (`helm-collect.py:493`) — check-in file mtime, the same class (the
`checkins/` directory is written by other agents). It did not differ in this run's 8 pairs; it is
the same class and is covered.
## The fix — selftest only, the collector is untouched
- `SCRUB_KEYS = {observed_at, rtt_seconds, age_seconds, last_active_at, last_checkin_at}`, **declared**.
A gate check verifies every key appears as a literal in the collector source (with `generated_at` and
`duration_seconds` handled explicitly at the top level), and a second check that the declared set is
exactly the clock literals minus the two top-level keys.
- **The pointer rule:** `evidence.source_path` is nulled inside any dict carrying `last_active_at` or
`last_checkin_at`. A gate check proves the carve-out is measured, not asserted: **12 stable
`evidence.source_path` citations survive the scrub** (the 24 newest-file pointers are nulled).
- **Non-vacuity per key:** each check name prints the fresh collect's live (non-None) value count, and
the **pre-Q32 scrub runs alongside as a control** proving the old set kept every value the new one
nulls. (`email.age_seconds` is legitimately `None` — UNREACHABLE source — so 5/6 is the honest count.)
- Diff: `helm-selftest-q32-before.py` (sha256 `1d58b823809aa76e…`) → `helm-selftest.py`
(sha256 `9ce6a4e971525fdd4e49150fbeb49e584b7eb9cad293164f060a6f2c47739562`), `DIFF-Q32.txt`,
**115 lines added / 5 removed**.
## Results, measured
**Paired runs** — `q32-paired.py`, 8 pairs, two real subprocess collects per pair, both bodies scrubbed
with the **gate's own `_scrub`** (extracted from `helm-selftest.py` by AST and exec'd, so the paired
script cannot drift from the gate), compared leaf by leaf:
- **8 pairs, 0 differing leaf paths** under the gate's scrub.
- Control, same 8 pairs under the pre-Q32 scrub: **12 differing leaf paths, 4 of 8 pairs red**
(`sources.cron_jobs.age_seconds`, `sources.hermes_profiles.age_seconds`,
`profiles[N].last_active_at`, `profiles[N].evidence.source_path` on actively-written profiles).
Log: `q32-paired.txt`.
**Gate invocations** — `HELM_COLLECTOR=/root/crucible/q31/helm-collect.py python3 helm-selftest.py`,
6 consecutive runs, each **exit 0, `helm-selftest: 150 passed, 0 failed`, `VERDICT: ALL GATES PASS`**
(137 standing + 13 new Q32 assertions; G8 is now 31 checks). One log's digest per run:
| run | exit | passed / failed | log sha256 (first 16) |
|---|---|---|---|
| 1 | 0 | 150 / 0 | `bfcc35cd92484acb` |
| 2 | 0 | 150 / 0 | `0a6272e3f6d88a96` |
| 3 | 0 | 150 / 0 | `bcf24e51049e661e` |
| 4 | 0 | 150 / 0 | `b9740dbd33f9f494` |
| 5 | 0 | 150 / 0 | `f69d56ef176b2eaf` |
| 6 | 0 | 150 / 0 | `4bf31912f396161d` |
The digests differ run to run **by construction**: the collector's stdout carries the digest of the
file it wrote, and the written body carries the unscrubbed clock fields. Each run's digest is the
evidence for that run.
New assertions, with their fresh-collect counts from run 1's log (`q32-gate-run-1.log`):
```
PASS G8 two CLI runs agree byte-for-byte once the clock fields are scrubbed
PASS G8 every clock-derived key the collector emits appears in its source
PASS G8 the declared scrub set is exactly the clock literals minus the two top-level keys
PASS G8 the scrub normalises age_seconds (fresh collect: 5/6 non-None)
PASS G8 the pre-Q32 scrub kept age_seconds (the defect, as a control)
PASS G8 the scrub normalises last_active_at (fresh collect: 20/20 non-None)
PASS G8 the pre-Q32 scrub kept last_active_at (the defect, as a control)
PASS G8 the scrub normalises last_checkin_at (fresh collect: 4/4 non-None)
PASS G8 the pre-Q32 scrub kept last_checkin_at (the defect, as a control)
PASS G8 the scrub normalises observed_at (fresh collect: 36/36 non-None)
PASS G8 the scrub normalises rtt_seconds (fresh collect: 1/1 non-None)
PASS G8 the newest-file pointer evidence.source_path is scrubbed (fresh collect: 24/24 non-None)
PASS G8 the pre-Q32 scrub kept the newest-file pointer (the defect, as a control)
PASS G8 stable evidence.source_path citations survive the scrub (12 paths)
```
## Nothing touched
- The gated collector `/root/crucible/q31/helm-collect.py` is unchanged at
`8a477376f72c578b0633e07741b44c717482fe62b29a3cc3f8abb7a6696ea76c`, verified before and after every
run this tick. Q32 changes the selftest only.
- The deployed `/root/.helm/bin/helm-collect.py` is unchanged at `ee0b36a82f23fcf3…` and
`/root/.helm/bin/helm-selftest.py` at `7ed1bca8f9da624f…` (the deployed gate is still the 119-pass
one; nothing was deployed).
- The live snapshot `/root/.helm/data/snapshot.json` is **14,039 B / sha256
`aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d` / mtime_ns
`1791479579475780303`** — byte- and mtime-identical before and after the paired runs and all six
gate invocations (delta 0).
- The live ledger index `https://alexanderwhitestone.com/stackchain/blocks/` is unchanged: **200 /
34,488 B / sha256 `4f764ef4dfb9c08b2654ff0783550d20ad1bb7a31cb0c32070974a5fe451557e`**.
- `blocks.json`, `decisions/` and `inbox/` were not touched; nothing was executed; no another-party
file was edited; no secret is printed in this record or in the files it lands.
## Limits, stated not hidden
- **The enumeration is declared and source-verified, not proven complete.** The gate fails closed on
any *new* volatile field (a pair goes red and the leaf path is named in the failure), but nothing
proves a priori that the seven clock literals are all the clock-derived keys the collector will ever
emit.
- **The pointer rule is a scrub convention; the design smell stands.** For a profile or a member,
`evidence.source_path` is a mutable newest-file pointer where every other `evidence.source_path` is
a stable citation. The clean fix is collector-side — point `source_path` at the profile directory
(or the member's file) and name the newest-file path its own field. The collector is the artifact
under adoption and Q32 deliberately does not change it.
- The gate runs with `--skip-tests` (the standing Q31 caveat): the ~2 s collector is deterministic and
does not invoke the Timmy suite.
## Adoption
Q32 changes only the selftest. The Q31 adoption ask becomes: `cp` the collector `8a477376…`
(unchanged) **plus the selftest `9ce6a4e9…`** (superseding `1d58b823…`) into `/root/.helm/bin/` — the
gate that ships is the deterministic one. Deploying collect+render, and scheduling it, remain
Alexander's under §4.
— Merlin, 2026-10-11