Skip to content

fix: resolve the Python interpreter by running one, not by looking up a name - #8

Merged
bhyi4 merged 7 commits into
mainfrom
fix/resolve-python-by-running-it
Aug 25, 2026
Merged

fix: resolve the Python interpreter by running one, not by looking up a name#8
bhyi4 merged 7 commits into
mainfrom
fix/resolve-python-by-running-it

Conversation

@bhyi4

@bhyi4 bhyi4 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

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 python3 and exits 49 without executing
anything. command -v python3 is satisfied by it, so bin/arc-open concluded an interpreter was
present 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):

machine python python3 py gate suite
laptop A stub stub 3.14.3 34/48
laptop B 3.11.15 (works) stub 3.12.10 34/48

Laptop 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 python3 first on PATH — same 34/48. Both
machine configurations reproduce, including laptop B's (stub python3 + working python). This is
now testable on any platform instead of resting on a claim about someone else's laptop.

The change

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 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.sh 52/52, and in all three interpreter configurations:

configuration before after
normal 48/48 52/52
stub python3 first 34/48 52/52
stub python3 + working python (laptop B) 34/48 52/52

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/status exiting
0 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.py 8/8, pre-publish guard clean over 41 files.

Not claimed

  • I have not run anything on Windows. The Windows numbers are triage's measurement, reused
    rather than re-measured. What I verified is that both configurations reproduce off Windows and
    that the repair fixes them there.
  • No real CP949 code-page run; the hostile-locale coverage is still the ASCII band.
  • The bash suite still does not run in Windows CI. This makes the suite able to run on a Windows
    machine; it does not add Windows coverage to CI.
  • Whether every Yeoul shell PR should be run on Windows from now on is a separate question, not
    decided here.

🤖 Generated with Claude Code

Mother Seara and others added 2 commits August 25, 2026 11:00
…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>
@bhyi4

bhyi4 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

What the green covers (from the run log):

job what ran
test (ubuntu-latest) 52/52 gate checks passed · ✓ clean (41 files scanned)
test (macos-latest) 52/52 gate checks passed · ✓ clean (41 files scanned)
mcp (ubuntu-latest) · mcp (windows-latest) 8/8 contract checks

🔴 The bash suite still does not run in Windows CI, so this fix is not exercised on Windows by
these jobs
— the four stub checks that prove it run on Linux and macOS. That is deliberate: the
stub is reproduced synthetically, which is what makes it testable at all. Confirming it on a real
Windows machine is still a manual run.

The three interpreter configurations, measured locally:

configuration before after
normal 48/48 52/52
stub python3 first 34/48 52/52
stub python3 + working python 34/48 52/52

Mother Seara and others added 5 commits August 25, 2026 11:31
…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>
@bhyi4

bhyi4 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Scope grew: this PR now also fixes what it revealed

Running this branch on two Windows machines surfaced two failures that are not caused by it —
they were red on main too, but there Python was dead, so they were the absence of a measurement.
Here Python actually runs and they are still red, which makes them genuine verdicts for the first
time. The cause turned out to be worth fixing in the same change, because it is the same shape.

The checker was dying while reporting its own result. 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 reached its verdict; only
the channel carrying it back was broken.

The second machine passed, and that green was a coincidence: cp1252 contains the em-dash,
cp949 does not. Neither contains Hangul, and these summaries are written in Korean — so both
machines carry the defect and only one happened to show it. The suite could not see it either,
because every test answer was ASCII apart from one em-dash: the test corpus shared the code's blind
spot. Credit for both the diagnosis and that argument goes to the reviewer who ran the machines.

The fix writes UTF-8 bytes to stdout.buffer, so the verdict channel does not depend on where
it is read. Deliberately 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. [ENC-02] asserts the answer round-trips intact and fails against exactly that approach.

Also: --selftest now echoes a non-ASCII probe through the same emit() 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. And _pybin.sh pins PYTHONUTF8/PYTHONIOENCODING for every gate script;
the MCP wrapper already did this, but the shell scripts called Python directly and bypassed it, so
index-append, arc-prereg and status had the same exposure.

61/61, with the encoding checks parametrised over 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 absent
from cp949, U+AC00 from cp1252). Reverting the fix turns all nine red on both. Sealed gate numbers
unchanged: 28/28 evasive caught, 0/39 false rejection, internal and OSS. Both copies of
substance_check.py remain byte-identical.

Happy to split the encoding fix into its own PR if you would rather review them apart. My
preference is to keep them together: it is a defect this branch revealed, and without this branch
the test that catches it cannot run at all.

@bhyi4
bhyi4 merged commit 1c1887e into main Aug 25, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant