Importing hermes_cli on a host whose stdout is not UTF-8 (a cp1252 pipe on
Windows, a latin-1 locale on a Pi) set PYTHONUTF8/PYTHONIOENCODING in
os.environ as a side effect. Every library importer -- gateway.relay, the
compute host, agent.auxiliary_client -- then leaked that into each child it
spawned, which is what
test_library_imports_of_dual_use_entry_modules_stay_side_effect_free
catches on Windows (changed == [PYTHONIOENCODING, PYTHONUTF8]).
The package import now only repairs its own streams and records whether it
had to. hermes_cli.main.main() turns that record into the child-process
hint, so `hermes` still steers its Python children to UTF-8 on such a host.
On Windows the entry-point bootstrap and configure_windows_stdio() already
export both variables; the tool subprocess env builders set them for
children independently.
The test was unmarked, so the OS lanes (`-m "platforms and not
integration"`) deselected it and Windows never ran it. Mark it
platforms("any"): list_os_marked_tests.py lists the file for every lane and
the -m expression now selects the test.
`hermes setup` launched by install.ps1 on PowerShell 5.1/conhost printed
`[35m`/`[0m` literally. hermes_cli.colors and the skins emit raw SGR codes
whenever stdout is a TTY, but no Hermes entry point ever set
ENABLE_VIRTUAL_TERMINAL_PROCESSING (origin/main didn't either; only
pm/cli.py did, for its own progress line), and shells hand native children
a console with VT off.
hermes_bootstrap now opts the stdout/stderr console in at import, before
anything prints and before the venv relaunch (the mode lives on the console
buffer, so the child inherits it). Non-console handles are left alone; a
console that refuses VT gets NO_COLOR so it shows plain text instead of
escape garbage. ctypes only: colorama is merely transitive.
Conflicts:
- hermes_bootstrap.py: main calls install_never_free_environ() in the
apply-on-import block right after the console fixes, where pm-clean runs its
PM block (activation, relaunch, environ writes) instead of
activate_durable_lazy_target(). It now runs ahead of the whole PM block,
main's order, so the glibc < 2.41 guard is in before anything writes
os.environ.
- tests/test_hermes_bootstrap.py: pm-clean dissolved the entry-point class and
dropped its source-reading test; main's new
test_library_imports_of_dual_use_entry_modules_stay_side_effect_free lands
as a module-level function.
tui_gateway.compute_host is also imported by tests, so the module-level
hermes_bootstrap import exported TMPDIR/TMP/TEMP/HERMES_SCRATCH_DIR into the
importer, the same defect as the hermes-tools MCP server. The new test imports
agent.auxiliary_client, gateway.relay and tui_gateway.compute_host in a clean
interpreter and asserts no bootstrap and no env change (red at b278fb79c61c).
Review of the never-free environ wrapper found two defects on glibc < 2.41:
- Lost updates: two threads adding new names each copied the live array and the
later publish dropped the other's names (8x300 names left 411/2400 visible to
C getenv and to children; a concurrent replacement could be undone). One
RLock now covers the read-build-publish of os.putenv and glibc's in-place
shift in os.unsetenv; a fork hook keeps a child from inheriting it held.
- Unbounded leak: every set/del cycle of the same name built a fresh array and
entry string (~1.2 KB/cycle; kanban ticks, the spinner pause and
_restore_env churn names forever). Like glibc 2.41, entry strings are now
cached per NAME=value and new names are appended in place to an array with
spare room; only a full array is replaced (doubling), never freed.
Replacements are also done in place by the wrapper (found via getenv's pointer,
no string reads), so os.putenv raises exactly one os.putenv audit event again,
matching the real function. Comments cover the publish ordering (TSO vs
aarch64) and the residual native-setenv case.
The tui_gateway intermittently died with SIGSEGV (rc=-11) on CI with
session.create (rpc id=1) in flight. Root cause: glibc < 2.41 reallocs the
environ array and frees the old one when setenv adds a NEW name. session.create
turns on gateway prompts (os.environ.update of three new HERMES_* names) while
the picker-prewarm thread is inside getaddrinfo/OpenSSL with the GIL released,
walking environ in C getenv; the freed slots hold mangled tcache pointers, so
the walk segfaults the whole process. The agent build (gateway.run import-time
env, HERMES_SESSION_ID) and the title thread hit the same window.
Reproduced in ubuntu:24.04 (glibc 2.39, the CI runner's) with the real gateway
loop: faulthandler names the picker-cache-prewarm thread in socket.getaddrinfo
as the faulting thread, matching the CI dump thread-for-thread. glibc 2.41+
never frees an environ array, which is why it never reproduced on newer hosts.
hermes_bootstrap (imported first by every entry point) now publishes new names
in a fresh array with one pointer store and never frees an array, the same fix
glibc 2.41 made. Replacing or removing an existing name was already in place.
Inactive on glibc >= 2.41, musl, macOS and Windows.
Every entry point wrapped `import hermes_bootstrap` in
`except ModuleNotFoundError: pass` for a partial update that left the
bootstrap unregistered. It also swallowed a module the bootstrap itself
failed to import, and since the bootstrap now owns PM activation that
silently ran the tree on stale dependencies: exactly how a pre-PM
editable venv hid its unreachable `pm` until it crashed on ruamel.
Re-raise unless the missing module is hermes_bootstrap, at all six entry
points. The stale "only Windows UTF-8 stdio suffers" comments go with it.
An install editable-built from main maps only the top-level packages it
saw then (setuptools' flat-layout finder: no `pm`) and never puts the
checkout on sys.path. After moving that checkout to this tree, `hermes`
died in hermes_cli/__init__.py: `__version__` was evaluated at import
through pm.paths, before anything had put the root on sys.path.
Two layers of the same break:
- hermes_cli.__version__ is served lazily (PEP 562). Shipped updaters
still get it from `from hermes_cli import __version__`; importing the
package no longer needs pm or reads the stamp.
- hermes_bootstrap hardens the import path before its first Hermes
import, not only on the legacy post-swap branch. Without that, its
`from pm.environments import ...` raised ModuleNotFoundError, which
hermes_cli.main swallows, so the launch silently skipped
prepare_launch: no blessed-checkout adoption and no PM sync, and the
tree ran on main's dependencies until it hit ruamel.
Reproduced with a real main editable venv swapped to this tree: exact
user traceback before; after, a stamp-less blessed checkout adopts, PM
syncs, relaunches, and the command answers. The new test drives both
imports through a pre-PM style finder and is red with either half
reverted.
Change-detectors, tautologies, source-reading tests, redundant duplicates,
mock-echo tests and dead/unrunnable tests. Per-test rationale in the lane
ledger (category + reason for every removal).
Both installed racers caught any non-OSError from the Happy Eyeballs core and
re-ran the stock serial ``create_connection``. That branch was untested and
its only effect was to hide a bug in the racer by silently reintroducing the
exact IPv6-first stall the racer exists to remove. OSError (every candidate
failed) still propagates unchanged, identical to the serial original; anything
else now raises. Invariant test drives both racers with a raising core.
Wire the racer #114277 added once, at the seam every Hermes process already
crosses first: ``hermes_bootstrap`` (imported before anything else by
``hermes``, ``hermes-agent``, ``hermes-acp``, ``gateway.run``, ``batch_runner``,
``tui_gateway.entry`` and the slash worker). Installing it from
``init_agent`` was too late for the startup path the report is about — the TUI
gateway starts MCP discovery and the model-catalog prewarm before any AIAgent
exists, and ``hermes model`` / picker prewarm never build one.
- Move the stdlib-only racer core (``_happy_eyeballs_create_connection``,
``_interleave_addrinfos``) and the installer into ``hermes_bootstrap`` and
apply it on import; ``agent.process_bootstrap`` keeps only the httpcore
backend and imports the racer from there. The bootstrap must stay stdlib-only
because entry points call ``harden_import_path()`` after importing it.
- Patch urllib3's own serial connect walker lazily through a one-shot
``sys.meta_path`` hook instead of importing urllib3 eagerly: ``hermes`` and
the TUI gateway never load urllib3 at start, and importing it costs ~50 ms.
- Idempotence by a marker on the installed function, so a re-import of the
bootstrap (tests, ``importlib.reload``) never wraps the racer twice.
- Drop the ``init_agent`` call site (redundant: ``process_bootstrap`` imports
the bootstrap) and trim the five added tests to two invariants in the
mirroring ``tests/test_hermes_bootstrap.py``; the racer unit test moves its
monkeypatch seams to the new module.
- Docs: ``network.force_ipv4`` now describes the default racing behaviour.
Live A/B (stub resolver: blackholed 100::1 first, local IPv4 second, driving
the real clients after ``import hermes_cli.main``): catalog fetch via requests
5.05s -> 0.39s, shared keepalive httpx client 15.24s -> 0.32s, inline
httpx.Client 5.01s -> 0.26s; IPv4-only control 0.01s both sides. Refused ports
still fail instantly with the same exception types; a blackhole-only host still
raises ConnectTimeout at the configured connect timeout.
Fixes#114265
Five tests were silently failing on the Windows CI job because they
either required a Linux/macOS runtime or created symlinks (which the
default Windows user cannot do without Developer Mode).
Mark them with the project's existing linux_only / require_symlinks
markers so CI on Windows skips them cleanly instead of reporting red:
* TestPosixNoOp, test_noop_on_posix, test_no_hermes_home_returns_native
-> @pytest.mark.linux_only (the asserts only make sense on POSIX
where '~/.hermes' is the natural HERMES_HOME fallback).
* test_atomic_replace_copy_fallback_preserves_symlink,
test_symlinked_target_survives_a_contended_rename
-> @pytest.mark.require_symlinks (matches the pattern already used
by sibling tests in the same file).
For the two symlink tests in test_hermes_constants.py that weren't
covered by require_symlinks, wrap the symlink_to() call in a
try/except(OSError, NotImplementedError) -> pytest.skip() so a host
without symlink privilege still skips instead of erroring out.
No production code touched. Verified locally on win32 Python 3.11.9:
88 passed, 25 skipped, 0 failed across the three files (down from
7 failed, 88 passed).
No shims, one marker system. The full mechanical sweep:
- 165 test files converted: every @pytest.mark.<os>_only decorator and
pytestmark assignment is now platforms("<os>"). Docstring/comment
prose mentioning the trio rewritten to the platforms() vocabulary.
- conftest: _OS_MARKS, the legacy skip loop, the double-mark reject for
the trio, and the selector-tagging shim are deleted. The single
platforms() gate does all host gating; _reject_contradictory_platform_
marks replaces the old reject (one platforms() marker per test — a
module-level pytestmark stacked on a per-test marker is the historic
skipped-everywhere-green-everywhere failure and stays a hard error).
- pyproject: only the platforms marker is registered.
- list_os_marked_tests.py: rewritten for the single vocabulary — takes
a platform name (linux/macos/windows), matches quoted platforms(...)
specs including negated and any-of forms, exits nonzero on empty
selection. Its test file rewritten to match (bare identifiers must
not match; the spec must appear inside the string literal).
- tests.yml macOS lane: marker: macos feeds the lister; the pytest
selection is -m "platforms and not integration" — the marker name is
the selector, the conftest's per-test host skips are the gate.
- test_os_marker_gating.py rewritten for the new reject (the old file
tested deleted machinery).
- AGENTS.md / CONTRIBUTING.md updated to the single vocabulary.
Verified: full-tree compile; marker unit tests (36); lister tests;
macOS-lane simulation (21 files, 180 collected / 429 deselected on a
windows host); broad xdist slices through scripts/run_tests.sh
(367 + 1674 passed, zero refactor-attributable failures — the 6 red
tests in the second slice fail identically with the changes stashed).
Gate the POSIX-only and symlink-only tests with the linux_only and
require_symlinks markers. Fix the real cross-platform bugs:
- file_operations: use the translate_path flag, send snippets as base64, and
use sys.executable (the MS Store python3 stub and the list2cmdline
backslash collapse both broke snippets)
- approval: treat backslash as a Windows path separator, not an escape
- registry, browser_registry, secret_sources, plugins: normalize scope-key case
- checkpoint_manager, plugins_cmd: clear read-only bits before delete
- deadline: make MAX_SAFE_TIMEOUT_S fit the Windows limit
- image_routing, acp, cua_backend, daytona: fix Windows and POSIX paths
- hermes_state: match backslash in the retag LIKE clause
- kanban, disk-cleanup: match drive paths and split command arguments
- scripts: emit host separators through as_posix
207 test files are gated or isolated.
many tests patched sys.platform or a module's _IS_WINDOWS flag, then
ran on linux ci. the patch selects the branch under test, but the host
does not have the behavior the branch exists for. the test proves the
patch, not the platform. some gated assertions never ran on any host.
this commit adds three markers: linux_only, macos_only, windows_only.
a conftest hook skips a marked test on the other hosts, with a clear
reason. no test fakes a host now. two documented fakes remain
(android/termux, freebsd) because no ci runner exists for them.
each fake site got one of four treatments:
- gate it: the real host supplies the platform; mocks cover real
dependencies only, never host identity
- patch the module's own probe when the subject is the probe's consumer
- assert against the real host when the fake stood in for any non-x host
- delete the patch when it set the value the host already has
bare skipif(sys.platform != ...) guards became markers too. the lane
model skips these on linux and never imports them on windows, so they
ran on no host. platform parametrize tables are now one marked test
per os.
running on real hosts found real errors: a chrome-sandbox failure in
test_gui_command that main hides, and two windows failures fixed here.
the agents.md testing section now documents the policy.
Addresses hermes-sweeper review on PR #54866: the installer runs
tools/skills_sync.py as a child python.exe whose PYTHONIOENCODING /
PYTHONUTF8 the scoped install.ps1 block sets, but there was no
regression test for this child-Python UTF-8 path. The existing
test_child_process_inherits_utf8_mode covers a different (bootstrap
entry-point) flow.
Add TestSkillsSyncUtf8Guard: three subprocess tests that import
skills_sync (triggering its import-time stdout/stderr reconfigure)
and assert the checkmark/up-arrow glyphs the script prints at
tools/skills_sync.py:596,675 emit valid UTF-8 and exit 0 even when
the child env is left unset or explicitly hostile (gbk). A third
test proves the guard is load-bearing by reproducing the crash
without it.
Also keep the new install.ps1 comment ASCII-only (the checkmark
spelled out as U+2713) per the file's PS 5.1 parser-compatibility
contract at scripts/install.ps1:79-80; the literal glyph in the
comment violated that contract.
Two gaps found auditing the decode-crash cluster:
1. suppress_platform_ver_console() only ran in hermes_cli.main processes;
slash workers, tui_gateway/entry, run_agent, batch_runner, and cli.py
import only hermes_bootstrap and were exposed to both the console
flash and (on Python 3.11.0/3.11.1, which lack CPython's
encoding='locale' fix) a UnicodeDecodeError inside platform.win32_ver()
under PEP 540 — the crash #69413 reported. Move the stub into
hermes_bootstrap so every entry point gets it; the _subprocess_compat
copy stays for non-bootstrap callers.
2. The desktop Electron spawn built the backend env without PYTHONUTF8,
so anything the Python child emitted before hermes_bootstrap ran
(interpreter startup errors, pre-bootstrap tracebacks) decoded with
the locale default. Re-port of PR #56499's env half (echoriver89) to
backend-env.ts (original targeted the deleted backend-env.cjs);
explicit user setting wins.
* fix(windows): stop terminal-window popups from background spawns
Native-Windows desktop/gateway users saw cmd/conhost windows flash on
gateway restart, image paste, the dashboard Projects tree, voice notes,
and ~5 min after closing the app (detached cron). Two root causes:
- Console-subsystem exes (taskkill, schtasks, wmic, netstat, tasklist,
agent-browser, git, ffmpeg, powershell, git-bash) spawned via raw
subprocess allocate a fresh console when the launching process has
none (pythonw desktop backend / detached gateway) - even with output
captured.
- uv venv pythonw shims re-exec console python.exe, so Python children
get a console regardless of how they're launched.
Fixes:
- Single hidden-spawn primitive (_subprocess_compat.run/.popen) that ORs
CREATE_NO_WINDOW on Windows, no-op on POSIX. Route every Hermes-owned
console-exe spawn through it.
- FreeConsole() catch-all in hermes_bootstrap: any Python child that
exclusively owns an auto-allocated console detaches it at startup
(GetConsoleProcessList()==1 gate leaves shared interactive consoles
untouched).
- Replace PowerShell/wmic gateway PID scans with in-process psutil.
- Skip schtasks queries on non-interactive desktop restarts.
- Prefer native agent-browser .exe over .cmd shims.
- Guard test bans raw subprocess spawns of the Windows-only console
tools repo-wide so the popup class can't regress.
* fix(windows): scope FreeConsole to background entry points; fix merge fallout
Console detach review (per #53810 feedback): GetConsoleProcessList()==1 can't
tell a uv pythonw->python phantom console apart from a user opening the
interactive CLI/TUI in its own fresh console (double-click, shortcut, ConPTY) —
both report a single attached process with a tty. Running FreeConsole() in the
import-time bootstrap therefore risked detaching a legitimately-interactive
terminal.
- Extract FreeConsole into explicit hermes_bootstrap.detach_orphan_console();
remove it from apply_windows_utf8_bootstrap() (import side effect).
- Call it only from known background mains: gateway run, dashboard backend
(start_server, what the desktop spawns), cron standalone, tui_gateway entry,
slash worker. Interactive CLI/TUI never calls it.
- Behavior-contract tests: frees only when solo owner, leaves shared console,
no-op without console / on POSIX, and asserts it's not an import side effect.
Merge fallout from origin/main (#53791):
- local.py: 3-way merge left a dangling **_popen_kwargs (NameError crashing
every terminal init). _subprocess_compat.popen already hides the window, so
drop it.
- discord adapter: merge stacked an undefined windows_hide_flags() onto the
primitive call; drop the redundant arg.
- test_gateway: scan now goes psutil-first (zero spawn); rewrite the
case-variant test to drive that production path.
* test(claw): mock _subprocess_compat.run seam for Windows process scan
claw.py's Windows tasklist/powershell scan routes through the hidden-spawn
primitive; the tests still patched claw_mod.subprocess, so on win32 the mock
was never hit and real spawns returned nothing. Patch the actual seam.
Launching Hermes from a directory that ships its own top-level package with a
Hermes-internal name (utils/, proxy/, ui/) crashed the gateway/TUI child with
an ImportError (exit 1, crash loop): from utils import atomic_replace resolved
to the user's package.
tui_gateway/entry.py already stripped the relative cwd forms ('' / '.'), but
the launch dir also reaches sys.path as its own ABSOLUTE path (venv activation
or a project that adds itself to PYTHONPATH), which the strip missed and which
sat ahead of the Hermes root.
Centralize a hardened guard in hermes_bootstrap.harden_import_path(): drop the
relative forms AND force the Hermes source root to the front even when an
absolute cwd entry is present. Wire it into tui_gateway/entry.py and
acp_adapter/entry.py (both spawn into arbitrary cwds); hermes_cli/main.py and
gateway/run.py already insert the root at front. gatewayClient.ts now also
exports HERMES_PYTHON_SRC_ROOT for defense in depth.
Remove unused imports (F401) and duplicate/shadowed import
redefinitions (F811) across the codebase using ruff's safe
autofixes. No behavioral changes -- imports only.
- ~1400 safe autofixes applied across 644 files (net -1072 lines)
- __init__.py re-exports preserved (excluded from F401 removal so
public re-export surfaces stay intact)
- Re-exports that are imported or monkeypatched by tests but look
unused in their defining module are kept with explicit # noqa:
F401 (gateway/run.py load_dotenv; run_agent re-exports from
agent.message_sanitization, agent.context_compressor,
agent.retry_utils, agent.prompt_builder, agent.process_bootstrap,
agent.codex_responses_adapter)
- Unsafe F841 (unused-variable) fixes deliberately skipped -- those
can change behavior when the RHS has side effects
- ruff lints remain disabled in pyproject.toml (only PLW1514 is
selected); this is a one-time cleanup, not a config change
Verification:
- python -m compileall: clean
- pytest --collect-only: all 27161 tests collect (zero import errors)
- core entry points import clean (run_agent, model_tools, cli,
toolsets, hermes_state, batch_runner, gateway)
- static scan: every name any test imports directly from an edited
module still resolves
teknium1 hit ModuleNotFoundError: No module named 'hermes_bootstrap' after
a code update, on both his Windows machine AND his Linux workstation. The
failure mode is real and affects every user who updates hermes by any path
OTHER than a fully-successful ``hermes update``.
## What happens
hermes_bootstrap.py is a top-level module registered via pyproject.toml's
``py-modules`` list (added by Brooklyn's Windows UTF-8 stdio work). It
must be registered in the venv's editable-install .pth file before Python
can find it as a bare ``import hermes_bootstrap``.
``hermes update`` handles this correctly: (1) git reset --hard, (2) clear
__pycache__, (3) uv pip install -e . (re-registers the package including
the new py-modules list), (4) restart.
BUT if any step AFTER (1) fails — network blip during pip install, PEP 668
on a system Python, venv locked, uv not in PATH, a crash mid-update — the
user is left with new code that references hermes_bootstrap and a venv
that doesn't know about it. Every hermes invocation after that crashes
with ModuleNotFoundError, including ``hermes update`` itself. No recovery
path without manual `uv pip install -e .`.
Also affects users who ``git pull`` the repo directly without running
hermes update — relatively common for developers.
## Fix
Wrap ``import hermes_bootstrap`` in a try/except ModuleNotFoundError
across all 6 entry points (hermes_cli/main, run_agent, gateway/run,
acp_adapter/entry, cli, batch_runner). On Windows, missing bootstrap
means the UTF-8 stdio setup doesn't run — degraded behavior (Unicode
chars may fail to print) but NOT a crash. POSIX is unaffected either way
since the bootstrap is a no-op there.
Once hermes is running again, the user can ``hermes update`` to fully
recover.
## Test update
tests/test_hermes_bootstrap.py::test_entry_point_imports_bootstrap
scans for the first top-level import in each entry point and asserts it
is hermes_bootstrap. Extended the check to accept a Try block whose body
is a lone Import of hermes_bootstrap — that's the recovery-friendly form
we just introduced.
Verified behavior by ``mv hermes_bootstrap.py hermes_bootstrap.py.bak``
and confirming ``python -c "import hermes_cli.main"`` succeeds. 82/82
tests pass (hermes_bootstrap + windows-native + windows-compat).
Codebase-wide fix for Python-on-Windows UTF-8 footguns, complementing
the earlier execute_code sandbox fixes (which remain load-bearing for
when the sandbox explicitly scrubs child env).
Problem: Python on Windows has two long-standing text-encoding pitfalls:
1. sys.stdout/stderr are bound to the console code page (cp1252 on
US-locale installs) — print('café') crashes with UnicodeEncodeError.
2. Subprocess children don't know to use UTF-8 unless PYTHONUTF8 and/or
PYTHONIOENCODING are set in their env — so any Python we spawn
(linters, sandbox children, delegation workers) hits the same bug.
Solution: A tiny bootstrap module (hermes_bootstrap.py) imported as the
first statement of every Hermes entry point:
- hermes_cli/main.py (hermes / hermes-agent console_script)
- run_agent.py (hermes-agent direct)
- acp_adapter/entry.py (hermes-acp)
- gateway/run.py (messaging gateway)
- batch_runner.py (parallel batch mode)
- cli.py (legacy direct-launch CLI)
On Windows, the bootstrap:
- os.environ.setdefault('PYTHONUTF8', '1') (PEP 540 UTF-8 mode)
- os.environ.setdefault('PYTHONIOENCODING', 'utf-8')
- sys.stdout/stderr/stdin.reconfigure(encoding='utf-8', errors='replace')
Children inherit the env vars → they run in UTF-8 mode.
Current process's stdio is reconfigured → print('café') works now.
On POSIX (Linux/macOS), the bootstrap is a complete no-op. We don't
touch LANG, LC_*, or anything else — users who have intentionally
configured a non-UTF-8 locale aren't affected. POSIX systems are
already UTF-8 by default in 99% of modern setups, so there's nothing
to fix.
setdefault() (not overwrite) means users who explicitly set PYTHONUTF8=0
or PYTHONIOENCODING=cp1252 in their environment are respected.
What this does NOT fix: bare open(path, 'w') calls in the *parent*
process still default to locale encoding because PYTHONUTF8 is only
read at interpreter init. A ruff PLW1514 sweep (separate follow-up)
will add explicit encoding='utf-8' at those ~219 call sites for
belt-and-suspenders.
Tests (17): 16 passed, 1 skipped on Windows.
- Windows: env vars set, stdio reconfigured, child inherits UTF-8 mode
- POSIX: complete no-op (verified on fake POSIX + skipped on real
POSIX since we don't have a Linux box in this session)
- Idempotence: multiple calls safe
- Graceful degradation: non-reconfigurable streams don't crash
- User opt-out: explicit PYTHONUTF8=0 is respected
- Load order: every entry point's FIRST top-level import is
hermes_bootstrap, enforced by an AST-level parametrized test
pyproject.toml: added hermes_bootstrap to py-modules so it ships with
pip installs.