What happens
On a stock macOS box with Homebrew Python installed alongside Apple's Command Line Tools, every ringer.py invocation fails before doing anything:
$ ./ringer.py lint manifest.json
ringer requires Python 3.11+ (tomllib); found 3.9.6 at /Library/Developer/CommandLineTools/usr/bin/python3
Python 3.12 was installed and on PATH the whole time. The gate at ringer.py:24 is correct; the problem is which interpreter reaches it.
Why
The shebang is #!/usr/bin/env python3, and on macOS python3 commonly resolves to Apple's bundled 3.9 (/Library/Developer/CommandLineTools/usr/bin/python3) rather than /opt/homebrew/bin/python3.12. Homebrew installs versioned names (python3.12, python3.11) but does not necessarily take over bare python3.
Workaround while debugging: /opt/homebrew/bin/python3.12 ringer.py …, which works fine — so this is purely interpreter selection, not a compatibility problem.
Why changing the shebang alone isn't enough
Pinning the shebang to python3.12 fixes ./ringer.py but leaves python3 ringer.py broken, and that second form is what wrappers, scripts, and CI tend to use. It also pins to one minor version, so a later brew upgrade python reintroduces the failure.
Suggested fix
Re-exec into a newer interpreter from inside the existing gate. os, shutil, and sys are already imported above it, so this needs no new imports, and the file parses cleanly on 3.9 (it already reaches line 24 to print the current message), so the shim is reachable.
if sys.version_info < (3, 11):
# tomllib is 3.11+. The shebang resolves `python3` through PATH, which on
# macOS is usually Apple's bundled 3.9 — so re-exec under a newer
# interpreter when one is installed rather than failing outright. This also
# covers `python3 ringer.py`, which a shebang change alone would not fix.
# Opt out with RINGER_NO_REEXEC=1; RINGER_REEXEC caps this at a single hop.
_newer = next(
(
_p
for _p in (
shutil.which(_c)
for _c in ("python3.14", "python3.13", "python3.12", "python3.11")
)
if _p
),
None,
)
if (
_newer
and os.environ.get("RINGER_REEXEC") != "1"
and os.environ.get("RINGER_NO_REEXEC") != "1"
):
os.execve(
_newer,
[_newer, os.path.abspath(__file__), *sys.argv[1:]],
{**os.environ, "RINGER_REEXEC": "1"},
)
raise SystemExit(
f"ringer requires Python 3.11+ (tomllib); found {sys.version.split()[0]} at {sys.executable}\n"
+ (
f"Re-exec suppressed by RINGER_NO_REEXEC/RINGER_REEXEC; {_newer} would have worked."
if _newer
else "No python3.11+ found on PATH to re-exec into. Install one "
"(e.g. brew install python@3.12), or invoke ringer with an explicit interpreter."
)
)
Searching newest-to-oldest means a future brew upgrade doesn't break it. RINGER_REEXEC caps it at one hop so a misconfigured environment can't spin. RINGER_NO_REEXEC=1 opts out for anyone who wants the old hard-fail.
Verified locally
| Invocation |
Before |
After |
./ringer.py --help |
fails |
re-execs, works |
python3 ringer.py --help |
fails |
re-execs, works |
python3.12 ringer.py --help |
works |
works, no re-exec |
python3.11 ringer.py --help |
works |
works, no re-exec |
RINGER_NO_REEXEC=1 python3 ringer.py |
n/a |
friendly error, no traceback |
RINGER_REEXEC=1 python3 ringer.py |
n/a |
exits in 0.28s — single hop, no loop |
./ringer.py lint <manifest> |
fails |
lint: clean — tomllib genuinely loads |
Also confirmed a real 3-worker run (codex / grok / opencode) completes end-to-end through the plain shebang afterwards.
None of the four baked-in invariants are touched — the change sits entirely above worker invocation and doesn't affect stdin handling, sandbox args, verification, or log contents.
Note on the test suite
python -m unittest discover -s tests gives 132 tests, 1 failure + 1 error, identical with and without this change — so no regression from it. Both were already failing on a clean checkout and look unrelated: test_design_reference.py errors on an absolute path baked in from a different machine, and test_scoreboard_page.py asserts a hardcoded "Generated July 6, 2026" that rots with the calendar. Happy to file those separately if useful.
Environment
- macOS (Darwin 24.6.0), Apple Silicon
/usr/bin/env python3 → /Library/Developer/CommandLineTools/usr/bin/python3 (3.9.6)
/opt/homebrew/bin/python3.12 (3.12.13) and python3.11 (3.11.15) both present
- ringer at
745905f
I have this committed on a local branch and tested, but only have READ on this repo — happy to open a PR from a fork if you'd prefer that to implementing it directly.
🤖 Generated with Claude Code
What happens
On a stock macOS box with Homebrew Python installed alongside Apple's Command Line Tools, every
ringer.pyinvocation fails before doing anything:Python 3.12 was installed and on
PATHthe whole time. The gate atringer.py:24is correct; the problem is which interpreter reaches it.Why
The shebang is
#!/usr/bin/env python3, and on macOSpython3commonly resolves to Apple's bundled 3.9 (/Library/Developer/CommandLineTools/usr/bin/python3) rather than/opt/homebrew/bin/python3.12. Homebrew installs versioned names (python3.12,python3.11) but does not necessarily take over barepython3.Workaround while debugging:
/opt/homebrew/bin/python3.12 ringer.py …, which works fine — so this is purely interpreter selection, not a compatibility problem.Why changing the shebang alone isn't enough
Pinning the shebang to
python3.12fixes./ringer.pybut leavespython3 ringer.pybroken, and that second form is what wrappers, scripts, and CI tend to use. It also pins to one minor version, so a laterbrew upgrade pythonreintroduces the failure.Suggested fix
Re-exec into a newer interpreter from inside the existing gate.
os,shutil, andsysare already imported above it, so this needs no new imports, and the file parses cleanly on 3.9 (it already reaches line 24 to print the current message), so the shim is reachable.Searching newest-to-oldest means a future
brew upgradedoesn't break it.RINGER_REEXECcaps it at one hop so a misconfigured environment can't spin.RINGER_NO_REEXEC=1opts out for anyone who wants the old hard-fail.Verified locally
./ringer.py --helppython3 ringer.py --helppython3.12 ringer.py --helppython3.11 ringer.py --helpRINGER_NO_REEXEC=1 python3 ringer.pyRINGER_REEXEC=1 python3 ringer.py./ringer.py lint <manifest>lint: clean— tomllib genuinely loadsAlso confirmed a real 3-worker run (codex / grok / opencode) completes end-to-end through the plain shebang afterwards.
None of the four baked-in invariants are touched — the change sits entirely above worker invocation and doesn't affect stdin handling, sandbox args, verification, or log contents.
Note on the test suite
python -m unittest discover -s testsgives 132 tests, 1 failure + 1 error, identical with and without this change — so no regression from it. Both were already failing on a clean checkout and look unrelated:test_design_reference.pyerrors on an absolute path baked in from a different machine, andtest_scoreboard_page.pyasserts a hardcoded"Generated July 6, 2026"that rots with the calendar. Happy to file those separately if useful.Environment
/usr/bin/env python3→/Library/Developer/CommandLineTools/usr/bin/python3(3.9.6)/opt/homebrew/bin/python3.12(3.12.13) andpython3.11(3.11.15) both present745905fI have this committed on a local branch and tested, but only have READ on this repo — happy to open a PR from a fork if you'd prefer that to implementing it directly.
🤖 Generated with Claude Code