hermes_cli.runtime_paths (venv generations, selection, activation) moves to
pm.environments, and gains venv_bin_dir / venv_python / project_python. Every
in-tree caller asks pm for an interpreter now; pm no longer reaches back into
hermes_cli for its own environment layout (pm.packages, pm.extras, pm.ensure,
pm.paths imported hermes_cli.runtime_paths). The three open-coded
"Scripts/python.exe or bin/python" ladders in pm collapse onto venv_python.
hermes_constants.venv_python_path / venv_bin_dir and hermes_cli.runtime_paths
stay as frozen-updater-surface shims only (tests/compat/old_updater_surface.json).
To keep the boot path light, pm/__init__ resolves its facade lazily (PEP 562)
and pm.registry loads the built-in package definitions on first read instead of
at import: `import hermes_bootstrap` now loads pm + pm.environments only (25ms,
was 37ms with the eager facade dragging in the downloader). The stripped-payload
fixtures that ship only pre-import files keep working for the same reason.
Also restores two frozen-surface re-exports the F401 sweep dropped
(banner._github_compare_behind, cua_backend.resolve_cua_driver_cmd).
source_completion.py re-execs itself into PM's selected interpreter before
touching any product, and the passthrough argv only carried --desktop. So
the startup heal's --finish-update was dropped, and the prepared invocation
fell through to the install tail: an update reported itself as an install.
✓ Install complete! [main @ c4056e3d6b]
Every flag that decides WHICH tail runs has to survive that boundary.
Verified with a stub that captures the bootstrap command instead of running
it: --finish-update present in the re-exec, --desktop still passed, and
without the flag the install path is unchanged. File compiles.
prepare_launch is the startup heal: it detects a stale PM generation
(pm.venv_is_current) and completes the update. But it only synced the
dependency generation and published the launchers -- so an install that
came through it landed in a different state than one that came through a
normal update or the installer.
The tail they share is complete_source_checkout (source_completion.py):
publish launchers, build the products, run the post-build maintenance.
"Its" own docstring: a fresh install and a finished update must land in the
same state, one implementation and two callers. The startup heal was a
third path taking a subset, and the e2e verifier caught exactly that -- it
requires ui-tui/dist fresh, which only build_update_products produces.
So drive the shared tail from the heal, the same way the installers do:
hand source_completion.py THIS interpreter (pre-sync, so it stays a
stdlib-only import for the old tree) and let it re-exec into PM's selected
python before touching any product. A failed tail is loud, matching
build_update_products' "a failed product aborts the update".
Verified: both files compile; the bare-3.11 pre-flip import check still
passes (bootstrap / prepare_launch / takeover_child, no third-party).
The installer ladder stopped at node-deps/path/desktop with its own
semantics while an update ran launchers, product builds and post-build
maintenance, so a fresh install and a finished update ended in different
states: after re-running the installer at HEAD the products had no receipts
and the read-only source acceptance failed.
hermes_cli/source_completion.py now owns that tail -- publish launchers,
build the products, run the maintenance -- and update_completion's
_complete_selected calls it, so there is one implementation. install.sh and
install.ps1 keep the bootstrap stages (prerequisites, repository, venv,
python-deps, config) and hand off to it in a single `products` stage;
--include-desktop selects the desktop product inside that stage instead of
adding a second build stage, and `desktop` stays dispatchable via --stage for
external callers.
Windows keeps its installer-owned PATH publication (expose_cli answers
"windows-installer-owned" on Windows) plus the packaged-artifact probe, ACL
grant and shortcuts. The desktop stage no longer pre-syncs wake/voice: pm
lazy-installs them at first use, as the update path does.