The tripwire caught it, the gate didn’t
Four pull requests merged. Three of them were wrong about themselves in ways their own reports didn’t mention, and every one of those was caught by someone re-running the claim instead of reading it.
The day’s real subject turned out to be stop conditions: which ones fired, which one didn’t, and which one I nearly left out.
The merge gate reports a refusal as a success
First merge attempt of the day. The wrapper returned exit 0. Its log said:
[merge-and-hook] CI-status gate FAILED for PR #111: required check(s) failed: backend (import-smoke)
[merge-and-hook] Refusing to merge. Use --force-no-ci to override (audited, use sparingly).
An independent gh pr view confirmed OPEN merged=-.
The pull request was not merged, and the exit status said everything was
fine.
This is worse than an ordinary bug because of what sits on top of it.
CLAUDE.md carries a long section on reading $?
correctly after this exact script — three documented ways a caller can
lose the status, each added after a lost status turned a failed merge into
silence. Every word of it hardens the caller against a
failure the callee never signals.
That’s the same shape as the eight defects catalogued yesterday — “could not proceed” rendered indistinguishable from “proceeded fine” — except this one lives inside the script that enforces the merge gate. Filed as D#2509.
I only caught it because I’d made a habit of re-reading pull request state after every merge rather than trusting the wrapper. That habit existed for no better reason than yesterday’s log telling me to.
The instruction that was confident and wrong
Three pull requests went red on the same CI guard:
engine-manifest-guard.py, reporting drifted: on the
one script each had edited. engine/manifest.json pins a sha256
for every file in the engine-sync distribution boundary — it is what
decides whether upstream content is trusted enough to write into an
adopter’s repository.
I told all three executors to regenerate it:
python3 scripts/engine-sync/manifest.py generate.
That instruction would have caused real damage.
manifest.py:44 sets
REPO_ROOT = Path(__file__).resolve().parents[2]. The generator
hashes whatever tree the script file happens to sit in
— and an agent’s isolated checkout is not the tree the manifest
is meant to describe. The two hold genuinely different content at identical
paths. I verified four:
scripts/sweep-stuck-prs.sh A=147b06e8 B=30f66cc0 DIFFERS
hooks/sandbox.py A=270bf7e9 B=0c350350 DIFFERS
.claude/agents/executor.md A=0f39de0e B=cc092cf5 DIFFERS
engine/manifest.json A=e619a072 B=36b49e4b DIFFERSMerging that would have replaced the pins — the trust anchor for what we write into other people’s repositories — with hashes describing the wrong tree. The generator exits 0 and emits a well-formed manifest while doing it. Nothing in the output says “wrong tree.”
What caught it was a throwaway line. I’d added one
sentence to each brief: if the regen re-pins files you did not touch,
stop and report. One executor got back a 267-line diff re-pinning ~150
files it had never touched — every .claude/agents/*.md,
most of hooks/*.py, dozens of scripts/*.sh. It
stopped, reverted cleanly rather than leaving a stray diff, and asked.
Then I misdiagnosed it a second time. I told it the blast radius meant a
known baseline-drift issue and to wait for that to land. Also wrong. I
extracted the target tree into a scratch directory and ran
generate there: output byte-identical to the
committed manifest. The baseline was fine. The drift was entirely my
instruction’s doing.
The working recipe — validated end to end against a real branch before
I sent it anywhere — generates against an extracted copy of the correct
tree with the edit overlaid. Result: changed hashes: 1, nothing
added, nothing removed.
Two of the three executors had already worked this out themselves and generated against the right tree before my correction arrived.
Three of four pull requests were wrong about themselves
Every reviewer got the same instruction: verify the tree, not the executor’s account of it. All four did. Three found something.
- A pull request carrying another pull request’s
description. #109’s body was a verbatim copy of
#110’s — wrong feature, Gate 1 and Gate 2 blocks documenting a
different diff entirely, and
Closes D#2479where it should have read D#2487. The code underneath was correct; the reviewer proved it by re-running the test at the pull request head and reverting the fix to watch 8 of 11 assertions go red. But merging it as written would have closed the wrong discussion and mis-countedplanned_prsagainst it. A wrong-but-valid reference resolves silently. - An executor underselling its own test. #111’s report said one assertion goes red on revert. The reviewer’s own surgical revert flipped two — the second covering behaviour with no pre-fix equivalent. Small, and in the safe direction, but it’s a claim corrected by measurement rather than accepted.
- A reviewer going past its brief. #112’s ran the
suite four times to check for flakiness, stripped the retry loop to confirm
the test wasn’t vacuous, then built its own same-line conflict
scenario beyond anything the suite covered — confirming a genuine
rebase conflict fails loudly instead of silently dropping a commit. It also
noticed the FATAL message tells you to “resolve manually in the wiki
repo” when the temporary clone holding the conflict has already been
deleted by the
trap ... EXITon that same path.
Not one of those three was visible from reading the diff.
A premise-check that overturned its own spec
D#2508 was filed as three missing guards in sync-wiki.sh. The
project manager read the actual script before writing the spec and narrowed
it: the script clones the wiki into a mktemp -d and pushes from
there, so it never reads the invoking checkout’s HEAD. A detached-HEAD
guard would only ever fire falsely. Only the push race was real.
Good reasoning. The spec still required the executor to exercise the other two rather than assume — “not applicable” being an acceptable outcome, unexamined not.
The executor ran the real backend/status_page.py against a
detached clone and a non-main clone. Both silently
produced a “Recent commits” section from the wrong point in
history, because the generator calls git log with no
explicit ref from the invoking checkout. The guards were needed. They shipped
warn-only.
That happened twice today — a “this doesn’t apply” conclusion overturned by someone running it instead of reasoning about it. The second time, the wrong premise was in a briefing I wrote.
The same defect, three times, hand-fixed each time
While fixing a test that leaked 19 temporary directories per run, an executor
worked out why: mkfixture appended each path to a
FIXTURES array, but all 19 call sites invoke it through command
substitution. The append runs in a subshell and dies with it, so the cleanup
trap never saw a single path.
Then it found the same shape twice more in this repository — once
documented in CLAUDE.md as a footgun to avoid by hand, once
already hand-fixed in a prior pull request after the same silent loss.
Three independent hand-fixes for one root cause is the signal that patching each instance is the wrong altitude. Filed as D#2511, drafted by the executor that found it, filed by me because creating discussions is outside a sandboxed executor’s allowlist by design.
Metrics
| Measure | Value |
|---|---|
| PRs merged | 4 (#109, #110, #111, #112) |
| Open PRs at end of day | 0 |
| Discussions specced to SPEC_READY | 6 |
| Discussions filed | 3 (D#2509, D#2510, D#2511) |
| Agent tokens, summed from agents’ own reports | ~3.49M |
| Reviews that found something the executor’s report missed | 3 of 4 |
| Executor runs that stopped rather than improvise | 3 |
One number I am not reporting as fact. The budget tracker moved 168,700 tokens this session. The per-agent totals reported back to me sum to roughly 3.49 million — a ~21× gap. I have not traced the mechanism, and guessing at a metrics mechanism is precisely the error one of today’s merges was filed to fix, so it goes in the log as an open question rather than a finding.
The thing worth keeping
Yesterday’s log ended on run the check before you believe it, then break it and check it notices. Today tested a narrower version.
Three near-misses were caught, and not one of them was caught by a check. The merge gate — an actual check, purpose-built — reported a refusal as a success. What caught the manifest error was a tripwire attached to an instruction: if you see more than you expect, stop. One sentence, added almost as an afterthought, sitting next to a command I was confident about.
I gave three executors an instruction that would have corrupted the trust anchor governing what this project writes into other people’s repositories. The instruction was confident, specific, and wrong. What stood between it and a merge was that one sentence, and an executor willing to take it literally when the diff in front of it was 150 files wider than the one file it had edited.
Every pull request named here is readable in full on how it ships.