fix: resolve the Python interpreter by running one, not by looking up a name - #8
Conversation
…ng up a name Windows ships a Microsoft Store stub that answers to `python3` and exits 49 without running anything. `command -v python3` is satisfied by it, so `bin/arc-open` decided an interpreter was present and then handed every python-backed gate a 49. Measured on two Windows machines (2026-08-25, [거울/트리아지]): the gate suite scored **34/48 on both**, and on the second machine a working `python` 3.11.15 was on PATH the entire time — the only blocking name was `python3`. The failures printed as `✗ expected 0, got 49`, which reads like a verdict and is the absence of a measurement: on Windows these gates were not failing, they were not running. Reproduced off Windows by putting a stub named `python3` first on PATH — the suite scores the same 34/48, so this is now testable on any platform rather than on a claim about someone else's laptop. bin/_pybin.sh resolves an interpreter by executing each candidate (`-c 'import sys'`) in order: $YEOUL_PYTHON, python3, python, py -3. The nine scripts that shell out to Python source it and use $PY. Both `command -v python3` name-checks are gone; there is no longer any place where existence stands in for execution. When no candidate runs, the run stops with exit 127 and a message naming what was tried and why a name-check would have passed. It does not skip the step — a skipped check that reads as a pass is how this stayed invisible. tests/test_gates.sh 52/52, including four checks that plant a stub: the stub is rejected, a working interpreter under another name is found (the second machine's configuration), resolution fails when every candidate is stubbed, and a script then stops with a named cause instead of continuing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The PY-02 check located a working interpreter with `command -v python3`, which hands back the stub when the suite itself runs under a stubbed PATH — a name lookup used to find a real interpreter, inside the test for that exact bug. It now runs each candidate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
What the green covers (from the run log):
🔴 The bash suite still does not run in Windows CI, so this fix is not exercised on Windows by The three interpreter configurations, measured locally:
|
…it resolved Two changes aimed at a Windows failure report (50/52 on one machine, 52/52 on another with the same interpreter layout), neither of which assumes what the two failures are. The PY-02 check built its working-interpreter-under-another-name with `ln -sf`. That is the only symlink in the suite, and MSYS/Git Bash copies the target instead of linking unless winsymlinks is set — copying a Windows python.exe yields a broken standalone, so the check could fail for a reason unrelated to what it tests. It now writes a wrapper script, which behaves the same everywhere. The suite also never printed which interpreter it resolved to. A failure report from another machine therefore cannot distinguish 'resolved a different interpreter' from 'this check does not work there' — the run now prints `resolved: <cmd> (<version> <platform>)`. 52/52 locally in all three interpreter configurations. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reported from a Windows machine: 50/52, where a second machine with the same interpreter layout scored 52/52. Both failures were the two PY-03 checks, and both were this test's fault. PY-03 stubs the interpreter names and then asserts that nothing can run. It never checked that. $YEOUL_PYTHON is tried before any PATH name and is not a PATH name at all, and on Windows the .exe forms are separate files — so an interpreter could survive the stubbing, the resolver would correctly succeed, and the checks would report the product as broken. Reproduced locally by running the suite with YEOUL_PYTHON set: 50/52, the same score, with the same two checks red. The precondition is now measured: the stub set covers the .exe forms, the subshells unset YEOUL_PYTHON, and if an interpreter still survives, both checks report inconclusive and fail the run rather than passing or blaming the product — neither verdict would mean anything there. A precondition that is assumed instead of measured is the defect this suite exists to catch. 52/52 in all four configurations: normal, stub-first, stub plus working python, and YEOUL_PYTHON set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two Windows machines reported different results with the same resolved name and the same version. The interpreter each had picked was a different venv sitting ahead on PATH — invisible in a line that printed only name and version, which is what made the difference hard to see. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…reporting its own result
Measured on Windows 2026-08-25 by [거울/트리아지], on two machines. Two gate checks failed with
`could not run the substance checker`, and the cause was the last line of the checker rather than
any judgement it made: having finished judging, it wrote `code + TAB + answer` back through
`sys.stdout`, whose encoding is the console code page. On cp949 an em-dash in the answer raised
UnicodeEncodeError, the process died, stdout came back empty, and arc-close correctly refused ("an
unmeasured field is not a passing field"). A genuine answer was rejected, and nothing in the
refusal pointed at encoding.
The judge had already reached its verdict. Only the channel carrying it back was broken.
The other machine passed, and that green was a coincidence: cp1252 contains the em-dash, cp949 does
not. Neither contains Hangul, and our summaries are written in Korean — so both machines carry this
defect and only one happened to show it. The suite could not see it either, because every test
answer was ASCII apart from a single em-dash: the test corpus shared the code's blind spot.
- `emit()` writes UTF-8 bytes to `sys.stdout.buffer`, so the verdict channel does not depend on
where it is read. Not a stream reconfigure with errors="replace": that keeps the code alive and
hands back an answer full of `?`, and the gate would then judge, report and quote mangled
evidence. A test asserts the answer round-trips intact, and fails against exactly that approach.
- `_pybin.sh` pins PYTHONUTF8/PYTHONIOENCODING for every gate script. The MCP wrapper already did
this for the tools it launches; the shell scripts called Python directly and bypassed it, so
index-append, arc-prereg and status had the same exposure on any non-UTF-8 console.
- The checker's `--selftest` now echoes a non-ASCII probe through `emit()` — the same function the
real call uses. It scored 27/27 on the machine where every real call carrying an em-dash was
dying, because it never wrote an answer back. A control that does not travel the path it vouches
for vouches for nothing. Its failure report writes ASCII to stderr: the first version used
`%r` of the exception, which contains the offending character, so the diagnostic died reporting
the fault it exists to report.
tests/test_gates.sh 61/61. The encoding checks run against `ascii`, `cp949` and `cp1252` — the two
real code pages, not one synthetic stand-in — on a payload built to be lethal to both: U+2014 is
absent from cp949, U+AC00 from cp1252. With the fix reverted all nine go red on both.
Internal and OSS copies of substance_check.py remain byte-identical.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A literal Hangul sample is a personalization leak in an English-only repo and the pre-publish guard rejects it, correctly. Rewriting the block put the literal characters back; they are escapes again. Verified the payload still carries the real bytes and still turns all nine ENC checks red when the fix is reverted, so escaping did not make it toothless. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Scope grew: this PR now also fixes what it revealedRunning this branch on two Windows machines surfaced two failures that are not caused by it — The checker was dying while reporting its own result. Having finished judging, it wrote The second machine passed, and that green was a coincidence: cp1252 contains the em-dash, The fix writes UTF-8 bytes to Also: 61/61, with the encoding checks parametrised over Happy to split the encoding fix into its own PR if you would rather review them apart. My |
On Windows the Yeoul gate suite does not run at all. It has not been failing — it has been not
running, while printing
✗ expected 0, got 49, which reads like a verdict.Windows ships a Microsoft Store stub that answers to
python3and exits 49 without executinganything.
command -v python3is satisfied by it, sobin/arc-openconcluded an interpreter waspresent and handed every python-backed gate a 49.
Existence is not execution. This is the same class as #7 — a signal asserting something the code
never checked — and it was sitting one line from where that PR worked.
The measurement
[거울/트리아지] ran both PRs on two Windows machines (2026-08-25):
pythonpython3pyLaptop B had a perfectly good interpreter on PATH the whole time. The only blocking name was
python3. Credit for the finding and the numbers is theirs; I have not run anything on Windows.Reproduced off Windows by putting a stub named
python3first on PATH — same 34/48. Bothmachine configurations reproduce, including laptop B's (stub
python3+ workingpython). This isnow testable on any platform instead of resting on a claim about someone else's laptop.
The change
bin/_pybin.shresolves an interpreter by executing each candidate (-c 'import sys') inorder:
$YEOUL_PYTHON,python3,python,py -3. The nine scripts that shell out to Pythonsource it and use
$PY. Bothcommand -v python3name-checks are gone — there is no longer anyplace where a name standing in for a working interpreter.
When nothing runs, the run stops (exit 127) with a message naming what was tried and why a
name-check would have passed. It does not skip the step: a skipped check that reads as a pass is
exactly how this stayed invisible for so long.
Evidence
tests/test_gates.sh52/52, and in all three interpreter configurations:python3firstpython3+ workingpython(laptop B)Four new checks plant a stub: the stub is rejected; a working interpreter under another name is
found; resolution fails when every candidate is stubbed; and a script then stops with a named cause.
Reverting the resolver to name-based lookup turns all four red — including
bin/statusexiting0 while Python is entirely broken, which is the silent pass this fixes.
Also verified: the six #7 fixes remain non-vacuous (6/6) against the pre-#7 base,
mcp/tests/test_run_contract.py8/8, pre-publish guard clean over 41 files.Not claimed
rather than re-measured. What I verified is that both configurations reproduce off Windows and
that the repair fixes them there.
machine; it does not add Windows coverage to CI.
decided here.
🤖 Generated with Claude Code