Skip to content

Commit d9e1ad6

Browse files
Jammy2211Jammy2211
authored andcommitted
session: regenerate the SessionStart hook from the updated canonical
PyAutoMind's policy/session_start_hook.sh gained leg 3b (the uv tool-env repair). This copy is generated from it and must stay byte-identical, or repos_sync.py --check reports drift and firewall_gate.yml goes red.
1 parent b78aaf8 commit d9e1ad6

1 file changed

Lines changed: 76 additions & 1 deletion

File tree

.claude/hooks/session-start.sh

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
# resolving PATH without this session's env (a subprocess with a scrubbed
2323
# environment, a `#!/usr/bin/env python3` script) also gets 3.12;
2424
# 3. the uv-managed tools rebuilt on 3.12 — mypy and flake8 read the
25-
# interpreter's version, so on 3.11 they judged code against 3.11 rules.
25+
# interpreter's version, so on 3.11 they judged code against 3.11 rules —
26+
# and then repaired, because (2) is what breaks each tool env's own
27+
# `bin/python`, and a rebuild cannot fix a link it is handed.
2628
#
2729
# What it deliberately does NOT touch: the update-alternatives links under
2830
# /usr/bin. Scripts with a literal `#!/usr/bin/python3` shebang follow those,
@@ -344,6 +346,69 @@ retool_uv_tools() {
344346
done
345347
}
346348

349+
# 3b. The link uv's rebuild cannot fix from inside.
350+
#
351+
# uv creates each tool env with `bin/python` as a SYMLINK to whatever `python3`
352+
# was at install time — here `/usr/local/bin/python3`, which leg 2 has already
353+
# replaced with a wrapper that `exec`s the session venv. Every tool env's python
354+
# then resolves its prefix to the VENV: `sys.prefix` is the venv, the tool's own
355+
# site-packages never reaches `sys.path`, and the console script dies with
356+
# `ModuleNotFoundError: No module named 'flake8'` — with flake8 sitting
357+
# installed two directories away.
358+
#
359+
# TWO paths reach that state, which is why this runs after leg 3 rather than
360+
# only on the envs leg 3 rebuilt: leg 3 rebuilds a 3.11 tool and hands the new
361+
# env the hijacked path, and leg 2 separately breaks every PRE-EXISTING tool env
362+
# that already pointed there and that leg 3 skips (`is_py312 … && continue`).
363+
# Running here covers both, because leg 2 runs before leg 3.
364+
#
365+
# Measured 2026-08-27, post-bootstrap: mypy, flake8, black, poetry and pyright
366+
# all dead this way; `ruff` survived (a native binary) and `pytest` survived
367+
# (leg 2b points its shim straight at the venv, which has pytest). The
368+
# bootstrap's `--check` called every one of them "3.12 OK", because the
369+
# interpreter they reach IS 3.12 — it is simply the wrong one. A session then
370+
# lints clean by not linting at all, and CI is the thing that finds out.
371+
#
372+
# The fix is one link: a venv's `bin/python` must resolve to a BASE interpreter,
373+
# never to a path this hook hijacks.
374+
#
375+
# This lived in `scripts/session_bootstrap.sh` for one pass, which is the wrong
376+
# home. The bootstrap is what a MULTI-repo session runs; a SINGLE-repo session
377+
# registers this hook and never calls the bootstrap, so it got leg 2's breakage
378+
# and none of the repair.
379+
repair_uv_tools() {
380+
local tools_dir base tool link prefix
381+
base="$("$VENV/bin/python" -c 'import sys, os; print(os.path.join(sys.base_prefix, "bin", "python3.12"))' 2>/dev/null)" || base=""
382+
[ -x "$base" ] || base="$(command -v python3.12 2>/dev/null)" || base=""
383+
[ -x "$base" ] || return 0
384+
tools_dir="${PYAUTO_UV_TOOLS_DIR:-$(uv tool dir 2>/dev/null || echo "$HOME/.local/share/uv/tools")}"
385+
[ -d "$tools_dir" ] || return 0
386+
for tool in "$tools_dir"/*/; do
387+
link="${tool}bin/python"
388+
[ -L "$link" ] || continue
389+
# Ask the interpreter where it thinks it lives, rather than tracing the
390+
# link: `/usr/local/bin/python3` is a WRAPPER SCRIPT (a symlink there
391+
# would lose the venv — leg 2's whole note), so `readlink -f` stops at
392+
# the wrapper and reports nothing about the venv behind it. sys.prefix
393+
# is the outcome; anything else is the mechanism.
394+
prefix="$("$link" -c 'import sys; print(sys.prefix)' 2>/dev/null)" || continue
395+
[ -n "$prefix" ] || continue
396+
[ "$prefix" = "${tool%/}" ] && continue # resolves to its own env: correct
397+
# Non-fatal like every other leg: an unwritable tools dir is a warning,
398+
# never a failed session start.
399+
ln -sfn "$base" "$link" || {
400+
log "WARNING: could not repoint $(basename "${tool%/}") at $base"
401+
continue
402+
}
403+
prefix="$("$link" -c 'import sys; print(sys.prefix)' 2>/dev/null)" || prefix=""
404+
if [ "$prefix" = "${tool%/}" ]; then
405+
log "repointed $(basename "${tool%/}") at $base (it resolved into $VENV, not its own env)"
406+
else
407+
log "WARNING: $(basename "${tool%/}") still resolves to ${prefix:-nothing} — it will not run"
408+
fi
409+
done
410+
}
411+
347412
# 4. Honest git history.
348413
#
349414
# A remote session clones shallow. `git merge-base --is-ancestor` then LIES
@@ -461,6 +526,15 @@ if [ "${PYAUTO_SESSION_DEFINE_ONLY:-}" = "1" ]; then
461526
return 0 2>/dev/null || exit 0
462527
fi
463528

529+
# Run leg 3b on its own, without a session start. The door
530+
# `scripts/session_bootstrap.sh` knocks on after it has run every repo's hook —
531+
# a subprocess rather than a source, so this script's `set -euo pipefail` never
532+
# leaks into a caller that is contractually "a bootstrap, never a gate".
533+
if [ "${1:-}" = "--repair-uv-tools" ]; then
534+
repair_uv_tools
535+
exit 0
536+
fi
537+
464538
ensure_full_clone
465539
install_workspace_settings
466540

@@ -475,6 +549,7 @@ if ensure_venv; then
475549
ensure_repo_extras
476550
point_system_default
477551
retool_uv_tools
552+
repair_uv_tools
478553
point_pytest_at_venv
479554
point_venv_scripts_at_venv
480555
# Every repo in the session registers this hook, so the second copy must not

0 commit comments

Comments
 (0)