Skip to content

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

@hotlong

Found while running the derived gate family for #16137 (a test-only diff that touches no scripts/ file). pnpm check:merge-driver fails, 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-test fail on macOS 25.5.0 (Darwin), pnpm 10.31.0:

  x a gate whose RUNNER is not installed refuses the same way
  x   ...naming the command the shell could not find, in the diagnosis
x self-test failed -- 1 failure(s) (cases and floor).

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() in scripts/check-regen-pending.mjs recognises a missing runner two ways. On macOS neither fires.

Leg 1 — the shell line. The matcher is anchored on a line-numbered spelling:

const runner = text.match(/^(?:sh|bash|dash|zsh): (?:line )?\d+: ([^:\n]+): (?:command )?not found$/m);

The \d+: segment is required. macOS /bin/sh omits it entirely — measured, sh -c 'os-regen-fixture-absent-runner --check':

sh: os-regen-fixture-absent-runner: command not found

The comment above the regex names the two spellings it was written from (sh: 1: tsx: not found from dash, bash: line 1: tsx: command not found); the bare macOS spelling is a third one.

Leg 2 — the exit-code fallback. || exitCode === 127 is meant to catch exactly that. It does not fire either, because the code the gate sees comes from pnpm -s run, not from the shell. Measured with a throwaway package whose only script is the absent command:

$ pnpm -s run g
sh: os-regen-fixture-absent-runner: command not found
pnpm exit=1

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

  1. pnpm check:merge-driver cannot 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.
  2. The field direction is worse than the self-test. With both legs missing, a real missing-runner case classifies as 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 the PREREQUISITE NOT MET grading 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/sh writes none), and stop reading the runner-missing signal off a raw 127 alone when the immediate child is pnpm, 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 MET grading these cases pin), #16007 (a different residue in the same self-test), #16137 (where this was found).

Activity

  1. self-assigned this
    on Sep 12, 2026
  2. hotlong commented on Sep 12, 2026

    @hotlong
    ContributorAuthor

    Claim: PM session 6679d191-11f4-465b-b322-0e0409d76793 (seat hotlong), dispatched to an os-dev worktree off origin/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.

  3. hotlong commented on Sep 12, 2026

    @hotlong
    ContributorAuthor

    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."
    ]
    }

  4. hotlong commented on Sep 12, 2026

    @hotlong
    ContributorAuthor

    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 === 127 check 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.

  5. added 2 commits that reference this issue on Sep 17, 2026
    7ef2965
    689d606
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions