Skip to content

build: install the toolchain from a locked mise.toml - #40

Merged
offendingcommit merged 14 commits into
mainfrom
chore/mise-toolchain
Oct 24, 2026
Merged

offendingcommit merged 14 commits into
mainfrom
chore/mise-toolchain

Conversation

@offendingcommit

Copy link
Copy Markdown
Owner

Unit U1 of the Hermes v0.22.5 upgrade plan: the kit's toolchain comes from one locked file, so CI and laptops run the same Python.

What changed

  • mise.toml pins Python 3.14.9 (the interpreter the deployed Hermes image uses, from upstream pm/lock.json), uv 0.13.1 (same as infra) and just 1.58.0 (the version CI already used).
  • mise.lock locks all three for linux-x64, linux-arm64 and macos-arm64 (mise lock --platform linux-x64,linux-arm64,macos-arm64).
  • test.yml, hermes-drift.yml and release.yml: every astral-sh/setup-uv (Python 3.11) and extractions/setup-just step is replaced by one jdx/mise-action step, pinned by SHA to v5.2.3. The action adds --locked itself when mise.lock is present. In the release materialize job the install moves ahead of just release-attach-trigger, which needs just.
  • mise.toml sets UV_PYTHON_PREFERENCE=only-system and UV_PYTHON_DOWNLOADS=never. Without them, uv picked its own managed Python 3.14 even with mise's 3.14.9 first on PATH (checked with uv python find both ways).
  • justfile recipes stay the entry points; nothing moved to mise tasks.
  • AGENTS.md and the README Development section now say to install with mise trust && mise install --locked.

Verification (macOS arm64)

  • mise trust && mise install --locked: exit 0.
  • just test under mise (uv reports Using CPython 3.14.9 from the mise install): Ran 258 tests, OK (skipped=17), exit 0.
  • Baseline on origin/main with uv's default Python (3.14.7 locally): Ran 258 tests, OK (skipped=17), exit 0. No errors before or after.
  • CI on this PR runs on 3.14.9 instead of 3.11.

Not in this PR

  • DEPLOYED_HERMES_REVISION, the contract lanes and the drift classifier are unchanged (U4).
  • No test asserts a tool version.
  • No [task_config] or mise tasks, because the justfile remains the task runner.
  • pyproject.toml requires-python = ">=3.11" is unchanged.

https://claude.ai/code/session_01VDtQ1pDTJksqAcSdrGePVb

CI ran Python 3.11 from astral-sh/setup-uv while laptops ran whatever
uv found, so the two never ran the same interpreter. mise.toml now pins
Python 3.14.9 (the deployed Hermes interpreter), uv and just, and
mise.lock locks them for linux-x64, linux-arm64 and macos-arm64. Every
workflow installs them with jdx/mise-action, which installs --locked
when a lock is present. The justfile recipes stay the entry points.

UV_PYTHON_PREFERENCE=only-system keeps uv on mise's Python: without it
uv picks its own managed 3.14 even with mise's Python first on PATH.

Claude-Session: https://claude.ai/code/session_01VDtQ1pDTJksqAcSdrGePVb
hermes-plugin-kit, hermes-agent-runtime-image and infra now pin the same
uv (0.13.5) and just (1.59.0) through mise, so one toolchain update moves
all three.

Claude-Session: https://claude.ai/code/session_01VDtQ1pDTJksqAcSdrGePVb
The drift lane fed the classifier only the last 25 lines of the suite
log. When both contract modules failed to import the host (No module
named 'ruamel'), the ERROR headers were cut, no failure block parsed,
and the classifier printed "No contract failures against upstream."
and exited 0.

These tests use the real captured shape of that log, tail-only and
full, plus Ran 0, a missing Ran line, and an unexplained non-zero exit.
Five fail on today's classify_drift.py; the full-log case already
passes because its ERROR headers survive.

The two baseline tests gain a realistic Ran/EXIT trailer, since a log
without one is no longer evidence that anything ran.

Refs: #40

Claude-Session: https://claude.ai/code/session_01VDtQ1pDTJksqAcSdrGePVb
classify_drift.py only knew about parsed FAIL/ERROR blocks, so a log
whose blocks were missing read as clean. It now also files as new drift:

- any "ImportError: Failed to import test module: X", naming X;
- no "Ran N tests" line, or Ran 0;
- no EXIT= line, or a non-zero EXIT with no failure block to explain
  it. A run whose only failure is baselined still exits non-zero and
  still classifies as known.

_run-contract-suite now prints the whole log instead of tail -25, since
the drift workflow classifies that output. The workflow's step summary
keeps its own tail for people.

Refs: #41

Claude-Session: https://claude.ai/code/session_01VDtQ1pDTJksqAcSdrGePVb
DEPLOYED_HERMES_REVISION moves from f80f453 to 939e45c9 (v2026.9.11),
the revision minilab deploys today. test.yml's hermes-deployed-contract
job carries two literals for it (env and checkout ref), because
actions/checkout reads the ref before any Python exists.

Both the constant's docstring and test.yml said a release-contract test
kept those literals equal to the constant. No such test existed:
moving the constant alone left `just test` green. A harness test now
asserts it, and fails on a constant that moves without the workflow.

release.yml reads the constant at run time and compares it with the
pinned checkout's HEAD, so it has no literal to move.

Refs: #41

Claude-Session: https://claude.ai/code/session_01VDtQ1pDTJksqAcSdrGePVb
The lanes ran the contract modules in the kit's venv, which lacks the
host's dependencies, so both modules died importing hermes_cli
(No module named 'ruamel'). The host's pyproject.toml alone does not
resolve on v0.22.5 either, so its uv.lock is the faithful source (KTD4).

provision-hermes-host now runs `uv sync --frozen` from the host checkout
into a venv kept in this repository (the checkout is only read), with
the first system interpreter satisfying the host's own requires-python,
then installs the kit into it with --no-deps. Both lanes call it after
the checkout is resolved, so CI's HERMES_AGENT_PATH path is covered too,
and _run-contract-suite runs on that venv's python.

The deployed pin 939e45c9 requires <3.14, so mise.toml adds Python
3.13.14 (the deployed image's 3.13 line) beside 3.14.9, and mise.lock is
regenerated for linux-x64, linux-arm64 and macos-arm64. A scoped
UV_PYTHON_DOWNLOADS=automatic was the alternative; it would bypass the
locked toolchain the [env] block exists to enforce. Drop 3.13 when the
pin moves to v0.22.5 (KTD7).

Verified locally: pinned lane at 939e45c9 ran 17 tests, all passing,
on 3.13.14. The upstream lane at v0.22.5 now imports the host and fails
only on SCHEMA_VERSION (U5), which the classifier reports as new drift.

Refs: #41

Claude-Session: https://claude.ai/code/session_01VDtQ1pDTJksqAcSdrGePVb
test-context-engine-contract still ran in the kit venv, so pointed at a
real host it hit the same import failure the full lanes just shed. It
now provisions the named host through provision-hermes-host first.
Against 939e45c9 it ran 3 tests, OK.

Refs: #41

Claude-Session: https://claude.ai/code/session_01VDtQ1pDTJksqAcSdrGePVb
Hermes v0.22.5 removed the PLUGIN-COMPAT pointer layer (upstream
#126164), so hermes_state no longer re-exports SCHEMA_VERSION and the
contract module failed to import against bf0203a7. Import it from
hermes_state_common first and fall back to hermes_state for older
hosts. SessionDB still comes from hermes_state.

Claude-Session: https://claude.ai/code/session_01VDtQ1pDTJksqAcSdrGePVb
The lanes installed the kit into the host env with --no-deps, on the
theory that the env should stay exactly the host's lock. The runtime
image does not install the kit that way: its Dockerfile and
plugin-refresh install it with its dependencies. The pinned host
(939e45c9) happened to lock pyyaml, so the gap was invisible there.
v0.22.5 dropped PyYAML for ruamel, so plugin_skill raised
"No module named 'yaml'" and the upstream lane reported one error.

Install the kit with its deps. Packages the host lock already satisfies
are left alone; on v0.22.5 the only addition is pyyaml 6.0.3.

Claude-Session: https://claude.ai/code/session_01VDtQ1pDTJksqAcSdrGePVb
…comments

provision-hermes-host read requires-python with whatever python3 was first
on PATH; it now goes through the same uv indirection as every other recipe.
Comments that narrated the change, restated code, or carried a plan label
from another repository are gone; the ones recording an external
constraint stay.

Claude-Session: https://claude.ai/code/session_01VDtQ1pDTJksqAcSdrGePVb
A lane whose unittest summary reads OK (skipped=N) exits 0 inside the suite;
the lane recipe then fails on its own skip guard, but the drift workflow
never appends the outer EXIT line, so the classifier saw a clean run.
Cross-model code review (adversarial lens) found the path; it is not
reachable today because the contract modules skip only at class level
under HERMES_CONTRACT_REQUIRED.

Claude-Session: https://claude.ai/code/session_01VDtQ1pDTJksqAcSdrGePVb
Without a version input the action installs the newest mise at least a day
old, so two runs of one commit, release jobs holding
PRIVATE_ARTIFACTS_TOKEN included, could use different installers. The
action keeps verifying the download against mise's signed checksums; a
sha256 input would replace that signature check with a plain hash, so only
the version is pinned. min_version follows the pin.

Claude-Session: https://claude.ai/code/session_01VDtQ1pDTJksqAcSdrGePVb
fix(contract): make the contract lanes pass on Hermes v0.22.5
fix(contract): make the contract lanes report what they ran
@offendingcommit
offendingcommit marked this pull request as ready for review October 24, 2026 22:29
@offendingcommit
offendingcommit merged commit 90da8eb into main Oct 24, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant