From 503ead04537e3ea833d6c070dd2d2e6c47ea36b4 Mon Sep 17 00:00:00 2001 From: Offending Commit Date: Sat, 24 Oct 2026 16:21:23 -0500 Subject: [PATCH 01/10] test(drift): pin down runs that never executed as drift 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: offendingcommit/hermes-plugin-kit#40 Claude-Session: https://claude.ai/code/session_01VDtQ1pDTJksqAcSdrGePVb --- .../contract-logs/import-failure-full.log | 54 ++++++++++++ .../contract-logs/import-failure-tail.log | 29 +++++++ tests/test_drift_classification.py | 82 +++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 tests/fixtures/contract-logs/import-failure-full.log create mode 100644 tests/fixtures/contract-logs/import-failure-tail.log diff --git a/tests/fixtures/contract-logs/import-failure-full.log b/tests/fixtures/contract-logs/import-failure-full.log new file mode 100644 index 0000000..5562277 --- /dev/null +++ b/tests/fixtures/contract-logs/import-failure-full.log @@ -0,0 +1,54 @@ +test_hermes_contract (unittest.loader._FailedTest.test_hermes_contract) ... ERROR +test_context_engine_contract (unittest.loader._FailedTest.test_context_engine_contract) ... ERROR + +====================================================================== +ERROR: test_hermes_contract (unittest.loader._FailedTest.test_hermes_contract) +---------------------------------------------------------------------- +ImportError: Failed to import test module: test_hermes_contract +Traceback (most recent call last): + File "/home/runner/.local/share/mise/installs/python/3.14.9/lib/python3.14/unittest/loader.py", line 141, in loadTestsFromName + module = __import__(module_name) + File "/home/runner/work/hermes-plugin-kit/hermes-plugin-kit/tests/test_hermes_contract.py", line 119, in + _REAL = _import_real_hermes() + File "/home/runner/work/hermes-plugin-kit/hermes-plugin-kit/tests/test_hermes_contract.py", line 116, in _import_real_hermes + return _try() + File "/home/runner/work/hermes-plugin-kit/hermes-plugin-kit/tests/test_hermes_contract.py", line 56, in _try + from hermes_cli.plugins import ( # type: ignore + ...<5 lines>... + ) + File "/home/runner/work/hermes-plugin-kit/hermes-plugin-kit/.hermes-agent-upstream/hermes_cli/plugins.py", line 31, in + from utils import env_var_enabled + File "/home/runner/work/hermes-plugin-kit/hermes-plugin-kit/.hermes-agent-upstream/utils.py", line 18, in + import hermes_yaml as yaml + File "/home/runner/work/hermes-plugin-kit/hermes-plugin-kit/.hermes-agent-upstream/hermes_yaml.py", line 10, in + from ruamel.yaml import YAML +ModuleNotFoundError: No module named 'ruamel' + + +====================================================================== +ERROR: test_context_engine_contract (unittest.loader._FailedTest.test_context_engine_contract) +---------------------------------------------------------------------- +ImportError: Failed to import test module: test_context_engine_contract +Traceback (most recent call last): + File "/home/runner/.local/share/mise/installs/python/3.14.9/lib/python3.14/unittest/loader.py", line 141, in loadTestsFromName + module = __import__(module_name) + File "/home/runner/work/hermes-plugin-kit/hermes-plugin-kit/tests/test_context_engine_contract.py", line 37, in + _HOST = _import_deployed_host() + File "/home/runner/work/hermes-plugin-kit/hermes-plugin-kit/tests/test_context_engine_contract.py", line 28, in _import_deployed_host + from hermes_cli.plugins import ( # type: ignore + ...<3 lines>... + ) + File "/home/runner/work/hermes-plugin-kit/hermes-plugin-kit/.hermes-agent-upstream/hermes_cli/plugins.py", line 31, in + from utils import env_var_enabled + File "/home/runner/work/hermes-plugin-kit/hermes-plugin-kit/.hermes-agent-upstream/utils.py", line 18, in + import hermes_yaml as yaml + File "/home/runner/work/hermes-plugin-kit/hermes-plugin-kit/.hermes-agent-upstream/hermes_yaml.py", line 10, in + from ruamel.yaml import YAML +ModuleNotFoundError: No module named 'ruamel' + + +---------------------------------------------------------------------- +Ran 2 tests in 0.000s + +FAILED (errors=2) +EXIT=1 diff --git a/tests/fixtures/contract-logs/import-failure-tail.log b/tests/fixtures/contract-logs/import-failure-tail.log new file mode 100644 index 0000000..04853f8 --- /dev/null +++ b/tests/fixtures/contract-logs/import-failure-tail.log @@ -0,0 +1,29 @@ +---------------------------------------------------------------------- +ImportError: Failed to import test module: test_context_engine_contract +Traceback (most recent call last): + File "/home/runner/.local/share/mise/installs/python/3.14.9/lib/python3.14/unittest/loader.py", line 141, in loadTestsFromName + module = __import__(module_name) + File "/home/runner/work/hermes-plugin-kit/hermes-plugin-kit/tests/test_context_engine_contract.py", line 37, in + _HOST = _import_deployed_host() + File "/home/runner/work/hermes-plugin-kit/hermes-plugin-kit/tests/test_context_engine_contract.py", line 28, in _import_deployed_host + from hermes_cli.plugins import ( # type: ignore + ...<3 lines>... + ) + File "/home/runner/work/hermes-plugin-kit/hermes-plugin-kit/.hermes-agent-upstream/hermes_cli/plugins.py", line 31, in + from utils import env_var_enabled + File "/home/runner/work/hermes-plugin-kit/hermes-plugin-kit/.hermes-agent-upstream/utils.py", line 18, in + import hermes_yaml as yaml + File "/home/runner/work/hermes-plugin-kit/hermes-plugin-kit/.hermes-agent-upstream/hermes_yaml.py", line 10, in + from ruamel.yaml import YAML +ModuleNotFoundError: No module named 'ruamel' + + +---------------------------------------------------------------------- +Ran 2 tests in 0.000s + +FAILED (errors=2) +EXIT=1 +contract suite failed (upstream 0000000000000000000000000000000000000000) +error: recipe `_run-contract-suite` failed with exit code 1 +error: recipe `test-contract-upstream` failed with exit code 1 +EXIT=1 diff --git a/tests/test_drift_classification.py b/tests/test_drift_classification.py index d3187ad..b60d3d3 100644 --- a/tests/test_drift_classification.py +++ b/tests/test_drift_classification.py @@ -9,6 +9,8 @@ from __future__ import annotations import importlib.util +import subprocess +import sys import unittest from pathlib import Path @@ -21,6 +23,12 @@ assert _spec.loader is not None _spec.loader.exec_module(classify_drift) +LOGS = ROOT / "tests" / "fixtures" / "contract-logs" + +#: What a completed run with one baselined failure ends with. Without it a log +#: is not evidence that the suite ran at all. +_RAN_TRAILER = "\n" + "-" * 70 + "\nRan 17 tests in 0.601s\n\nFAILED (failures=1)\nEXIT=1\n" + class FingerprintStabilityTests(unittest.TestCase): """Same failure, different machine, same fingerprint.""" @@ -112,6 +120,7 @@ def test_a_known_failure_classifies_as_known(self) -> None: + "-" * 70 + "\nTraceback (most recent call last):\n" + f"AssertionError: ... {fingerprint.replace('-', ' ')} ...\n" + + _RAN_TRAILER ) known, new = classify_drift.classify(log, classify_drift.load_baseline()) @@ -126,6 +135,7 @@ def test_the_same_test_failing_differently_is_new(self) -> None: + "-" * 70 + "\nTraceback (most recent call last):\n" + "AssertionError: an entirely unrelated cause\n" + + _RAN_TRAILER ) known, new = classify_drift.classify(log, classify_drift.load_baseline()) @@ -134,5 +144,77 @@ def test_the_same_test_failing_differently_is_new(self) -> None: self.assertEqual(1, len(new), "a baselined test failing differently is a new break") +class RunThatDidNotExecuteTests(unittest.TestCase): + """A suite that never ran its checks is drift, not a clean report. + + The captured logs under ``tests/fixtures/contract-logs`` come from a real + run where both contract modules died importing the host + (``No module named 'ruamel'``). The tail-only one is exactly what the drift + workflow handed the classifier while the lane printed ``tail -25``: the + ``ERROR:`` headers were cut, no failure block parsed, and the report read + "No contract failures against upstream." + """ + + def test_tail_of_an_import_failure_is_new_drift_naming_the_module(self) -> None: + log = (LOGS / "import-failure-tail.log").read_text() + + known, new = classify_drift.classify(log, classify_drift.load_baseline()) + + self.assertEqual([], known) + self.assertTrue(new, "an import failure must never classify as clean") + self.assertTrue( + any("test_context_engine_contract" in entry for entry in new), new + ) + + def test_full_import_failure_log_names_both_modules(self) -> None: + log = (LOGS / "import-failure-full.log").read_text() + + _, new = classify_drift.classify(log, classify_drift.load_baseline()) + + joined = "\n".join(new) + self.assertIn("test_hermes_contract", joined) + self.assertIn("test_context_engine_contract", joined) + + def test_the_cli_exits_non_zero_on_the_tail_only_log(self) -> None: + result = subprocess.run( + [sys.executable, str(ROOT / "scripts" / "classify_drift.py"), + str(LOGS / "import-failure-tail.log"), "0" * 40], + capture_output=True, text=True, check=False, + ) + + self.assertEqual(1, result.returncode, result.stdout) + self.assertNotIn("No contract failures", result.stdout) + + def test_zero_tests_ran_is_new_drift_even_when_unittest_exits_zero(self) -> None: + log = "\n" + "-" * 70 + "\nRan 0 tests in 0.000s\n\nOK\nEXIT=0\n" + + _, new = classify_drift.classify(log, {}) + + self.assertTrue(new) + + def test_a_log_with_no_ran_line_is_new_drift(self) -> None: + """Provisioning died before the suite started: nothing ran.""" + log = "error: Failed to spawn: `uv sync`\nEXIT=2\n" + + _, new = classify_drift.classify(log, {}) + + self.assertTrue(new) + + def test_a_non_zero_exit_with_no_parsed_failure_is_new_drift(self) -> None: + log = "\n" + "-" * 70 + "\nRan 17 tests in 0.601s\n\nOK\nEXIT=0\nEXIT=1\n" + + _, new = classify_drift.classify(log, {}) + + self.assertTrue(new) + + def test_a_clean_run_reports_no_drift(self) -> None: + log = ( + "test_x (tests.test_hermes_contract.HermesContractTests.test_x) ... ok\n" + "\n" + "-" * 70 + "\nRan 17 tests in 0.601s\n\nOK\nEXIT=0\nEXIT=0\n" + ) + + self.assertEqual(([], []), classify_drift.classify(log, {})) + + if __name__ == "__main__": unittest.main() From b98783628ce3c907ab49ed1770cba48c741309ff Mon Sep 17 00:00:00 2001 From: Offending Commit Date: Sat, 24 Oct 2026 16:23:01 -0500 Subject: [PATCH 02/10] fix(drift): report runs that never executed as new drift 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: offendingcommit/hermes-plugin-kit#41 Claude-Session: https://claude.ai/code/session_01VDtQ1pDTJksqAcSdrGePVb --- justfile | 4 +++- scripts/classify_drift.py | 32 +++++++++++++++++++++++++++++- tests/test_drift_classification.py | 11 ++++++++-- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/justfile b/justfile index 93cab96..d4150ea 100644 --- a/justfile +++ b/justfile @@ -106,7 +106,9 @@ _run-contract-suite label: > "$log" 2>&1 echo "EXIT=$?" >> "$log" set -e - tail -25 "$log" + # The whole log, not a tail: the drift classifier reads this output, and a + # tail cut the ERROR headers of an import failure and read as clean. + cat "$log" grep -qE '^EXIT=0' "$log" || { echo "contract suite failed ({{ label }})" >&2; exit 1; } ran="$(grep -oE '^Ran [0-9]+' "$log" | grep -oE '[0-9]+' | head -n1)" test -n "$ran" && [ "$ran" -gt 0 ] \ diff --git a/scripts/classify_drift.py b/scripts/classify_drift.py index 73315f1..6eabc7f 100644 --- a/scripts/classify_drift.py +++ b/scripts/classify_drift.py @@ -90,10 +90,39 @@ def fingerprint(body: str) -> str: return "unknown" +_IMPORT_FAILURE = re.compile(r"^ImportError: Failed to import test module: (\S+)", re.M) +_RAN = re.compile(r"^Ran (\d+) tests?\b", re.M) +_EXIT = re.compile(r"^EXIT=(\d+)\s*$", re.M) + + +def _did_not_execute(log: str, parsed_failures: int) -> list[str]: + """Reasons the log is not evidence that the suite ran its checks. + + These need no failure block: the blocks are exactly what goes missing when + a run dies early or only part of its log arrives. + """ + reasons = [ + f"{module} — failed to import, so none of its checks ran" + for module in dict.fromkeys(_IMPORT_FAILURE.findall(log)) + ] + ran = _RAN.findall(log) + if not ran: + reasons.append("no `Ran N tests` line — the suite never completed") + elif int(ran[-1]) == 0: + reasons.append("Ran 0 tests — nothing was checked") + exits = [int(code) for code in _EXIT.findall(log)] + if not exits: + reasons.append("no `EXIT=` line — the run's outcome is unknown") + elif any(exits) and parsed_failures == 0: + reasons.append(f"suite exited {max(exits)} with no failure the log explains") + return reasons + + def classify(log: str, baseline: dict[str, str]) -> tuple[list[str], list[str]]: known: list[str] = [] new: list[str] = [] - for match in _BLOCK.finditer(log): + blocks = list(_BLOCK.finditer(log)) + for match in blocks: test_id = match.group("test_id") print_id = test_id fp = fingerprint(match.group("body")) @@ -105,6 +134,7 @@ def classify(log: str, baseline: dict[str, str]) -> tuple[list[str], list[str]]: f"baselined as {expected!r} but failed as {fp!r}" ) new.append(f"{print_id} — {reason}") + new += _did_not_execute(log, len(blocks)) return known, new diff --git a/tests/test_drift_classification.py b/tests/test_drift_classification.py index b60d3d3..ddbe311 100644 --- a/tests/test_drift_classification.py +++ b/tests/test_drift_classification.py @@ -193,8 +193,15 @@ def test_zero_tests_ran_is_new_drift_even_when_unittest_exits_zero(self) -> None self.assertTrue(new) def test_a_log_with_no_ran_line_is_new_drift(self) -> None: - """Provisioning died before the suite started: nothing ran.""" - log = "error: Failed to spawn: `uv sync`\nEXIT=2\n" + """The suite died before its summary, whatever the exit said.""" + log = "test_x (tests.test_hermes_contract.T.test_x) ... ok\nEXIT=0\n" + + _, new = classify_drift.classify(log, {}) + + self.assertTrue(new) + + def test_a_log_with_no_exit_line_is_new_drift(self) -> None: + log = "\n" + "-" * 70 + "\nRan 17 tests in 0.601s\n\nOK\n" _, new = classify_drift.classify(log, {}) From 38dd0d36899b1e24ec1b61d26fe6caf365f22228 Mon Sep 17 00:00:00 2001 From: Offending Commit Date: Sat, 24 Oct 2026 16:24:36 -0500 Subject: [PATCH 03/10] fix(contract): pin the deployed lane to the revision minilab runs 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: offendingcommit/hermes-plugin-kit#41 Claude-Session: https://claude.ai/code/session_01VDtQ1pDTJksqAcSdrGePVb --- .github/workflows/test.yml | 4 ++-- hermes_plugin_kit/testing.py | 4 ++-- tests/test_testing_harness.py | 14 ++++++++++++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e951e9f..1fb9664 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,13 +44,13 @@ jobs: hermes-deployed-contract: runs-on: ubuntu-latest env: - DEPLOYED_HERMES_REVISION: f80f453ae0679347e38abc917c7f94f717bf96c5 + DEPLOYED_HERMES_REVISION: 939e45c91d751fadd94dcd1b873ac3cb44846213 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: NousResearch/hermes-agent - ref: f80f453ae0679347e38abc917c7f94f717bf96c5 + ref: 939e45c91d751fadd94dcd1b873ac3cb44846213 path: .hermes-agent-deployed - name: Install the locked toolchain (mise.toml) uses: jdx/mise-action@82a612c5ab8bcda7cc5db6b7e81a8e7584c3dc6e # v5.2.3 diff --git a/hermes_plugin_kit/testing.py b/hermes_plugin_kit/testing.py index da198f3..52e0b84 100644 --- a/hermes_plugin_kit/testing.py +++ b/hermes_plugin_kit/testing.py @@ -78,8 +78,8 @@ #: #: The CI workflow keeps its own literal -- ``actions/checkout`` consumes the #: ``ref:`` before a Python toolchain exists, so the workflow cannot read this -#: value -- and a release-contract test asserts the two agree. -DEPLOYED_HERMES_REVISION = "f80f453ae0679347e38abc917c7f94f717bf96c5" +#: value -- and a harness test asserts the two agree. +DEPLOYED_HERMES_REVISION = "939e45c91d751fadd94dcd1b873ac3cb44846213" #: Parameter names the kit's registrars pass positionally, in order. #: diff --git a/tests/test_testing_harness.py b/tests/test_testing_harness.py index 3ad45f8..42b6c06 100644 --- a/tests/test_testing_harness.py +++ b/tests/test_testing_harness.py @@ -180,6 +180,20 @@ def test_no_host_reports_unchecked_and_names_the_required_revision(self) -> None def test_deployed_revision_is_a_full_commit_sha(self) -> None: self.assertRegex(hpk_testing.DEPLOYED_HERMES_REVISION, r"^[0-9a-f]{40}$") + def test_the_deployed_contract_job_checks_out_the_same_revision(self) -> None: + """The workflow cannot read the constant, so its two literals must match it.""" + workflow = ( + Path(__file__).resolve().parents[1] / ".github" / "workflows" / "test.yml" + ).read_text() + job = workflow.split("hermes-deployed-contract:", 1)[1] + literals = re.findall(r"(?:DEPLOYED_HERMES_REVISION:|ref:)\s*([0-9a-f]{40})", job) + + self.assertEqual( + [hpk_testing.DEPLOYED_HERMES_REVISION] * 2, literals, + "test.yml's hermes-deployed-contract env and checkout ref must equal " + "hermes_plugin_kit.testing.DEPLOYED_HERMES_REVISION", + ) + class HostShapeModeTests(unittest.TestCase): """Both branches of the kit's own capability probe must be reachable.""" From 042c0c36a8273f1575c83516ceb86e6148161d3f Mon Sep 17 00:00:00 2001 From: Offending Commit Date: Sat, 24 Oct 2026 16:26:39 -0500 Subject: [PATCH 04/10] fix(contract): run the contract lanes on the host's own locked env 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: offendingcommit/hermes-plugin-kit#41 Claude-Session: https://claude.ai/code/session_01VDtQ1pDTJksqAcSdrGePVb --- .gitignore | 2 ++ justfile | 45 ++++++++++++++++++++++++++++++++++++++------- mise.lock | 20 ++++++++++++++++++++ mise.toml | 4 +++- 4 files changed, 63 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 4007cc1..34f7c71 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ dist/ .hermes-agent-pinned/ .hermes-agent-upstream/ .DS_Store +.hermes-venv-pinned/ +.hermes-venv-upstream/ diff --git a/justfile b/justfile index d4150ea..61794c3 100644 --- a/justfile +++ b/justfile @@ -6,6 +6,8 @@ openspec_min := "1.13.1" hermes_agent_repo := env_var_or_default("HERMES_AGENT_REPO", "https://github.com/NousResearch/hermes-agent.git") hermes_pinned_dir := env_var_or_default("HERMES_PINNED_DIR", ".hermes-agent-pinned") hermes_upstream_dir := env_var_or_default("HERMES_UPSTREAM_DIR", ".hermes-agent-upstream") +hermes_pinned_venv := env_var_or_default("HERMES_PINNED_VENV", ".hermes-venv-pinned") +hermes_upstream_venv := env_var_or_default("HERMES_UPSTREAM_VENV", ".hermes-venv-upstream") # Show available recipes. default: @@ -59,6 +61,32 @@ prepare-hermes-ref ref dir: git -C "{{ dir }}" checkout -q FETCH_HEAD echo "hermes-agent at $(git -C "{{ dir }}" rev-parse HEAD)" +# Build the host's own environment from the host's own lockfile (KTD4). +# +# The kit's venv lacks the host's dependencies (ruamel, dotenv, ...), so the +# contract modules died at import there. The host's `pyproject.toml` alone +# does not resolve either, so its `uv.lock` is the only faithful source. +# +# The interpreter is the first one on PATH that satisfies the host's own +# `requires-python`. mise.toml installs one per host generation: 3.14.9 for +# v0.22.5 and later, 3.13 for the pre-upgrade deployed pin (<3.14). The venv +# lives in this repository, so the host checkout is read, never rewritten. +[private] +provision-hermes-host dir venv: + #!/usr/bin/env bash + set -euo pipefail + host="$(cd "{{ dir }}" && pwd)" + mkdir -p "{{ venv }}" + venv="$(cd "{{ venv }}" && pwd)" + test -f "$host/uv.lock" || { echo "no uv.lock in $host; cannot provision the host faithfully" >&2; exit 1; } + spec="$(python3 -c 'import sys, tomllib; print(tomllib.load(open(sys.argv[1], "rb"))["project"]["requires-python"])' "$host/pyproject.toml")" + py="$({{ uv }} python find --system "$spec")" \ + || { echo "no interpreter on PATH satisfies the host's requires-python $spec; add one to mise.toml" >&2; exit 1; } + echo "hermes host python: $("$py" -V 2>&1) at $py (host requires-python $spec)" + (cd "$host" && UV_PROJECT_ENVIRONMENT="$venv" {{ uv }} sync --frozen --quiet --python "$py") + # --no-deps: the environment stays exactly the host's lock. + {{ uv }} pip install --quiet --python "$venv/bin/python" --no-deps --reinstall-package hermes-plugin-kit . + # Run the full contract suite against the deployed pin. # # HERMES_EXPECTED_REVISION makes the lane refuse a checkout at any other @@ -71,9 +99,10 @@ test-contract-pinned: if [[ -z "${HERMES_AGENT_PATH:-}" ]]; then just prepare-hermes-ref "$pin" "{{ hermes_pinned_dir }}" fi + just provision-hermes-host "$dir" "{{ hermes_pinned_venv }}" HERMES_CONTRACT_REQUIRED=1 HERMES_EXPECTED_REVISION="$pin" \ HERMES_AGENT_PATH="$(cd "$dir" && pwd)" \ - just _run-contract-suite "pinned $pin" + just _run-contract-suite "pinned $pin" "{{ hermes_pinned_venv }}" # Run the full contract suite against upstream main -- the drift lane. test-contract-upstream: @@ -84,25 +113,27 @@ test-contract-upstream: just prepare-hermes-ref main "{{ hermes_upstream_dir }}" fi rev="$(git -C "$dir" rev-parse HEAD)" + just provision-hermes-host "$dir" "{{ hermes_upstream_venv }}" HERMES_CONTRACT_REQUIRED=1 HERMES_AGENT_PATH="$(cd "$dir" && pwd)" \ - just _run-contract-suite "upstream $rev" + just _run-contract-suite "upstream $rev" "{{ hermes_upstream_venv }}" # Backwards-compatible alias for the upstream lane. test-contract: test-contract-upstream # Run both contract modules and prove the run actually executed checks. # -# The exit status alone cannot say whether anything ran: on Python 3.11 -- -# the version CI pins -- unittest exits 0 both when every test skips and when -# zero tests are collected. So the executed count is the evidence. +# The exit status alone cannot say whether anything ran: unittest exits 0 when +# every test skips, and on Python before 3.12 also when zero tests are +# collected. So the executed count is the evidence. The suite runs on the +# host's own environment (see provision-hermes-host). [private] -_run-contract-suite label: +_run-contract-suite label venv: #!/usr/bin/env bash set -euo pipefail log="$(mktemp)" trap 'rm -f "$log"' EXIT set +e - {{ uv }} run python -m unittest tests.test_hermes_contract tests.test_context_engine_contract -v \ + "{{ venv }}/bin/python" -m unittest tests.test_hermes_contract tests.test_context_engine_contract -v \ > "$log" 2>&1 echo "EXIT=$?" >> "$log" set -e diff --git a/mise.lock b/mise.lock index ca6fa34..696e354 100644 --- a/mise.lock +++ b/mise.lock @@ -42,6 +42,26 @@ checksum = "sha256:9f8c3480b8f0db553c270db548e8a21ae8e39df412e5661d66e8050c5c6ed url = "https://github.com/astral-sh/python-build-standalone/releases/download/20261021/cpython-3.14.9+20261021-aarch64-apple-darwin-install_only_stripped.tar.gz" provenance = "github-attestations" +[[tools.python]] +version = "3.13.14" +backend = "core:python" +specifiers = ["3.13.14"] + +[tools.python."platforms.linux-arm64"] +checksum = "sha256:f1cda716f5595a97e79f760f81da7150e4f6b4ae9dc49968b4ce5d6d64bba058" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.13.14+20260805-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz" +provenance = "github-attestations" + +[tools.python."platforms.linux-x64"] +checksum = "sha256:39e82d05926bdcd206732026bcd878d9f00e288cd227d91c1fabf379b6ea4fa5" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.13.14+20260805-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz" +provenance = "github-attestations" + +[tools.python."platforms.macos-arm64"] +checksum = "sha256:f0e634654e1d6b55cf81419eb629caf78f50234d01ede318f78e5091bada6085" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.13.14+20260805-aarch64-apple-darwin-install_only_stripped.tar.gz" +provenance = "github-attestations" + [[tools.uv]] version = "0.13.5" backend = "aqua:astral-sh/uv" diff --git a/mise.toml b/mise.toml index cb1fa99..a229793 100644 --- a/mise.toml +++ b/mise.toml @@ -6,7 +6,9 @@ min_version = "2026.10.15" [tools] # Must equal the Python in the deployed Hermes image (upstream pm/lock.json). -python = "3.14.9" +# 3.13 exists only for the pre-upgrade host's venv (its requires-python is <3.14); +# drop it when DEPLOYED_HERMES_REVISION moves to v0.22.5 or later. +python = ["3.14.9", "3.13.14"] uv = "0.13.5" just = "1.59.0" From 78d78f4e73c7e3ab85ec0cce0e771e5a70b1e7bd Mon Sep 17 00:00:00 2001 From: Offending Commit Date: Sat, 24 Oct 2026 16:29:42 -0500 Subject: [PATCH 05/10] fix(contract): run the context-engine lane on the host env too 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: offendingcommit/hermes-plugin-kit#41 Claude-Session: https://claude.ai/code/session_01VDtQ1pDTJksqAcSdrGePVb --- justfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/justfile b/justfile index 61794c3..407d34d 100644 --- a/justfile +++ b/justfile @@ -155,7 +155,8 @@ test-context-engine-contract: #!/usr/bin/env bash set -euo pipefail : "${HERMES_AGENT_PATH:?set it to a hermes-agent checkout; this lane does not guess}" - HERMES_CONTRACT_REQUIRED=1 {{ uv }} run python -m unittest tests.test_context_engine_contract -v + just provision-hermes-host "$HERMES_AGENT_PATH" "{{ hermes_pinned_venv }}" + HERMES_CONTRACT_REQUIRED=1 "{{ hermes_pinned_venv }}/bin/python" -m unittest tests.test_context_engine_contract -v # Refuse to run when the installed OpenSpec predates the archive-correctness fixes. [private] From 70739c4e0b2df7a19067fd2101546575b4c0495d Mon Sep 17 00:00:00 2001 From: Offending Commit Date: Sat, 24 Oct 2026 16:36:20 -0500 Subject: [PATCH 06/10] fix(contract): read SCHEMA_VERSION from hermes_state_common 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 --- tests/test_hermes_contract.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_hermes_contract.py b/tests/test_hermes_contract.py index 638c25a..bd29a2e 100644 --- a/tests/test_hermes_contract.py +++ b/tests/test_hermes_contract.py @@ -68,7 +68,11 @@ def _try(): run_tool_execution_middleware, ) from tools.registry import registry # type: ignore - from hermes_state import SCHEMA_VERSION, SessionDB # type: ignore + from hermes_state import SessionDB # type: ignore + try: # v0.22.5 dropped the hermes_state re-export (upstream #126164) + from hermes_state_common import SCHEMA_VERSION # type: ignore + except ImportError: + from hermes_state import SCHEMA_VERSION # type: ignore from agent.context_engine import ContextEngine # type: ignore # A stale checkout that predates plugin-owned skills is not the From b961ec81dbbac35cbb26f10e01e642e9a947022c Mon Sep 17 00:00:00 2001 From: Offending Commit Date: Sat, 24 Oct 2026 16:37:45 -0500 Subject: [PATCH 07/10] fix(contract): install the kit with its declared deps in the lanes 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 --- justfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/justfile b/justfile index 407d34d..8d183cd 100644 --- a/justfile +++ b/justfile @@ -84,8 +84,10 @@ provision-hermes-host dir venv: || { echo "no interpreter on PATH satisfies the host's requires-python $spec; add one to mise.toml" >&2; exit 1; } echo "hermes host python: $("$py" -V 2>&1) at $py (host requires-python $spec)" (cd "$host" && UV_PROJECT_ENVIRONMENT="$venv" {{ uv }} sync --frozen --quiet --python "$py") - # --no-deps: the environment stays exactly the host's lock. - {{ uv }} pip install --quiet --python "$venv/bin/python" --no-deps --reinstall-package hermes-plugin-kit . + # The kit installs with its declared deps, as the runtime image installs it. + # Packages the host lock already satisfies are left alone; v0.22.5 dropped + # PyYAML from its lock, so --no-deps left plugin_skill without `yaml`. + {{ uv }} pip install --quiet --python "$venv/bin/python" --reinstall-package hermes-plugin-kit . # Run the full contract suite against the deployed pin. # From 81dfbea2c76b249d9abdedbd79f55b1ebc225182 Mon Sep 17 00:00:00 2001 From: Offending Commit Date: Sat, 24 Oct 2026 17:07:46 -0500 Subject: [PATCH 08/10] refactor(contract): read the host spec through uv and drop narrating 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 --- hermes_plugin_kit/testing.py | 2 +- justfile | 18 +++--------------- mise.toml | 4 ---- scripts/classify_drift.py | 5 ----- 4 files changed, 4 insertions(+), 25 deletions(-) diff --git a/hermes_plugin_kit/testing.py b/hermes_plugin_kit/testing.py index 52e0b84..816ded2 100644 --- a/hermes_plugin_kit/testing.py +++ b/hermes_plugin_kit/testing.py @@ -78,7 +78,7 @@ #: #: The CI workflow keeps its own literal -- ``actions/checkout`` consumes the #: ``ref:`` before a Python toolchain exists, so the workflow cannot read this -#: value -- and a harness test asserts the two agree. +#: value. DEPLOYED_HERMES_REVISION = "939e45c91d751fadd94dcd1b873ac3cb44846213" #: Parameter names the kit's registrars pass positionally, in order. diff --git a/justfile b/justfile index 8d183cd..64025c9 100644 --- a/justfile +++ b/justfile @@ -61,16 +61,11 @@ prepare-hermes-ref ref dir: git -C "{{ dir }}" checkout -q FETCH_HEAD echo "hermes-agent at $(git -C "{{ dir }}" rev-parse HEAD)" -# Build the host's own environment from the host's own lockfile (KTD4). +# Build the host's own environment from the host's own lockfile. # # The kit's venv lacks the host's dependencies (ruamel, dotenv, ...), so the # contract modules died at import there. The host's `pyproject.toml` alone # does not resolve either, so its `uv.lock` is the only faithful source. -# -# The interpreter is the first one on PATH that satisfies the host's own -# `requires-python`. mise.toml installs one per host generation: 3.14.9 for -# v0.22.5 and later, 3.13 for the pre-upgrade deployed pin (<3.14). The venv -# lives in this repository, so the host checkout is read, never rewritten. [private] provision-hermes-host dir venv: #!/usr/bin/env bash @@ -79,14 +74,11 @@ provision-hermes-host dir venv: mkdir -p "{{ venv }}" venv="$(cd "{{ venv }}" && pwd)" test -f "$host/uv.lock" || { echo "no uv.lock in $host; cannot provision the host faithfully" >&2; exit 1; } - spec="$(python3 -c 'import sys, tomllib; print(tomllib.load(open(sys.argv[1], "rb"))["project"]["requires-python"])' "$host/pyproject.toml")" + spec="$({{ uv }} run python -c 'import sys, tomllib; print(tomllib.load(open(sys.argv[1], "rb"))["project"]["requires-python"])' "$host/pyproject.toml")" py="$({{ uv }} python find --system "$spec")" \ || { echo "no interpreter on PATH satisfies the host's requires-python $spec; add one to mise.toml" >&2; exit 1; } echo "hermes host python: $("$py" -V 2>&1) at $py (host requires-python $spec)" (cd "$host" && UV_PROJECT_ENVIRONMENT="$venv" {{ uv }} sync --frozen --quiet --python "$py") - # The kit installs with its declared deps, as the runtime image installs it. - # Packages the host lock already satisfies are left alone; v0.22.5 dropped - # PyYAML from its lock, so --no-deps left plugin_skill without `yaml`. {{ uv }} pip install --quiet --python "$venv/bin/python" --reinstall-package hermes-plugin-kit . # Run the full contract suite against the deployed pin. @@ -125,9 +117,7 @@ test-contract: test-contract-upstream # Run both contract modules and prove the run actually executed checks. # # The exit status alone cannot say whether anything ran: unittest exits 0 when -# every test skips, and on Python before 3.12 also when zero tests are -# collected. So the executed count is the evidence. The suite runs on the -# host's own environment (see provision-hermes-host). +# every test skips. So the executed count is the evidence. [private] _run-contract-suite label venv: #!/usr/bin/env bash @@ -139,8 +129,6 @@ _run-contract-suite label venv: > "$log" 2>&1 echo "EXIT=$?" >> "$log" set -e - # The whole log, not a tail: the drift classifier reads this output, and a - # tail cut the ERROR headers of an import failure and read as clean. cat "$log" grep -qE '^EXIT=0' "$log" || { echo "contract suite failed ({{ label }})" >&2; exit 1; } ran="$(grep -oE '^Ran [0-9]+' "$log" | grep -oE '[0-9]+' | head -n1)" diff --git a/mise.toml b/mise.toml index a229793..bcf48e0 100644 --- a/mise.toml +++ b/mise.toml @@ -1,7 +1,3 @@ -# The one home for this repository's toolchain. CI installs these tools with -# jdx/mise-action from mise.lock, so laptops and CI run the same Python, uv and -# Just. Run `mise trust && mise install --locked` once per checkout; the -# justfile recipes stay the entry points. min_version = "2026.10.15" [tools] diff --git a/scripts/classify_drift.py b/scripts/classify_drift.py index 6eabc7f..65654a5 100644 --- a/scripts/classify_drift.py +++ b/scripts/classify_drift.py @@ -96,11 +96,6 @@ def fingerprint(body: str) -> str: def _did_not_execute(log: str, parsed_failures: int) -> list[str]: - """Reasons the log is not evidence that the suite ran its checks. - - These need no failure block: the blocks are exactly what goes missing when - a run dies early or only part of its log arrives. - """ reasons = [ f"{module} — failed to import, so none of its checks ran" for module in dict.fromkeys(_IMPORT_FAILURE.findall(log)) From babfcf525f87d2aebf077c748950e8e4f06078b0 Mon Sep 17 00:00:00 2001 From: Offending Commit Date: Sat, 24 Oct 2026 17:28:30 -0500 Subject: [PATCH 09/10] fix(drift): count skipped contract tests as runs that checked nothing 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 --- scripts/classify_drift.py | 4 ++++ tests/test_drift_classification.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/scripts/classify_drift.py b/scripts/classify_drift.py index 65654a5..2e36a38 100644 --- a/scripts/classify_drift.py +++ b/scripts/classify_drift.py @@ -93,6 +93,7 @@ def fingerprint(body: str) -> str: _IMPORT_FAILURE = re.compile(r"^ImportError: Failed to import test module: (\S+)", re.M) _RAN = re.compile(r"^Ran (\d+) tests?\b", re.M) _EXIT = re.compile(r"^EXIT=(\d+)\s*$", re.M) +_SKIPPED = re.compile(r"^(?:OK|FAILED) \(.*\bskipped=(\d+)", re.M) def _did_not_execute(log: str, parsed_failures: int) -> list[str]: @@ -105,6 +106,9 @@ def _did_not_execute(log: str, parsed_failures: int) -> list[str]: reasons.append("no `Ran N tests` line — the suite never completed") elif int(ran[-1]) == 0: reasons.append("Ran 0 tests — nothing was checked") + skipped = sum(int(n) for n in _SKIPPED.findall(log)) + if skipped: + reasons.append(f"{skipped} contract tests skipped — they checked nothing") exits = [int(code) for code in _EXIT.findall(log)] if not exits: reasons.append("no `EXIT=` line — the run's outcome is unknown") diff --git a/tests/test_drift_classification.py b/tests/test_drift_classification.py index ddbe311..7a2e7ea 100644 --- a/tests/test_drift_classification.py +++ b/tests/test_drift_classification.py @@ -214,6 +214,14 @@ def test_a_non_zero_exit_with_no_parsed_failure_is_new_drift(self) -> None: self.assertTrue(new) + def test_a_run_that_skipped_contract_tests_is_new_drift(self) -> None: + """A skipped contract test checked nothing, whatever the exit said.""" + log = "\n" + "-" * 70 + "\nRan 17 tests in 0.601s\n\nOK (skipped=3)\nEXIT=0\n" + + _, new = classify_drift.classify(log, {}) + + self.assertTrue(any("skipped" in entry for entry in new), new) + def test_a_clean_run_reports_no_drift(self) -> None: log = ( "test_x (tests.test_hermes_contract.HermesContractTests.test_x) ... ok\n" From a3ce33c13ff03c403996a853b34bf62216caacfc Mon Sep 17 00:00:00 2001 From: Offending Commit Date: Sat, 24 Oct 2026 17:28:30 -0500 Subject: [PATCH 10/10] ci: pin the mise binary every mise-action step installs 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 --- .github/workflows/hermes-drift.yml | 4 ++++ .github/workflows/release.yml | 16 ++++++++++++++++ .github/workflows/test.yml | 12 ++++++++++++ mise.toml | 2 +- 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/.github/workflows/hermes-drift.yml b/.github/workflows/hermes-drift.yml index 8c1bacc..ee6a758 100644 --- a/.github/workflows/hermes-drift.yml +++ b/.github/workflows/hermes-drift.yml @@ -43,6 +43,10 @@ jobs: path: .hermes-agent-upstream - name: Install the locked toolchain (mise.toml) uses: jdx/mise-action@82a612c5ab8bcda7cc5db6b7e81a8e7584c3dc6e # v5.2.3 + with: + # Pinned so every run, release jobs included, installs the same mise; + # the action still verifies it against mise's signed checksums. + version: "2026.10.18" - name: Record the upstream revision under test id: upstream run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e1668e9..3f16b84 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,6 +55,10 @@ jobs: ref: ${{ github.sha }} - name: Install the locked toolchain (mise.toml) uses: jdx/mise-action@82a612c5ab8bcda7cc5db6b7e81a8e7584c3dc6e # v5.2.3 + with: + # Pinned so every run, release jobs included, installs the same mise; + # the action still verifies it against mise's signed checksums. + version: "2026.10.18" - name: Attach the exact trigger to local main env: EXPECTED_SHA: ${{ github.sha }} @@ -172,6 +176,10 @@ jobs: ref: ${{ github.sha }} - name: Install the locked toolchain (mise.toml) uses: jdx/mise-action@82a612c5ab8bcda7cc5db6b7e81a8e7584c3dc6e # v5.2.3 + with: + # Pinned so every run, release jobs included, installs the same mise; + # the action still verifies it against mise's signed checksums. + version: "2026.10.18" - name: Download the immutable release source uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -221,6 +229,10 @@ jobs: ref: ${{ github.sha }} - name: Install the locked toolchain (mise.toml) uses: jdx/mise-action@82a612c5ab8bcda7cc5db6b7e81a8e7584c3dc6e # v5.2.3 + with: + # Pinned so every run, release jobs included, installs the same mise; + # the action still verifies it against mise's signed checksums. + version: "2026.10.18" - name: Download the immutable release source uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -298,6 +310,10 @@ jobs: ref: ${{ github.sha }} - name: Install the locked toolchain (mise.toml) uses: jdx/mise-action@82a612c5ab8bcda7cc5db6b7e81a8e7584c3dc6e # v5.2.3 + with: + # Pinned so every run, release jobs included, installs the same mise; + # the action still verifies it against mise's signed checksums. + version: "2026.10.18" - name: Install ORAS uses: oras-project/setup-oras@1d808f7d7f6995cc68b7bf507bfe5c5446e1dc9d # v2.0.1 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1fb9664..98c499e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,6 +16,10 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install the locked toolchain (mise.toml) uses: jdx/mise-action@82a612c5ab8bcda7cc5db6b7e81a8e7584c3dc6e # v5.2.3 + with: + # Pinned so every run, release jobs included, installs the same mise; + # the action still verifies it against mise's signed checksums. + version: "2026.10.18" - name: Run unittest suite run: just test @@ -25,6 +29,10 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install the locked toolchain (mise.toml) uses: jdx/mise-action@82a612c5ab8bcda7cc5db6b7e81a8e7584c3dc6e # v5.2.3 + with: + # Pinned so every run, release jobs included, installs the same mise; + # the action still verifies it against mise's signed checksums. + version: "2026.10.18" - name: Build and validate package metadata run: | just build @@ -54,6 +62,10 @@ jobs: path: .hermes-agent-deployed - name: Install the locked toolchain (mise.toml) uses: jdx/mise-action@82a612c5ab8bcda7cc5db6b7e81a8e7584c3dc6e # v5.2.3 + with: + # Pinned so every run, release jobs included, installs the same mise; + # the action still verifies it against mise's signed checksums. + version: "2026.10.18" # The full suite, not the context-engine module alone. It is green at # this pin, so widening adds coverage rather than risk. - name: Run the full contract suite against the deployed Hermes revision diff --git a/mise.toml b/mise.toml index bcf48e0..e89f898 100644 --- a/mise.toml +++ b/mise.toml @@ -1,4 +1,4 @@ -min_version = "2026.10.15" +min_version = "2026.10.18" [tools] # Must equal the Python in the deployed Hermes image (upstream pm/lock.json).