Three wrappers commit to Python 3.7+ and each says why:
host-setup/linux/install-skills.sh:76 and host-setup/windows/install-skills.ps1:50, "since the installer needs from __future__ import annotations"
scripts/skills_install.sh:10, "Pick the first candidate that is actually Python 3.7+"
spec/host-tools.json's python3 entry records the reasoning: that path "must run on whatever a host already has before this floor's own toolchain exists to install one", which is why it sits below the fleet's 3.13 target rather than drifting from it.
scripts/skills_install.py imports scripts/build_dist.py at module scope. So the floor binds build_dist.py and everything it imports at module scope, transitively, and nothing checks that.
How it surfaced
On #1430 the first draft decorated a new accessor in build_dist.py with functools.cache, which is 3.9+. Every gate passed: ruff (whose target-version is py313), mypy (python_version = "3.13"), the full unit suite, build_dist.py --check, prose_lint.py, repo_gate.py, spec/validate.py. On a real CPython 3.8.20:
functools.cache variant import build_dist: BROKE -> module functools has no attribute cache
lru_cache variant import build_dist: OK import skills_install: OK
An AST scan confirms build_dist.py carried no other construct above the floor before that change, so the file was 3.7-clean and one decorator took it out. It was caught by a local review pass raising the floor as an open question, not by anything mechanical, and ruff actively pushes the other way: UP033 rewrites lru_cache(maxsize=None) back to functools.cache, so the correct form needs a noqa to survive.
Why a gate rather than care
The failure is silent on every machine that develops this repository, since they all run 3.13. It appears only on the bootstrap path, on a host that by definition has no newer interpreter yet, and it is an AttributeError at import rather than a degraded feature. Nobody who could notice it runs the code that breaks.
Possible shapes, not a decision
- Compile the module set the install path reaches against the floor.
uv python install 3.8 fetches a real interpreter in about two seconds, and a python3.8 -c "import skills_install" is the whole check. Cheapest and closest to the real failure.
- An AST scan for constructs above the floor, in
scripts/tests/ beside the existing bootstrap tests. No interpreter needed, but it has to enumerate what to look for and will always trail the language.
- A per-file
ruff override pinning target-version = "py37" for just this import set, which would make UP033 and its siblings stop pushing the wrong way rather than needing a noqa each time.
Worth settling at the same time which files are actually in the set, since today it is discoverable only by reading imports: skills_install.py, build_dist.py, and whatever either adds later.
Filed from #1430, where the near-miss happened.
Three wrappers commit to Python 3.7+ and each says why:
host-setup/linux/install-skills.sh:76andhost-setup/windows/install-skills.ps1:50, "since the installer needsfrom __future__ import annotations"scripts/skills_install.sh:10, "Pick the first candidate that is actually Python 3.7+"spec/host-tools.json'spython3entry records the reasoning: that path "must run on whatever a host already has before this floor's own toolchain exists to install one", which is why it sits below the fleet's 3.13 target rather than drifting from it.scripts/skills_install.pyimportsscripts/build_dist.pyat module scope. So the floor bindsbuild_dist.pyand everything it imports at module scope, transitively, and nothing checks that.How it surfaced
On #1430 the first draft decorated a new accessor in
build_dist.pywithfunctools.cache, which is 3.9+. Every gate passed:ruff(whosetarget-versionispy313),mypy(python_version = "3.13"), the full unit suite,build_dist.py --check,prose_lint.py,repo_gate.py,spec/validate.py. On a real CPython 3.8.20:An AST scan confirms
build_dist.pycarried no other construct above the floor before that change, so the file was 3.7-clean and one decorator took it out. It was caught by a local review pass raising the floor as an open question, not by anything mechanical, and ruff actively pushes the other way:UP033rewriteslru_cache(maxsize=None)back tofunctools.cache, so the correct form needs anoqato survive.Why a gate rather than care
The failure is silent on every machine that develops this repository, since they all run 3.13. It appears only on the bootstrap path, on a host that by definition has no newer interpreter yet, and it is an
AttributeErrorat import rather than a degraded feature. Nobody who could notice it runs the code that breaks.Possible shapes, not a decision
uv python install 3.8fetches a real interpreter in about two seconds, and apython3.8 -c "import skills_install"is the whole check. Cheapest and closest to the real failure.scripts/tests/beside the existing bootstrap tests. No interpreter needed, but it has to enumerate what to look for and will always trail the language.ruffoverride pinningtarget-version = "py37"for just this import set, which would makeUP033and its siblings stop pushing the wrong way rather than needing anoqaeach time.Worth settling at the same time which files are actually in the set, since today it is discoverable only by reading imports:
skills_install.py,build_dist.py, and whatever either adds later.Filed from #1430, where the near-miss happened.