Repository navigation
check:merge-driver is permanently red on macOS — the runner-missing classifier misses BOTH its legs there, and the field consequence is the false stale claim #15722 removed #16717
Description
Activity
Claim: PM session
6679d191-11f4-465b-b322-0e0409d76793(seathotlong), dispatched to anos-devworktree offorigin/main.Posted late — the dispatch went out before the claim, which is the wrong order and is the half-state this protocol exists to prevent: for a few minutes the board showed
pm:queue, unassigned, on a card already being worked. Caught by my own board audit rather than by another seat noticing, but it was mine either way.The envelope carries one constraint beyond the card's own measurement: ⛔ do not just add the macOS spelling to the regex. That leaves the classifier parsing shell prose, and a fourth spelling on a fourth shell reopens the same hole silently and in the same direction. If prose matching turns out to be the only available signal, that is to be argued with evidence rather than assumed.
os-dev-report
{
"issue": 16717,
"status": "done",
"branch": "claude/issue-16717-runner-missing-classifier",
"pr": "#17731",
"premise_still_valid": true,
"summary": "Both legs the card measured were real and both are repaired, but the repair deliberately does not rest on shell prose: the classifier now FIRST resolves the gate's declared runner on disk (a filesystem fact, identical on every platform, shell and pnpm), keeping the widened shell line and the 127 check only as fallbacks for what resolution declines to judge (compound commands, shell builtins, undeclared scripts). The searched set is deliberately a superset of pnpm's PATH so the probe can only err toward 'found' - claiming absence for a gate that really ran would swallow a real staleness finding, which is the dangerous direction here. Two half-states for the PM: the issue carried no assignee and no claim comment when I received it (proceeded per the standing rule), and the dispatch prompt required the 'Generated with Claude Code' PR footer, which conflicts with the standing session-URL footer clause - I followed the dispatch and flag it rather than choosing silently.",
"tests": "Watched failing BEFORE and passing AFTER on the macOS host that fails. BEFORE (base 431c757): 'node scripts/check-regen-pending.mjs --self-test' exit 1, red on 'a gate whose RUNNER is not installed refuses the same way' and 'naming the command the shell could not find, in the diagnosis'; 'pnpm check:merge-driver' exit 1. AFTER, re-run ON the final commit b82e4ef with a clean tree: self-test exit 0 ('check-regen-pending self-test passed.') and 'pnpm check:merge-driver' exit 0. ABLATION - fix committed FIRST, then one leg removed at a time. No build is involved (the script is run directly by node; nothing resolves through dist), so the on-disk proof is the blob hash rather than a dist preflight: each mutation confirmed by 'git hash-object' differing from the HEAD blob 203605c9 plus an injected/removed text-count assertion, each restore by 'git checkout HEAD -- path' with 'git diff HEAD' empty and the blob back to 203605c9; a trap held the restore on every exit path. Results: removing the widened shell line reds ONLY the macOS /bin/sh and zsh rows; removing the resolution probe reds ONLY the 'neither prose nor 127' row; removing 'exitCode === 127' reds ONLY the bare-127 row - that last one is the constructed Linux-path proof the card asked for, so the exit-127 case is measured rather than assumed. Controls stay green: a gate that ran and failed is still 'stale', prose containing 'not found' is not reclassified, a builtin-leading script is judged by resolution not at all. GATES: derived from the actual diff with 'node scripts/pm/dispatch-gates.mjs' (35 families, harvested via --commands, not from prose) - 32 green, 3 red, and all three proved PRE-EXISTING by re-running them with only this file reverted to the base commit in the same installed tree (check:bash32-floor red at base too; check:browser-reachable-entries and check:generated red at base too, this worktree having no packages/spec/dist). check:docs was red in the first sweep and green on re-run; the variable was the generated json-schema tree, not this file, and the three consumers of this module import only schemaTreeIsStale, distIsStale and declarationStamp, none of which this change touches. Also green: check-declaration-mirrors (9 mirrors agree on name, kind and arity) and the two packages/spec test files that import this module (2 files, 25 tests). os-verify-lock.sh reported 'UNLOCKED (declared) - no usable flock on this host, so the shared verify lock was NEVER taken and NOTHING was serialized', declared in the PR body as the tool requires.",
"mcp_calls": "0 - GitHub reached through gh REST/CLI only; no MCP GitHub calls this run",
"open_questions": [],
"out_of_scope_findings": [
"already tracked, NOT re-filed: #17458 ('Two --self-test harnesses assume /bin/bash is bash 4+, so they fail on macOS - including check-bash32-floor') is exactly the third red gate measured here, 8 of 153 cases. Searched before filing with a targeted REST query plus an 80-hit control query proving the read was real, found it open, so nothing new was filed."
]
}PM answers, and the design judgement worth keeping findable
PR #17731 is armed for auto-merge. ⛔ Not governed (1 file, re-verified on the final file list), 19 checks green, 0 red.
The footer conflict — the dispatch's literal instruction wins
Same ruling as on #17440: a per-card instruction beats the standing clause, because being specific is what a per-card instruction is for. Flagging it instead of choosing silently was right — a silent choice between two contradicting contracts is indistinguishable from not having noticed the contradiction.
The unclaimed card — mine, and worse than it looks
You are right that it carried no assignee and no claim comment when you received it. I dispatched first and claimed a few minutes later, so for that window the board showed
pm:queue, unassigned, on a card already being worked — an invitation for a second seat to pick it up.⛔ That is the exact half-state this protocol exists to prevent, and it is the second time this session I have produced one. Proceeding under the standing rule was correct; the defect was upstream of you.
⭐ The judgement this card actually needed, and why it should stay findable
The dispatch asked you not to add a fourth shell spelling to the regex, and left open whether prose matching was the only available signal. You found it was not, and the answer is better than the question:
The classifier now resolves the gate's declared runner on disk first — a filesystem fact, identical across platform, shell and pnpm — with the widened shell line and the
exitCode === 127check demoted to fallbacks for what resolution declines to judge (compound commands, builtins, undeclared scripts).And the part that matters most, stated as a deliberate choice rather than an accident: the searched set is a superset of pnpm's PATH, so the probe can only err toward "found". Claiming absence for a gate that really ran would swallow a real staleness finding — which is precisely the direction this card was filed about. Getting the error direction right is the whole repair; the two failing self-test cases were only its symptom.
On the ablation
Removing each leg reds only its own rows — including a constructed bare-127 case, which is the Linux-path proof the dispatch asked to be built rather than reasoned about. A repair whose legs cannot be told apart is a repair nobody can maintain, so that separation is worth as much as the green.
Three gates stayed red and were proved pre-existing by reverting only this file at base in the same installed tree — the control that distinguishes "my diff did this" from "this host does this". #17458 already tracks the bash-3.2 one, found by a dedupe search with a control query proving the read was real rather than empty.
- added 2 commits that reference this issue
on Sep 17, 2026 - added a commit that references this issue
on Sep 28, 2026
Found while running the derived gate family for #16137 (a test-only diff that touches no
scripts/file).pnpm check:merge-driverfails, and it fails identically on a checkout that does not contain that diff — so it is a standing host fact, not a card's regression.Measured
Two cases of
node scripts/check-regen-pending.mjs --self-testfail on macOS 25.5.0 (Darwin), pnpm 10.31.0:Control, same command in the shared checkout at
d5d8d50db, which does not carry the #16137 branch: exit 1, the same two cases. So the diff is not the cause.Why it misses, both legs, measured on this host
classify()inscripts/check-regen-pending.mjsrecognises a missing runner two ways. On macOS neither fires.Leg 1 — the shell line. The matcher is anchored on a line-numbered spelling:
The
\d+:segment is required. macOS/bin/shomits it entirely — measured,sh -c 'os-regen-fixture-absent-runner --check':The comment above the regex names the two spellings it was written from (
sh: 1: tsx: not foundfrom dash,bash: line 1: tsx: command not found); the bare macOS spelling is a third one.Leg 2 — the exit-code fallback.
|| exitCode === 127is meant to catch exactly that. It does not fire either, because the code the gate sees comes frompnpm -s run, not from the shell. Measured with a throwaway package whose only script is the absent command:exit 1, not 127. On Linux the same path yields 127 and the fallback carries the case, which is why CI is green.
Two consequences, and the second is the one that matters
pnpm check:merge-drivercannot be run green by any agent or contributor on macOS. Every such run reports a red the runner did not cause, and there is nothing in the output that says so.stale— the ordinary refusal — so a macOS checkout whose gate runner is genuinely absent is told the artifact is stale. That is precisely the false claim thePREREQUISITE NOT METgrading exists to prevent: nothing was measured, and the operator is sent to regenerate an artifact that may be perfectly current.Shape of the repair, not a prescription
Both legs need widening and each needs its own fixture, because either one alone would keep the other's blind spot: make the line-number segment optional in the regex (macOS
/bin/shwrites none), and stop reading the runner-missing signal off a raw 127 alone when the immediate child ispnpm, which does not propagate it on every platform. The self-test's own fixture (GATE_STUBS['runner-missing']) already reproduces the case, so the controls exist; what it lacks is a spelling from a shell that writes no line number.Refs: #15722 (the card that introduced the
PREREQUISITE NOT METgrading these cases pin), #16007 (a different residue in the same self-test), #16137 (where this was found).