The _hermes-python prologue compared uv.lock / pyproject.toml / pm/lock.json
against facts.json with `-nt`. facts.json is only rewritten on a real sync,
so a checkout that rewrites an input without changing it left the input
newer forever: every run from an activated shell re-sourced activate
(~370ms instead of ~13ms).
pm now records, after every successful full-closure install (no-op syncs
included), a stamp per input under installs/<key>/inputs carrying the exact
mtime the install was verified against, snapshotted before installing so
an input edited mid-install still reads as stale. The prologue re-activates
when any input's mtime differs from its stamp in either direction (branch
switches can move it backwards), when an input is missing, or when there
are no stamps. It globs the stamp dir, so pm owns the only input list.
Also read pyvenv.cfg as utf-8-sig (footgun lane, same file).
Repo scripts assume the PM-activated environment, so running one without
activation fails much later with a confusing ImportError. Add the two halves
covering both invocation paths:
- scripts/_activation.py: require_activation() exits immediately, naming the
exact command for the caller's shell (source ./activate on POSIX,
. .\activate.ps1 on a native Windows host), before any heavy import.
- scripts/_hermes-python: the POSIX shebang target. `#!/usr/bin/env -S bash -c
'...'` hands itself the target path through bash -c's $0, sources activate,
then execs the interpreter on the same file -- so tracebacks and __file__
still point at the real script and ./scripts/foo.py works from any cwd with
no manual source.
__HERMES_ACTIVATED changes from a bare "1" to the installed-state file the
environment was composed against, so one value carries activation, which
checkout activated it, and a staleness stamp. The prologue compares that file
against uv.lock / pyproject.toml / pm/lock.json with the `-nt` builtin -- no
process spawn -- and re-activates once when the inherited environment predates
its inputs. pm rewrites that file only on a real sync, so the check settles
back to current rather than re-syncing on every run.
A legacy "1" keeps working: require_activation() tests non-emptiness, and the
prologue's [ -e ] fails on it, so it activates once and upgrades.