Changelog#

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Unreleased#

Changed#

  • The Linux packages carry the family’s own GStreamer (owner rule 2026-10-17: never the distro’s). packaging/app.json names gst-launch-1.0, five plugins (libcamera optional) and the seven elements --backend gstreamer uses; the hub’s packager bundles them, proves each element loads from the package alone, and the launchers put the bundled tool first on PATH. get_gstreamer_env passes every GST_ variable on, so gst-launch-1.0 gets that environment.

  • The Linux packages start on Debian 13 and Ubuntu 24.04: the image’s glibc travels with the bundle’s Python, and the .deb no longer asks for libc6 (>= 2.43).

  • The README logo is the family’s ant (2026-10-17, owner: one icon in every repository), drawn from OmniAccelerANT’s assets/icons/kataglyphis_app_icon.svg.

0.0.32 - 2026-10-17#

  • The default model is ModelANT’s DEIM-D-FINE-N (Apache-2.0), fetched by checksum; the tracked yolov26m.onnx is gone (2026-10-17). It was Ultralytics YOLO26, which is AGPL-3.0: an MIT project cannot ship it, and 78 MiB of weights did not belong in a code repository. Its history stays as it is (owner decision 2026-09-15). resources/models/models.lock names the .onnx, its LICENSE and its NOTICE by SHA-256 and URL, with OxidANT’s format and entries, and the new scripts/fetch_models.py puts them beside it: stdlib only, one implementation for Linux and Windows, with OxidANT’s fetch-models.sh semantics (--dest, --lock, a cache keyed by checksum in $KATAGLYPHIS_MODEL_CACHE, else the XDG cache or %LOCALAPPDATA%, both copies checked, .part writes, mode 644). ci_packaging.sh, Build-Windows.ps1 and Build-WindowsArm64.ps1 run it before packaging, and the packages ship all three files, since Apache-2.0 asks for the NOTICE to travel with the model. A missing model now names that command, in the self-test’s JSON and in the log.

    • A plain resize for models that ask for one. DEIM-D-FINE-N wants its 640x640 input stretched, not letterboxed, and says so in its ONNX metadata (kataglyphis.preprocess=stretch). The monitor and the self-test read it (wants_stretch), preprocess(..., stretch=True) resizes each axis on its own, and DecodeConfig.scale takes an (x, y) pair to undo it. Any other model keeps the letterbox, so a YOLO export passed with --model decodes as before; the [1, 300, 6] output is the layout the generic decoder already read. The self-test reports which one it used ("preprocess"). The window title, the banner and the CLI description no longer say YOLOv10.

    • In :latest on arm64: on OxidANT’s two_cats.jpg the monitor’s own pre/postprocess, from source and from the packaged bundle’s compiled modules alike, finds both cats (class 15) at 0.909 and 0.604 over its default --conf 0.5. ci_packaging.sh fetched the model from GitHub into a fresh container, and the tar.gz, deb and AppImage self-tests passed with it. The seven lint gates and the six static-analysis gates pass.

0.0.31 - 2026-10-16#

  • Every push to main releases v<version>, and only when all five lanes are green (2026-10-16, release.yml, the family’s rule in ANTfrastructure’s branch-and-release-strategy.md). The release carries what the pushed v* tags did, every lane’s packages/ (the .deb, AppImage and tarball of both Linux arches, both Windows arches’ MSI, MSIX, zip and test certificate), now from one run: scripts/linux/collect-release-assets.sh, whose suite runs with Linux x64 on every push, gathers them and refuses two files of one name. The tag triggers and the four lanes’ release jobs are gone, and with them the contents: write that on-request.yml granted. The hub moves to 0910c9ba, whose Windows app bundle leaves the PyPI onnxruntime out of the lock export: the image’s CPython 3.15 has no PyPI wheel of it, which stopped Windows x64’s packaging (520aca2a). Windows arm64’s packaging stopped at pyyaml on 3.15, which has no cp315 win_arm64 wheel; the hub moves on to a7800f00, whose cross bundle builds it as a pure-Python wheel from uv.lock’s hash-checked sdist.

  • A push runs Linux x64 and the gates; Linux arm64, Linux riscv64, Windows x64 and Windows arm64 run on request (2026-10-16), the family’s CI rule (ANTfrastructure’s branch-and-release-strategy.md) that OmniAccelerANT, OxidANT, AccelerANTgine and BeschleunigerBallett follow. on-request.yml starts the lanes a pushed commit message asks for with the ci token (windows, arm64, riscv64, all); every lane keeps its Run workflow button, and a pushed v* tag still builds and attaches the release packages as before. Each lane’s concurrency group names the lane and its caller, so a push, a request and a run by hand never cancel each other.

  • ruff 0.17.0, ANTfrastructure 3a0ab1c3 -> 0768e5e0, and the 2026-10-13 updates; the ty gate is green again. The hub’s tool batch moves RUFF_VERSION to 0.17.0, so pyproject.toml, .pre-commit-config.yaml and uv.lock follow it in the same commit, as the consumer-pins gate requires. The lock’s maintenance moves nine more packages, mlflow 3.17.1 among them. mlflow still caps pyarrow below 26, so the override stays. onnxruntime-genai stays at 0.17.0, the hub’s version, because 0.17.1 is on PyPI but has no tag on GitHub. benchmarks.yml takes setup-uv v10.3.0 and ollama 0.40.3, both by digest. ty had flagged LaneProcess since the lock moved to ty 0.0.86: it read listens_here as always None, because _find_listener set it as a side effect. That failed static analysis on every lane. _find_listener now returns the pair, and its tests pass unchanged. docs/source/monitoring.md takes the formatter’s blank line in a code block, so the pre-commit hooks pass. In :latest: the seven lint gates and the six static-analysis gates pass. The tests pass on 3.14 with the chain ORT (2548 passed, 75 skipped), on 3.14t (2542, 77) and on CPython 3.15.0 with the test extra (2541, 78).

  • OrchestrANT installs on the images’ Python 3.15. The lock is regenerated with uv 0.13.0 (uv lock --upgrade): numpy 2.5.4, scipy 1.18.1, contourpy 1.4.0, pyarrow 26.0.0 and the other compiled dependencies now carry cp315 wheels. Three changes keep it installable and correct there:

    • [tool.uv] override-dependencies = ["pyarrow>=26"]: mlflow 3.17.0 caps pyarrow below 26, and no pyarrow below 26 has a cp315 wheel. mlflow uses pyarrow only in its HDFS artifact store, whose pyarrow.fs API 26 keeps.

    • py-cpuinfo2>=10.1 replaces py-cpuinfo: pytest-benchmark 5.3 moved to the fork, and both install cpuinfo/. orchestrant.yolo.monitor imports the same cpuinfo.get_cpu_info.

    • ai-edge-litert applies below 3.15 only: PyPI has no cp315 wheel or sdist (2.3.0, 2.4.0.dev nightlies), and the images ship one built from source. The Windows scripts name no Python any more: Build-Windows.ps1 runs the image’s pinned X.Y and X.Yt, and static analysis, packaging and the arm64 lane take the pin through the hub’s Get-PinnedPythonVersion (ANTfrastructure 97118139 -> 156f79f4, CON91). Proven on the host: uv lock --check with uv 0.13.0; the base and test extras on CPython 3.14.8 pass 1725 tests, 45 skipped; a dry-run of the image torch stage’s sync on CPython 3.15.0 resolves 180 packages.

  • The thread-safety tests on the installed cp314t wheel run through the hub’s step. scripts/linux/ci_packaging.sh calls the hub’s test-free-threaded-wheel.sh (hub ff91d46d, CON66) with this repo’s knobs, in place of the local ci_free_threaded_wheel_tests.sh, which is gone. Same venv, same tests, same ORCHESTRANT_REQUIRE_COMPILED=1 and python -I; the lock’s set now comes from uv export --locked. A local run of the wrapper in :latest proved 64 compiled modules and passed 8 tests, 1 skipped (no free-threaded OpenCV).

  • import orchestrant no longer loads matplotlib.pyplot: 86 ms instead of 800 ms on x64. pyplot was 0.72 s of the 0.80 s, and under riscv64 QEMU that import cost 31-37 s per subprocess (bench_agent.py --list took 41.9 s against a 60 s timeout). MetricsPlotter imports it on its first plot; the tests that patch matplotlib.pyplot.* are unchanged, and a new one fails when the package import loads pyplot again (checked against an eager import). Hub CON83’s other two calls stay as they are: bench grading keeps its pytest plugins, since benchmarks/tests already disables their autoload for its nested runs and a native run pays 1 s; and the process ceiling stays probed once, so it cannot drift between launch and assertion.

  • Every compiled module builds with clang-cl on Windows; the first one used MSVC’s cl until now. ClangBuildExt set the compiler’s cc to clang-cl before the first compile(), which then ran initialize() and put cl.exe back. So orchestrant/dummy.c compiled with MSVC’s cl in every Windows build, GIL and free-threaded, x64 and the arm64 cross lane (run 38221408508: D9002 on its --target). build_extension now initializes the compiler first. The spawn hook that was meant to catch this is gone: setuptools 84, which the isolated build resolves, never calls it. A local run of the Windows x64 lane compiled all of them with clang-cl and none with cl.exe.

  • The Windows arm64 cross lane ships the cp314t wheel too, and the device proves it. The hub (f15de3c9, CON75) builds it on the cross lane with the image’s free-threaded trees, gates it, and packs it with a device kit in dist/windows-arm64/free-threaded. The windows-11-arm run job then loads every compiled module with the GIL off.

    • setup.py adds --target=aarch64-pc-windows-msvc to the extensions’ compile arguments when VSCMD_ARG_TGT_ARCH is arm64. Without it, ClangBuildExt’s clang-cl compiled for the host.

    • In :winarm64, orchestrant-0.0.30-cp314-cp314t-win_arm64.whl built this way and passed the tag, import and ARM64 gates.

  • The Windows x64 app bundle builds again, and the arm64 test staging asks only the image.

    • Run 38086634222 failed the bundle’s arch gate on four violations, all in the runtime’s ensurepip pip wheel: distlib’s I386 and ARM64 launcher templates. The hub pin moves to 712ba4e6, whose gate reports such templates instead of counting them. A local run of this lane on that gate passed every step, the app bundle and installers included.

    • scripts/windows/Stage-Arm64Tests.ps1 installs the pytest stack only from the :winarm64 wheel store and stops when it lacks one, instead of falling back to PyPI (hub CON67). Run 38086634226 already staged from that store.

  • The compiled modules are audited for free-threading, and CI runs the cp314t wheel from eight threads. The cp314t wheel has been built and proved on both Linux lanes since the hub pin 4f0cb7f7: 64 compiled module(s) of orchestrant loaded on free-threaded 3.14.8; the GIL stayed disabled, run 38086634220. That proof creates each module without running its body. The 3.14t test leg ran the source, so no compiled code ever ran with the GIL off. The audit found five places where module or instance state was shared without a GIL-free guard:

    • gateway._SERVED, the process-wide reply tally, counted with a bare Counter +=. Concurrent post_json replies lost counts. It now takes a lock, and served() reads under the same lock.

    • openai_api.gpu_probe(), the one-per-process GPU probe: two first callers each opened a probe and leaked one. It is now double-checked under a lock.

    • The GStreamer reader queued a view of its reusable readinto buffer, which the next read overwrote while the consumer still held it. It now queues a copy. The reader also raised queue.Full when open() put its probe frame back concurrently, and read self.process three times while release() set it to None. Both paths now go through _offer_frame and a single read of process.

    • streaming.FrameCapture.get_frame checked empty() and then called get_nowait(). A second stream on the same capture could take the frame in between and kill the first with queue.Empty. It now catches Empty, and the producer uses put_nowait.

    • The monitor read the log deque with list() while loguru sinks on other threads appended to it, which raises deque mutated during iteration. The new orchestrant.pipeline.logging.log_buffer_lines copies it under the deque’s own lock.

    tests/unit/test_thread_safety.py drives each of these from eight threads, on every leg. It also checks that importing every module leaves the GIL off on a free-threaded interpreter. pipeline.capture now resolves its exports lazily, as pipeline and yolo already did. That lets the GStreamer backend import without OpenCV, which has no cp314t wheel, so the 3.14t leg runs these tests too. scripts/linux/ci_packaging.sh then runs scripts/linux/ci_free_threaded_wheel_tests.sh. That step runs the same file against the installed cp314t wheel on 3.14t, and fails when a module under test came from source (AGENTS.md § 2). openai_api.py changed, so the lab’s tool_sha256 changes with it. Measured locally in :latest:

    • Before the fixes, the file failed 6 of its tests against a cp314t wheel, and 5 on the 3.14t source leg (lost gateway counts, 8 probes instead of 1, an overwritten frame, the reader’s 'NoneType' object has no attribute 'stdout', deque mutated during iteration). It failed 4 on 3.14 with the GIL.

    • After them, the wheel step passes 8 tests with 1 skipped, and the GIL stays off.

    • A wheel built without freethreading_compatible fails at import: The global interpreter lock (GIL) has been enabled to load module 'orchestrant'.

  • The riscv64 lane’s 20 failures, and the 3.5 h test leg that ran it into the 6 h job limit. Three causes, none of them riscv64 code (run 37520541295):

    • RLIMIT_NPROC counts every task of the UID, and a container’s /proc shows only its own. In CI the runner agent is uid 1001 as well, so the candidate ceiling (visible tasks + 64) sat below the real count. Under qemu-user every candidate’s start creates a QEMU thread, and 17 tests died with qemu_thread_create: Resource temporarily unavailable. bench_coding._nproc_ceiling now takes the kernel’s own count, measured in a fresh interpreter by starting a thread under trial ceilings. With 80 tasks of uid 1001 in a second container, the five affected modules went from 126 failures to none under the riscv64 image.

    • qemu-user accepts a guest’s RLIMIT_AS and drops it, so a candidate there had no address-space ceiling (2 tests). The grader notices that the limit does not read back and hands the same ceiling to QEMU as QEMU_RESERVED_VA; the report’s grader_selfcheck.rlimits.as_enforced_by says which held. A new test proves on every arch that twice the ceiling fails and a quarter of it does not.

    • Every nested python -m pytest in benchmarks/tests loaded this venv’s eight pytest plugins: 49 of its 60 s under QEMU, pytest-md-report’s chardet import alone 32 s. The 600 s self-test timed out on it (the 20th failure). The suite’s conftest now sets PYTEST_DISABLE_PLUGIN_AUTOLOAD for its subprocesses; the toy repos they grade use no plugin. The two agent-bench modules, 11575 s of the lane’s 12751 s, took 3806 s under the riscv64 image on the dev box (the self-test 340 s), all passing. bench_coding.py’s tool_sha256 changes with it. The riscv64 wheel’s cross link is the hub’s to fix: it took the host libc.so and failed (run 37329297990).

  • A proved free-threaded wheel beside the GIL one. pyproject.toml drops Programming Language :: Python :: 3.14t, which is no trove classifier (PyPI refuses an upload that names it), for the official Programming Language :: Python :: Free Threading :: 2 - Beta: Beta, because the 3.14t test legs gate. The hub’s packaging drivers read that classifier: once the hub pin carries them, they build a second Cython wheel on the image’s 3.14t (cp314-cp314t), then load its 64 compiled modules in a fresh 3.14t venv and fail when the GIL comes back on. setup.py therefore sets Cython’s freethreading_compatible=True (cython>=3.1, the first with the directive). Proved locally with the hub’s new drivers:

    • :latest ships orchestrant-0.0.30-cp314-cp314{,t}-manylinux2014_x86_64.manylinux_2_17_x86_64.whl. The hub now runs the packaging extra’s auditwheel, so the Linux binaries are repaired for the first time; until now they shipped as linux_x86_64.

    • :winamd64 ships orchestrant-0.0.30-cp314-cp314{,t}-win_amd64.whl. Both app bundles still take the cp314 wheel.

    • The suite against the installed cp314t wheel on 3.14t ends with sys._is_gil_enabled() False and the same results as the cp314 wheel on 3.14: Linux 2418 passed, Windows 1637. The failures both wheels share (57 and 25, plus 3 errors) come from testing a source-stripped compiled install, such as run-start source hashes and subprocesses on the image’s /opt/venv, not from free-threading.

  • Dependencies follow the hub’s 2026-10-07 Renovate bumps (hub 07388b85, which this repo now pins): torch 2.14.1 and torchvision 0.29.1 in every extra and the riscv64 git sources, onnxruntime-genai(-cuda) 0.17.0 (the newest version tagged on GitHub; PyPI’s 0.17.1 has no tag the image could build), ruff 0.16.10, and in uv.lock iree-base-compiler/runtime 3.12.0 and onnxruntime 1.30.0. The image’s torch stage and the consumer-pins gate compare these with the hub’s versions.env, so they move with the hub pin.

  • The arm64 test staging installs pytest and its plugins offline from the image’s wheel store when PYTHON_WHEELS carries them (hub CON67: 31 cp314 wheels, SHA-pinned in the hub’s versions.env). An image published before that still resolves them from PyPI, and the log says which source it used. Proven on the host: the 33 pinned wheels resolve the whole set with --no-index.

  • benchmarks/tests joins testpaths, so every lane runs the LLM lab’s offline suite, not only the path-filtered x64 job in benchmarks.yml, which is gone. Its ten harness modules skip on Windows with a reason: the candidate sandbox needs POSIX rlimits, process groups and shell fixtures. On Windows the suite runs 795 tests (20 skipped); in :latest both Linux legs run 2531.

  • Fixed while joining: the suite now runs under the project’s filterwarnings = ["error"], which turned resource leaks collected after the session into a failed run.

    • client.http_error_detail and openai_api._get_json close the HTTPError they handle; against a real server, an unclosed one holds its connection until GC.

    • The offline guard closes the socket it refuses, which socket.create_connection does not do for a non-OSError.

    • benchmarks/tests/conftest.py closes every HTTPError a test made. Its blanket ResourceWarning ignore now marks only its own directory: pytest_collection_modifyitems sees the whole session, so it had applied to tests/ too.

  • A riscv64 lane: .github/workflows/linux-riscv64.yml runs the riscv64 image under QEMU on an amd64 runner and pytest on it (tests only, test-extras: test). It needs ANTfrastructure 63638861, which the pin moves to.

  • uv.lock now covers riscv64: [tool.uv] environments names it as a real environment, torch/torchvision resolve through the git sources, and pytorch-custom is empty there on purpose. Every non-riscv64 row says platform_machine != 'riscv64' to keep impossible cross-products out.

0.0.30 - 2026-10-02#

The first release whose tag attaches the arm64 packages to the release itself: the Windows arm64 lane’s zip, MSI and MSIX and the Linux arm64 lane’s tar.gz, deb and AppImage, each proven in CI today. Also new under it: the device pytest run now reports, measures and benches like x64’s (TESTS: passed=921 failed=0 skipped=40 on windows-11-arm), with the reports uploaded from the device.

Added#

  • MSIX for Windows, and the Linux packages for arm64 too.

    • The MSIX puts all three commands on PATH as execution aliases. It is signed with a test certificate, which is shipped next to it as …-test-signing.cer; trust that certificate first, as described in README § Installation.

    • The arm64 CI leg now builds its own tar.gz, deb and AppImage.

    • CI proves the MSIX unpacked, and by its signature against that .cer. Server Core cannot install an MSIX.

  • Windows arm64: the zip, MSI and MSIX (windows-arm64-cross.yml, “Windows arm64 · cross build + test”; renamed from “… + run” on 2026-10-02, now that the arm64 job runs the pytest suite, not a launch smoke only). They are cross-built in the hub’s :winarm64 image from the pure wheel. A windows-11-arm runner then starts the bundle and the unpacked zip with yolo-monitor --self-test. With :winarm64 rebuilt without NVIDIA (owner decision 2026-10-01; the stack belongs in :winarm64-nvidia) the packages are zip 204 MB, MSI 167 MB and MSIX 207 MB (run 36876194988). Before the rebuild, the arm64 OpenCV’s CUDA modules made them about 900 MB.

Changed#

  • CI follows the fleet’s workflow convention. ubuntu-26.04-amd64-arm64.yml is split into linux-x64.yml (“Linux x64 · build + test”) and linux-arm64.yml (“Linux arm64 · build + test”), one arches row each. Both attach their packages to a v* release. Every lane starts from permissions: contents: read, and the README badges are in the fleet order.

  • The Windows bundle carries one ONNX Runtime: OpenCV’s DNN module now loads the one in onnxruntime\capi instead of a second copy (561 → 550 MB unpacked).

Fixed#

  • tests/conftest.py (then tests/unit/conftest.py) selects matplotlib’s Agg backend through MPLBACKEND. This stops TkAgg crashing the Windows CI container. The Benchmarks workflow’s frontend suite, which runs without matplotlib, loads again; on the v0.0.29 tag it had failed on that import.

  • Every lane runs the whole configured suite, and 3.14t really tests (2026-10-01).

    • The suite: CI used to run tests/unit only. It now runs testpaths, so tests/integration runs too: through the hub on Linux, and through a bare pytest in Build-Windows.ps1.

    • 3.14t: the free-threaded leg used to sync nothing and pass on a warning. It now syncs only the test extra and gates. To get there:

      • atheris is gone: it was unused and has no cp314t wheel.

      • orchestrant.yolo loads its re-exports lazily.

      • yolo/core/preprocess.py imports OpenCV only when it resizes. With those, the pure-numpy tests import without cv2, and only the OpenCV/ORT-bound module skips. Measured on free-threaded 3.14.7 in :latest: the whole unit suite passed.

    • Windows arm64 runs the pytest suite on windows-11-arm, under the arm64 bundle’s own python.exe, so it tests the shipped package (scripts/windows/Stage-Arm64Tests.ps1, hub 65d61617). The stager also copies speed_summary.py and answers.py, which test_benchmark_data.py loads by file path. Without them the first arm64 run stopped there at collection. Since 2026-10-02 the staging also carries x64’s coverage and report plugins (pytest-cov/pytest-md/pytest-md-report/pytest-html, plus coverage — all available as cp314 win_arm64 wheels, resolved against a +gil interpreter because a bare 3.14 can pick a free-threaded build whose cp314t .pyds would not load) and runs bench/demo_pytest_benchmark.py as a second manifest entry, so the device pytest run now reports, measures and benches like x64’s. The device’s reports leave the runner through the hub’s new test-results-path upload (orchestrant-windows-arm64-test-results, hub 305a1c92). With no __init__.py beside them they cannot shadow the installed package.

    • The conftest moved to tests/conftest.py. With testpaths, pytest loads tests/unit/conftest.py only once it reaches tests/unit. By then tests/integration’s import orchestrant has already imported matplotlib, which reads MPLBACKEND once, at import. Both Windows x64 legs then drew with TkAgg and crashed in test_metrics_plotter.py (0xC0000005). Before the whole suite ran there, the unit conftest loaded first. That crash only came to light once every leg gated.

    • Two tests close their HTTPErrors. An unclosed one warns when it is collected, and the free-threaded leg collects it after the session, where filterwarnings=error turned it into exit 1 although every test had passed.

Security#

  • The 44 open Dependabot alerts in uv.lock are fixed (2 critical, 27 high):

    • gitpython 3.1.51 → 3.2.0;

    • mlflow 3.14.0 → 3.16.1;

    • virtualenv 21.6.1 → 21.14.2;

    • urllib3 2.7.0 → 2.8.0;

    • cryptography 48.0.1 → 50.0.2;

    • sqlparse 0.5.5 → 0.6.0;

    • aiohttp 3.14.1 → 3.14.3;

    • soupsieve 2.8.4 → 2.10.

    Dependabot cannot raise them itself here. Its updater fails on the git source pytorch/vision (private_source_authentication_failure), so its security-update runs stay red while an alert is open.

0.0.29 - 2026-10-01#

Added#

  • Installable packages: MSI and zip on Windows; deb, AppImage and tar.gz on Linux. packaging/app.json opts this repo into the hub’s Python app bundles. The CI lanes build a relocatable folder with its own CPython, the locked wheels, the chain ONNX Runtime and the model. They wrap it as those five packages and start each one once with yolo-monitor --self-test. The MSI and the deb are installed, started and removed. A v* tag attaches the packages to its GitHub release. Measured on 2026-10-01: MSI 209 MB, zip 254 MB, deb 213 MB, AppImage 248 MB, tar.gz 266 MB.

    • Windows ships the image’s OpenCV, which does not import Media Foundation, so the Windows CI test venvs use it too. PyPI’s cv2 did not import in the Server Core container at all.

    • The Linux packages need glibc 2.43 (Ubuntu 26.04+), because the bundled chain ONNX Runtime asks for it.

  • yolo-monitor runs from any directory: the first step toward packaged builds. Until now it read the model from resources/models/ in the working directory and logged to logs/ there, so it worked only from a checkout.

    • Model. orchestrant/paths.py resolves it from $ORCHESTRANT_MODEL, then the install’s share/orchestrant/models/, then the checkout.

    • Logs. A packaged install logs to the user’s state dir.

    • Execution provider. It is chosen from what ONNX Runtime offers: CUDA, DirectML, then CPU. DirectML was never tried before.

    • --self-test. It runs one inference on a blank frame and prints JSON, so CI can install-test a package without a camera or display.

    • app extra. It holds what yolo-monitor imports beyond the core: numpy, OpenCV and ONNX Runtime. Until now those came only with an ml-ai* extra.

      • It conflicts with every ml-ai* extra: each brings its own OpenCV and ONNX Runtime, and uv cannot fork opencv-python across both.

      • It asks for opencv-python>=5. Unversioned, uv’s win32 fork took ml-ai’s riscv64 git build (4.13), which then had to compile.

      • CI still syncs ml-ai and leaves app out.

  • The hub’s Windows rocm image builds this repo’s torch. ANTfrastructure 2da6c29a compiles torch 2.14.0 / torchvision 0.29.0 from source against ROCm 10.0 (gfx1200, gfx1201), because AMD’s ROCm 10.0 Windows wheels stop at 2.13.0 and the image refused to pair them with this lock. AGENTS.md § 4’s torch bullet says which hub image takes which wheel line. Nothing here changes. At 9427c7a0 the first rocm build of it is green: this repo’s develop (705e769) passed its wheel smoke on the source-built torch (13/15, no failures), and the image’s smoke gate passed.

  • The gateway’s acceptance: the lab measured through APISIX (roadmap P9.1; benchmarks/docs/gateway-acceptance-2026-09-25.md, raw files and a derive snapshot in benchmarks/benchmark_results/2026-09-25-gateway-acceptance/).

    • Transparency: on raw-npu and raw-gpu no contract answer changed. All 42 tool cases got the direct verdict and 41 byte-identical replies. The NPU paid a median 10 ms to first token, and decode is unchanged.

    • Shaping: lab-chat scored what the NPU with the tools prompt scored (41 ties). It answered the tool case the NPU refuses and the ~7k-token documents, on the GPU.

    • Explained: the refusal left open in P8.1 is context_length_exceeded, at 4353 prompt tokens.

    • Found: the size rule sends the ~3.1k-token documents to the GPU too, at about 12× the NPU’s time. P9.2 sets out the owner’s decision.

    • The procedure now says to run a gateway bench_tools or bench_chat with the direct run’s --label, so bench_compare pairs them.

  • scripts/linux/serve-stack.sh: the lab’s gateway, the eighth thin wrapper (P1 of the APISIX design). It runs ANTfrastructure’s linux/llm-stack/scripts/serve-stack.sh (keys, up, status, reload, down: APISIX on 127.0.0.1:9080 in front of the GenieX lanes, from WSL) with the renderer pointed at benchmarks/prompts/, whose tools prompt the registry pins by sha256, and at LLM_BACKENDS when that is set, so the gateway serves the registry the lab reads. AGENTS.md § 4 gains two pitfalls: stop the gateway during a direct-lane campaign, and lab-raw-* is not byte-transparent.

  • A report taken through the gateway says which lane served, and on what. A lab-* backend’s base URL is the gateway, and such a report recorded runtime: null. Now runtime_info() answers a gateway alias with its primary lane’s runtime, found exactly as a direct run of that lane finds it (the lane map is the registry’s serving block), so lab-raw-npu and geniex-npu reports compare like for like. The new provenance.gateway block (orchestrant/benchmark/gateway.py; null on a direct lane) names the lanes the alias reaches, the overflow lane’s own runtime, the running gateway’s /gateway/info shas and whether it was rendered from the registry the lanes were read from (registry_matches), and served: the lane every reply named in its X-Gw-Lane/X-Gw-Rerouted headers, counted in client.post_json. compare() names a gateway run against a direct one, another alias, a changed gateway and runs that other lanes served, and it drops its “served models differ” note between a gateway run and a direct one (the gateway’s /v1/models is its alias list). Nothing changes for a direct lane: only a base URL equal to the registry’s serving.gateway.listen is treated this way, and only its static /gateway/info is asked anything. benchmarks/README.md (Through the gateway) lists the lab-* backends, what a report records, what the gateway changes on the way through, and the P1 acceptance procedure. What gateway.py reads crosses the gitlink, so TestAgreesWithTheHub holds it to the pinned hub: every alias (and raw-* route) reaches the lanes the renderer routes it to, every registry entry at the gateway names a lane, /gateway/info carries each key a report keeps, and geniex-shape sets the two headers served counts.

  • orchestrant-bench depth: the decode-at-depth trace as a lab tool (roadmap P8.4, the campaign’s defect 6). The 2026-09-24 depth traces came from a scratch script that was never stored and recorded no provenance. orchestrant/benchmark/depth.py sends the same warm-up, prompt (the contract’s filler at seed 7, pinned by sha256 in the tests) and streamed request, cuts the same windows and writes the same six fields, so the tracked *-depth-8k.json files compare with new traces. The report uses the shared envelope (runtime, model files, tool_sha256 over depth.py and answers.py), reads the host load over the 30 s rest before the measured request, keeps the raw delta times, and gives a burst window a null rate. A server error inside the stream, a timeout, an HTTP error or a reply with no generated token is recorded in the row’s error and exits 1; the report is written first.

  • Every report’s provenance records its command line (argv, defect 6). Credentials are redacted (client.redact_argv): a flag named for one loses its value, and a value shaped like one — a provider key prefix (sk-, glpat-), a token prefix (hf_, ghp_, github_pat_), a bearer header, a key-named URL parameter, a URL’s password — is replaced under any flag. provenance.compare() does not read it.

  • The roadmap’s P8 measurements, written up as § P8 of benchmarks/docs/roadmap-campaign-2026-09-24.md, over benchmarks/benchmark_results/2026-09-25-p8/ (reports, the chain script and a derive.py.snapshot that recomputes every number). The tool gap narrows to one case, no longer separable, and the recommendation stands for tool calling (P8.1): with prompts/tool-disambiguation.md the Qwen3-4B-Instruct NPU bundle passes 40 of the 41 cases it answered and the same model’s GGUF on the CPU lane 42/42 — 1 case to 0, p = 1.0, +2.4 points [−2.3, +7.2] — where without the prompt the GGUF had won 7 to 0; the prompt fixed all eight of the bundle’s misses and cost it one. Not separable is not equal: the point estimate still favours the GGUF, and the bundle refused the suite’s longest request with a 400, as in 10 of the 12 draws of that case tracked since 2026-09-23. The instruct GGUF answers the ~7k-token chat documents the bundle refuses (P8.2: 32/33, not separable from the bundle or the thinking 4B). In one trace per lane, taken on different days, the Adreno GPU lane decodes the 9B distill at 0.52–0.54× the CPU lane’s rate after 7.2k tokens instead of doubling it (P8.3), so the agent stays on the CPU lane. On the default tool set, without opencode’s preamble and schemas, the 9B passes 33 of the 34 cases it passed 27 of under them (P8.5: 7 to 1, p = 0.070, −17.6 points [−33.1, −2.2]). The roadmap marks P8.1–P8.3 and P8.5 measured.

  • benchmarks/docs/roadmap-campaign-2026-09-24.md — the roadmap campaign of 2026-09-24/25, written up over its raw reports (benchmarks/benchmark_results/2026-09-24-roadmap/ and 2026-09-24-upgrade-check-v070/). Every number is recomputed from them by one script stored beside them, derive.py.snapshot, with speed_summary’s pooled definitions and stats’ intervals; one section per item measured — P4.1, P7.2, P7.3, P4.2, P4.3, CV-4, P3.2/P3.3, R2/R7/R11, P7.4–P7.7, P1.2, Ollama against GenieX on byte-identical files, and the Adreno GPU lane. The recommendation holds for chat on inputs that fit the bundle’s 4096 tokens, for coding and for agent work (the 9B distill: 11/15 trials, pass^3 60 %); for tool calling it now depends on prompts/tool-disambiguation.md, which no run used — without it the Qwen3-4B-Instruct GGUF on the CPU lane passes 41/42 cases where the NPU bundle passes 33/41 (7 to 0, p = 0.016). P4.1 (Qwen3-8B) and P4.2 (Qwen2.5-Coder-7B) are re-measured on v0.7.0 and no longer conditional; the hub page’s § 1i/§ 1n coding tables are re-derived under the fixed grader. The page lists seven lab defects the campaign found, and corrects the data commits’ decode figures, which were means of per-request rates (the instruct GGUF’s “26.1 tok/s” is 22.2 pooled). The roadmap marks each item with its result and gains Phase 8; the lab README links the page.

  • --prompt-variants reports the spread, and bench_coding has paraphrases too (roadmap P1.2). The classic and novel tasks carry two paraphrases each, and strings_normalize_tag, lists_chunk_with_remainder_policy, stateful_classify_ticket and bash_split_comma_list one each. Each keeps the signature, every rule and every worked example (examples); benchmarks/tests/test_bench_coding_variants.py checks each paraphrase against its original literal by literal and runs the worked examples against the reference. Both bench_tools and bench_coding now report the spread — the report gains variant_spread, variant_spread_rate, variant_case_count, variant_spread_cases, by_variant and variant_outcomes, and the run prints the cases passed in one phrasing and failed in another, the score per phrasing and the effective sample. With one draw per phrasing a sampling lane shows spread from an unlucky draw alone (a third of two-way cases at 80 % per draw); the run says so, and --repeats 3 shrinks that share. bench_coding counts total as one attempt per request and votes on determinism per (task, phrasing), as bench_tools already did. No lane has been measured with the flag yet.

  • Every lab tool takes a run-start record (roadmap P1.5, review SRC-1). Before its first request, the speed runner, lanes, contract, bench_tools, bench_coding, bench_agent, bench_embeddings and bench_chat take the start time, tool_sha256 over their files (so a mid-run edit shows up as source_changed_during_run, as it already did for the speed runner) and a 3 s host-load snapshot (hostload.load_snapshot), written into provenance as run_started_utc, host_load and tool_files. provenance.compare() — and so bench_compare and contract --diff — names two runs taken under different load (other cores differ by more than 0.3), and prints a stronger HOST WAS BUSY when either run started above one core of other load, even against a baseline older than the record. The thresholds come from the GenieX v0.7.0 CPU lane: about −14.5 tok/s per core of other load; 30 tok/s on a quiet machine, 14.1 at 0.93 other cores. In contract --diff both notes are warnings; in bench_compare they now withhold a verdict (exit 4, below).

  • A WSL2 harness records the Windows host’s load (roadmap P1.5). bench_coding and bench_agent run in WSL2 against Windows lanes, and their host_load recorded other_cores: null, because the VM’s counters are not the host’s. hostload.load_snapshot() now reads the Windows host through interop when it runs in WSL, the lane URL is loopback and nothing in WSL listens on its port. The new orchestrant/benchmark/winhost.py makes one powershell.exe -EncodedCommand call. It samples Win32_PerfRawData_PerfOS_Processor idle ticks around a Start-Sleep and sums the Win32_Process CPU time of the process listening on the port (Get-NetTCPConnection) and its children. It uses CIM classes because typeperf counter paths are localised, and this host runs a German Windows. The arithmetic, rounding and subtraction are the local reading’s; other_cores then includes the WSL VM itself — the harness and anything else running in WSL. Every load_snapshot() reading now carries via ("local" or "wsl-interop"). A failed interop reading keeps the old null and appends the reason to note; with nothing listening on the port on Windows either, busy_cores is the host’s and the note says so. Measured from WSL2 with the NPU lane serving: 3.3 busy cores of 8, the lane 1.04, other 2.26, and 4.5 s end to end for the 3 s window; with no lane listening, 0.68 busy of 8 and 2.3 s for a 1 s window.

  • bench_compare says what a “no regression” is worth, and stops treating repeats as independent draws (roadmap P7.1). Three draws of one prompt are not three trials. When some case’s repeats disagree, the score now carries a case-clustered interval (CR1 cluster-robust, after Miller 2024 “Adding Error Bars to Evals”, Wilson on the effective sample): the v0.7.0 NPU tool run reads 98/124 = 79% [71-85%] clustered [66-88%, deff 2.6]. Paired reports print the paired estimate, the mean per-case difference, computed exactly: two identical runs read paired diff +0pt [+0, +0] where the unpaired interval read ±10 pt, and cases that cancel print +0pt, not -0pt. Every “unchanged” and “no regression detected” is followed by the minimum detectable drop at 80 % power for that case count, at the observed back-flip rate, never taken below 5 % — at face value, one back-flip in 42 cases (2.4 %) printed a smaller detectable drop than seeing none did. 42 paired cases cannot see a drop under 26 points, and 31 cases catch a 10-point drop only 8–11 % of the time. After a multi-label comparison the closing line names the weakest pairing: the one with the largest detectable drop, not the one with the fewest cases. On a sampling lane run with --repeats above 1 it also prints pass^k (tau-bench’s unbiased estimator), with k the repeats — the paraphrases of --prompt-variants are not draws: 73 % pass^3 at 79 % per draw. New in orchestrant.benchmark.stats: clustered_rate, paired_difference, pass_hat_k, paired_power, paired_mde, back_flip_estimate.

  • bench_agent.py --repeats N (roadmap P7.4). Each trial gets a fresh scratch repository and a fresh opencode data and state directory. XDG_STATE_HOME is redirected too now: the recent-model list and prompt history used to leak into ~/.local/state and carry across trials, and opencode 1.18.31 reads it — when the config names no model, its default model is the most recent entry in state/model.json, so a run without --model ran whatever model the host used last. Trials run round-robin over the tasks. Rows carry attempt; the report adds repeats, trials_run, per_task, pass_hat_k (k = 1..N, through stats.pass_hat_k) and wilson_95, the interval the score line only printed until now. No lane has been measured with it yet.

  • benchmarks/upgrade_check.py: the post-upgrade protocol as one command (roadmap P7.5). Per lane, in a fixed order and never two lanes at once: orchestrant-bench contract (+ --diff against --previous), speed --stream --correctness, an answer-sized speed --max-tokens 2048, bench_tools --repeats 3 and bench_coding (through WSL with --wsl on Windows, else skipped with the reason); then bench_compare --dir. It writes into a fresh directory, with steps.jsonl and a MANIFEST.md mapping each file → command → exit code and naming each lane’s serving runtime. It exits non-zero on a failed step, a regression, nothing compared, a lane that was unreachable or relaunched mid-check, a speed step whose correctness check answered wrong or that lost a prompt, or a check that ran no step. bench_compare’s exit 1 (regression) and 3 (nothing compared) stay separate, and an exit 1 without a REGRESSION verdict counts as a failure. It has not had its first live run yet.

  • PowerShell coding tasks (roadmap P7.6, first half). bench_coding has a powershell runner: pwsh in the bash tasks’ sandbox, checks run one statement at a time, PSScriptAnalyzer at error severity where the module is installed, and a visible SKIP without pwsh. Six tasks come from this repository’s own traps: #requires -Version 7.0 versus 5.1, module-private nested imports, pipeline output in a return value, single-element array unrolling, $null -eq $x operand order, and -ErrorAction Stop for try/catch. Each has a reference, the original bug and 1–3 plausible half-fixes — 18 known-wrong answers in all, and the contract tests reject every one. pwsh runs with an 8 GiB address space, a 1 GiB DOTNET_GCHeapHardLimit and W^X off, because .NET cannot start under the 1 GiB RLIMIT_AS or the 8 MiB RLIMIT_FSIZE (measured on pwsh 7.6.6, aarch64 WSL2). No model has been measured on them yet.

  • A medium-size repository for bench_agent: fix_medium_repo (roadmap P7.6, medium-repo half). A 32-file toy ledger package with one sign bug two imports from its red tests, verified by its own suite and by a CLI run on inputs the agent never sees; that hidden check decodes the program’s output leniently, so bytes that are not text fail the trial instead of ending the run. Three cheats are refused: edited red tests, a root-conftest patch, hardcoded numbers. The data is in benchmarks/bench_agent_medium_files.py, the task in benchmarks/bench_agent_medium.py.

  • benchmarks/bench_chat.py: a chat-quality instrument (roadmap P7.7). 33 code-graded cases cover instruction following, JSON-schema replies, multi-turn memory and document QA at ~1k/~3.5k/~8k tokens. OVERFLOW and CUT rows are excluded and counted, repeats are spaced, each category gets a Wilson interval, and results use the shared report envelope and the run-start record; tool_sha256 covers bench_chat.py and determinism.py. Measured on the NPU lane 2026-09-24: 27/30 = 90 % [74–97 %], the three doc_8k rows OVERFLOW; the CPU lane not yet.

  • bench_sweep runs bench_chat (roadmap P7.7). chat is a sweep tool like tools: the candidate’s endpoint, --label and the sweep’s --repeats, into chat_<slug(label)>.json. It is gated first, skipped on unreachable, and never overwrites a file. It keeps bench_chat’s own --max-tokens (2048) and every category, and it is not in the default --tools.

  • orchestrant-bench contract: three new checks, 21 in all. output_cap: does the server stop short of a 3000-token max_tokens, and at how many tokens. bundle_system_prompt: evidence of a default system prompt, read from usage. response_format_json_schema: is the schema honoured, ignored or refused. The module docstring and the lab README no longer say the output cap and the bundle’s system prompt are unchecked.

  • Reports name the files that served, the drivers under them, and every lane’s runtime (review PROV-1). provenance.runtime named the GenieX build and its serve flags, but not the weights a model id resolved to, the bundle config that decides how they sample, or the drivers. Given the served model id — which write_report now passes whenever every row served one model, so single-lane reports carry it too — runtime_info() adds model_files:

    • a GGUF (and its mmproj) by size, mtime, head_sha256 (its first MiB) and sampled_sha256 (16 windows of 64 KiB spread evenly to the end of the file). The first MiB alone identifies no quant: in this cache it is byte-identical across all four Qwen3-4B quants, because it holds general.* and the vocabulary, and general.file_type sits at 5.66 MiB;

    • a QAIRT bundle by its genie_config.json hashed in full, with the sampler and context size lifted out, its context binaries (with the same two hashes), the HTP extensions file and the QAIRT build it was compiled with;

    • every file says whether it still matches the size in geniex.json.

    It also adds drivers: the Hexagon NPU and Adreno GPU driver versions from the Windows registry, which Windows Update moves while the GenieX build stays the same. compare() prints MODEL FILES CHANGED behind an unchanged id, and names an edited genie_config.json or HTP extensions file. A lanes report now puts each lane’s own runtime on its row; before, the provenance block covered only the first lane.

  • Lane-runtime files for tools run from WSL2 (review OPS-7). From WSL2 the Windows-side lane process is invisible, so the runtime could only be guessed from the installed binary (verified: false). orchestrant-bench runtimes --output lanes-runtime.json geniex-npu geniex-cpu, run on the host once the lanes are up, writes the host’s own view; export LLM_LANE_RUNTIMES=<its /mnt/c path> in WSL2 and every report there takes each lane’s runtime, model files and drivers from it. A snapshot older than 12 h keeps its data but loses verified, and so does one whose lane ran another CLI version than the installed GenieX now reports (snapshot.installed_cli_mismatch): v0.6.1 → v0.7.0 was one session. An entry the host could not attribute ({"error": ...}) is skipped, and the probes after it still run.

  • bench_compare diffs each lane’s runtime in a lanes report (roadmap OPS-7). The provenance block covers one URL (the first lane’s, or the batching endpoint’s under --batching), so a second lane rebuilt, relaunched with other flags or re-pulled behind the same id moved its tok/s unnoted. compare_lanes.py matches lane rows by name and prints provenance’s own runtime notes per lane as ! lane NAME: ... (SERVING RUNTIME CHANGED, serve flags, MODEL FILES CHANGED, genie_config.json/HTP extensions edits). A lane in one report only is named, because the aggregate row then covers another set of lanes. Rows older than the field take the provenance runtime when their URL is the block’s. provenance._runtime_notes is now the public runtime_notes.

  • Benchmark viewer: Answers, load and energy, Serving runtime and Server contract cards (review OPS-6, viewer half). They show answered k/n, time to first answer, thinking share, lane and other cores, CPU-rail J/token gross and net with the report’s net_reliable verdict (gross only for a run with no idle baseline), provenance.runtime with its serve flags (a lanes report’s lane column no longer lists aggregate), and contract reports as one grid of checks against runs, oldest first by each report’s timestamp, with changed answers highlighted. The per-prompt drill-down gains first answer, lane and other cores, and J/token. tests/unit/frontend/test_page_compiles.py builds and dry-run compiles the Reflex page and skips without Reflex; .github/workflows/benchmarks.yml’s viewer job now runs tests/unit/frontend with reflex==0.9.6.post1, so CI compiles the page where the test used to skip. tests/unit/benchmark/test_bench_report.py::TestTheViewerReadsTheTrackedRun builds the manifest over benchmarks/benchmark_results/2026-09-23-geniex-upgrade and checks that the viewer’s tables print the GenieX v0.7.0 page’s published figures.

  • The speed runner knows whether an answer arrived. answers.py reads every place a server puts thinking (inline <think>, reasoning, reasoning_content) and records finish_reason, answered and ttfa_s (the first token after any thinking); a reply cut at max_tokens has no wall_s_to_answer, and the summary prints Answered k/n and averages time to an answer over the answered rows only. The viewer and orchestrant-bench report follow — their answer column says how many rows it covers (“24.8s (8/9 answered)”), since a run that cut more replies would otherwise rank fastest — and read an older report’s never-closed <think> as all thinking.

  • bench_compare is a speed tripwire too. compare_speed.py pairs decode, prefill and TTFT per prompt and flags SLOWER when the median decode ratio falls by more than 5 % or the prompts’ own scatter; a CPU lane measured over 0.3 cores of other load is reported, not judged; CPU-rail J/token is reported; it derives the load for reports older than other_cores, and a run that load could explain — slower under load, or faster than a loaded baseline — is printed NOT judged. A speed report no longer gets a latency verdict (its wall moves with max_tokens and includes cut replies). A contract pair, or any pair with nothing in common, now exits 3 (NOTHING COMPARED; 2 stays argparse’s usage error) instead of “no regression detected”.

  • bench_compare refuses a load-confounded verdict (roadmap P1.5). When a pairing’s run-start load notes fire — a run started with more than 1.0 other cores busy, or both recorded their load and it differs by more than 0.3 — the verdicts load can move (the speed tripwire’s, the per-attempt time, lane throughput) are withheld, named on a closing WITHHELD for load line, and the run exits 4, CONDITIONS DIFFER. Scores, per-case flips and batching are still judged. The order is REGRESSION 1 > CONDITIONS DIFFER 4 > NOTHING COMPARED 3 > 0, for one pair and over --dir. A baseline that predates the record is not refused for lacking it, and an NPU-lane speed pair (under 4 lane cores on both sides) is still judged when neither run started above 2.0 other cores, the range it was measured unmoved over, with a line saying so. --allow-load-difference restores the old behaviour: the gated verdicts are judged, a NOT judged line passes unjudged. upgrade_check.py records exit 4 as the failing status conditions-differ in steps.jsonl and MANIFEST.md, and has no override of its own. contract --diff prints the same notes and is deliberately not gated. The exit codes, their order and the gate live in the new benchmarks/compare_verdict.py.

  • orchestrant-bench contract — re-check the server behaviours the lab depends on, and diff them across runtime upgrades. Every GenieX release moved one of them (v0.6: the output cap, max_tokens, tool-call parsing, the prefix cache; v0.7: QAIRT stop sequences, power_mode, the bundle’s system prompt), and each was found by hand after it had distorted a number. Eighteen checks, each answering yes/no/inconclusive/error/skipped with evidence; --diff exits 1 when any answer moved. Its first run found that the v0.6.1 QAIRT lane ignores a chat stop while reporting finish_reason: "stop" and caches a conversation extended by a turn but not an identical repeat. Two of its checks, temperature0_is_greedy and identical_repeat_intact, name the GenieX defects under Fixed below; every determinism check puts an unrelated request before each draw.

  • Measured CPU and energy per request on a Windows host. hostload.py finds the process listening on the lane’s port and reports its CPU-seconds over the request (lane_cpu_s, lane_cores) and everything else the machine did meanwhile (other_cores — a llama.cpp CPU lane lost about 20–55 % of its decode rate to 0.5–1.0 cores of IDE and antivirus work, and no report said so); energy.py reads the Windows Energy Meter Interface through PDH (English counter names, so a German install reads the same) and interpolates the CPU-cluster rails’ cumulative energy onto each request’s exact bounds (cpu_rail_energy_j, cpu_rail_j_per_token, and *_net_* net of an idle baseline). The Snapdragon X exposes CPU_CLUSTER_0/1 and no NPU or GPU rail, and every report’s energy.scope says so. This replaces the roadmap’s “joules are not measurable here” for the half of the question that separates the lanes.

  • benchmarks/docs/geniex-v0.7.0-cpu-npu-2026-09-24.md — the GenieX v0.6.1 → v0.7.0 upgrade, measured on the CPU and NPU lanes, with every raw report under benchmarks/benchmark_results/2026-09-23-geniex-upgrade/. The upgrade changed two lane behaviours (QAIRT /v1/completions stop sequences, power_mode) and no model output; its NPU slowdown is --log info (−13 % decode, +21 % CPU-side J/token gross), not the runtime. It also retires beliefs of the hub’s GenieX page: the QAIRT lane caches a conversation extended by a turn, “T=0 samples” is a server defect rather than the models, since v0.6 the NPU + CPU pair delivers less than the NPU lane alone (0.54–0.78×), and on llama.cpp 4ff829e one Unsloth Dynamic IQ3_XXS answered correctly again (a three-question smoke test). Revised the same day after a six-lens, adversarially verified review of the lab — the page’s “What the review corrected” lists every number that moved.

  • Runtime provenance. Reports record runtime — the serving build (GenieX CLI, QAIRT and llama.cpp versions from the lane’s own binary, or Ollama’s /api/version) and the lane’s serve flags — and interpreter, the Python build’s own platform (an x64 Python under emulation still reports ARM64). bench_compare names SERVING RUNTIME CHANGED and changed serve flags before any score. Speed reports gained the provenance block the Sphinx page already claimed they had, and bench_compare reads it.

  • The PowerShell lint gate this repo never had. scripts/windows/Invoke-Lint.ps1 (the dev-box command) and the lint-powershell job of .github/workflows/windows-2025.yml (CI) run ANTfrastructure’s windows/scripts/Invoke-Lint.ps1 over scripts/windows/: a mandatory parse pass, an AST-trap pass (comma-attribute quoting, switch shadowing, glued parameter tokens) and PSScriptAnalyzer 1.25.0 against the hub’s own PSScriptAnalyzerSettings.psd1, consumed by reference so nothing is copied here to drift. -FailOnAnalyzer and -Path are both passed and neither is optional: without the first the analyzer prints findings and exits 0, without the second the hub script grades the HUB’s trees out of this checkout. Before this, ~400 lines of Windows build driver were gated by nothing but running them. The gate landed as a standalone .github/workflows/powershell-lint.yml; that file is gone again in the same unreleased cycle, for the reason under Changed below.

  • The hub’s --ratchets measurement gates, with their freeze files seeded and committed. scripts/linux/run-lint-gates.sh now passes --ratchets unconditionally, adding nine --root gates: the docs cross-reference gate, code size, complexity, dead functions, comment size, stdout returns, masked declarations, trailing conditionals and the shellcheck warning ratchet. The first run is the seed, so function-size.allow (22 rows), file-size.allow (9), code-complexity.allow (28), comment-size.allow (7) and dead-functions.allow (1) land in the same change; four gates need no file because this tree has nothing over their limits. Every row carries a reason, because the contract is four-way — a frozen number that shrinks, or an entry whose subject is gone, fails exactly like new growth.

  • benchmarks/, frontend/, bench/ and examples/ are graded by the static-analysis gate. The hub’s STATIC_ANALYSIS_EXTRA_PATHS (Linux) and -ExtraPaths (Windows) reach first-party code that is not $PACKAGE_NAME; 48 files of it had been outside every analyser because the driver’s target list was the package, tests/, docs/source/conf.py and setup.py. codespell, vulture, ruff check and ruff format are clean over all four (ruff measured where its EXE rules cannot fire; 15 of the 22 EXE001 in Fixed); bandit could not reach them at all until the pin below. The Windows lane invokes the driver through pwsh -Command rather than -File for this: -File binds ONE element of an array parameter and silently discards the rest, so the lane would have reported green over two trees nothing had read.

  • Decision, 2026-09-15: the NAS document-AI thread moves here from ANTfrastructure, beside the benchmark lab that owns the question. benchmarks/nas_census.py (the census: walk a document tree and publish the four numbers — total PDF pages, scanned fraction, German fraction, table density — plus the gate that decides whether a VLM gets budget at all), benchmarks/tests/test_nas_census.py (44 tests, offline, and green without PyMuPDF, which is the environment this repo actually has), and benchmarks/docs/nas-document-ai.md (the 36-agent review that prescribes both: model shortlist, per-file-type routing, and why the Hexagon NPU cannot read a page). The hub keeps no copy. The page was written in ANTfrastructure and says so, and its links now resolve from here — hub pages through third_party/ANTfrastructure/, the census tool as a sibling. The census needs no dependency this repo does not already have: it is stdlib-only, and pip install pymupdf upgrades it from an extension census to per-page classification, reported as SKIPPED rather than as a fabricated zero when it is absent.

  • AMD GPU support across monitoring and the benchmark runner. GPUProbe now picks a vendor: NVML for NVIDIA as before, and AMD through ADL on Windows (the driver’s atiadlxx.dll) or the amdgpu sysfs counters on Linux. The Windows ADL path reads the modern PMLog sensors for utilization, temperature and power — the legacy Overdrive5 API that pyadl wraps returns ADL_ERR on RDNA-era cards, verified on an RX 9070 XT — and the adapter memory APIs for VRAM; no Python package is required. The gpu-rocm extra adds amdsmi, used only to resolve a Linux card’s marketing name. AMD devices are ordered largest-VRAM-first, so index 0 is the discrete GPU on an APU+dGPU host. orchestrant-bench now records hardware.gpu (vendor, name, VRAM, backend) and per-prompt gpu_utilization_percent, gpu_memory_used_gb and gpu_power_watts; the viewer renders them as a hardware row, a comparison column and a chart.

  • The benchmark viewer is a Reflex app in frontend/ (the frontend extra), replacing the React/Vite app that travelled with the lab. It reads the manifest directly (ORCHESTRANT_BENCHMARK_MANIFEST overrides the default), so the build/copy step is gone; the table and Wilson-interval logic is pure Python in frontend/frontend/benchmark_data.py, tested without Reflex.

  • orchestrant.benchmark and orchestrant-bench: the LLM endpoint runner. Ported from ANTfrastructure’s linux/llm-stack (one request path, backend registry, speed/lane measurements, statistics, provenance) so OrchestrANT owns the measuring identity the capability benchmarks build on. Stdlib plus the already-present psutil; named backends come from the serving stack’s registry, found through LLM_BACKENDS, the hub submodule, or a package copy. Tests live in tests/unit/benchmark and run offline; requests joins the test extra for the live-contract modules.

Changed#

  • Comments are one line, only the why (2026-09-30). The owner rule for every repo of the family, in third_party/ANTfrastructure/AGENTS.md § Comments: one line, only the why, and linked from § 2 here. 136 files, 6.5k lines out and 1.4k in; what was a trap moved to AGENTS.md § 4 (pythonpath, tool_sha256, the two bootstrap copies) or to the hub page it belongs to. Code is unchanged. Because tool_fingerprint hashes raw source, every lab tool whose comments changed has a new tool_sha256: reports from before and after compare as different tool versions. comment-size.allow holds no rows: nothing is over one line.

  • ANTfrastructure to 497ba711. The comment sweep itself, whose comment-size gate now grades every language, plus CON38 to CON41 (clang-tidy’s GCC, atheris’ libFuzzer, 3.14 vs 3.14t venvs, the arm64/riscv64 Vulkan WSI) and the Windows rocm builders sharing one owner. The two bootstrap copies follow the hub templates.

  • scripts/linux/ci_static_analysis.sh no longer clears VIRTUAL_ENV. The family image exported VIRTUAL_ENV=/opt/venv (root-owned bin/) and UV_PYTHON; it has exported neither since hub CON18 (:latest of 2026-09-29), so the export VIRTUAL_ENV="" that kept uv sync off /opt/venv went. UV_PROJECT_ENVIRONMENT and UV_NO_SYNC=1 stay: the driver still never activates the venv it creates.

  • ANTfrastructure to 547d9fd0: the hub’s images now build this repo’s develop. renovate-local.sh --apply took two hub commits. 547d9fd0 sets the hub’s APP_REF=develop and resolves it to the commit develop points at whenever an image build starts, so what is pushed here is what the next image contains, with no release tag in between (AGENTS.md § 4). It also passes the ROCm extra as pytorch-rocm10. 3a692eca gives container-ci-windows a compiler cache across runs; it is reached at @develop anyway. Of what this repo calls through the pin, only versions.env APP_REF changes; the pin gate still passes (7 pins compared). Known gap, on the hub side: the Windows ROCm image cannot build torch 2.14 yet, because AMD’s ROCm 10.0 wheels stop at 2.13.0.

  • ANTfrastructure to c2385230, by renovate-local.sh --apply. Three hub commits: the consumer-pins gate now also holds torch, torchvision and onnxruntime-genai (9468eb11; this repo passes, 7 pins compared), the runtime image keeps base’s sccache/ccache caps (a0835db9), and new compiler-cache-restore/-save actions (c2385230). Of what this repo calls, only consumer_pins.py changes; RUFF_VERSION, the torch pins and the CI image tags stay where they were.

  • torch 2.14.0 / torchvision 0.29.0, and the ROCm extra is pytorch-rocm10 (breaking: --extra pytorch-rocm71 is gone). The pins now equal ANTfrastructure’s PYTORCH_VERSION / TORCHVISION_VERSION, which its image force-installs over this lock; until now a dev box ran 2.13.0 and the image 2.14.0. The hub’s consumer-pins gate holds them there from 2026-09-29, with the riscv64 source pins and onnxruntime-genai. The ROCm extra targets ROCm 10 systems but takes pytorch.org’s rocm7.14 wheel line, as the hub’s ROCm image does: no torch 2.14 is built against ROCm 10 (AMD’s own ROCm 10 wheels stop at 2.13.0), and rocm7.1 never got 2.14. That torch pulls rocm[device-all,libraries], so the extra lists its 28 runtime packages to route them to that index, and installing it downloads the device libraries of every GPU architecture. The hub still passes pytorch-rocm71, because it builds tag v0.0.28 (APP_REF), and switches when APP_REF moves. The lock also moves triton and triton-rocm to 3.8.0 and, through the cu130 wheel, nvidia-cudnn-cu13 to 9.24.0.43 and nvidia-nccl-cu13 to 2.30.7; nothing else changes. Unit tests on torch 2.14.0+cpu: 913 passed, 32 skipped.

  • ANTfrastructure to 4ae8ef81, and the benchmarks lane’s pins, by renovate-local.sh --apply (2026-09-29). The gitlink takes the hub’s develop tip, 38 commits on from eba03218. Of what this repo calls, only action pins in the hub’s reusable workflows and composite actions, the llm-stack’s open-webui (v0.11.4) and glances (4.5.7) images, and new Windows-only FFmpeg codec keys in versions.env change. The four Python drivers, run-lint-gates.sh, renovate-local.sh, ci-image-ref.sh, backends.json, the gateway renderer and plugin and the imported PowerShell modules are byte-identical, and RUFF_VERSION stays 0.16.8. benchmarks.yml moves astral-sh/setup-uv v9.0.0 → v10.2.0, pinned to its commit as the hub’s lanes are; v10’s one breaking change turns the cache off for pull_request_target, workflow_run and release, none of which trigger this workflow. The contract job’s ollama service moves 0.34.0 → 0.34.4 by digest, by hand: the tool has no locator for a services: image. Still held by renovate.json’s approval rules: ruff 0.16.9 (the hub pins 0.16.8), onnxruntime-genai 0.17.1 (the hub pins 0.15.2), and torch 2.14.0 / torchvision 0.29.0, which whl/rocm7.1 does not carry (whl/rocm7.2 does).

  • ANTfrastructure to eba03218, the hub’s develop with the gateway merged. 38b91aa1 merges feature/apisix-gateway (6bf205b2, the previous pin) into the hub’s develop (1dc4a776). eba03218 on top of it rewrites the two gateway-e2e matrix rows of the hub’s llm-stack-serving.yml as block mappings, which the hub’s workflow-conventions gate can read; nothing here calls that workflow. The pin now sits on the branch .gitmodules tracks again: branch = develop since 750d455, which renovate-local.sh --apply and git submodule update --remote follow. Of what this repo calls, the 20 develop commits it adds change only ci-image-ref.sh, versions.env and the CI image ref gate, all to add the Windows arm64 cross bundle (--windows-arm64, CI_IMAGE_WINDOWS_ARM64_TAG=winarm64), which no lane here uses. The four Python drivers, run-lint-gates.sh, renovate-local.sh, the llm-stack serve-stack.sh, backends.json, the gateway renderer and plugin, the three imported modules, the two Windows Python drivers, Invoke-Lint.ps1, the two shared templates and the four reusable workflows are byte-identical to 6bf205b2, and RUFF_VERSION stays 0.16.8. The hub’s ci-build-triggers.md now says every platform lane runs on every push, so the AGENTS.md § 2 row names it for that; AGENTS.md, README.md and renovate-local.sh now say branch = develop.

  • ANTfrastructure to 6bf205b2, and ruff 0.16.7 → 0.16.8 with it. The pin takes the hub’s llm-stack gateway and the 145 hub commits since f65ccae9. 6bf205b2 is the tip of the hub’s feature/apisix-gateway, the commit the gateway was accepted on; it reached the hub’s develop with the merge 38b91aa1 (the entry above). The hub now pins RUFF_VERSION=0.16.8, so pyproject.toml, .pre-commit-config.yaml and uv.lock move together; ruff format --check and ruff check 0.16.8 are clean over the tree, measured on Windows, where ruff’s EXE rules are compiled out (the Linux lane’s 22 EXE001 are in Fixed). The registry the lab reads moved with the pin: geniex-gpu serves unsloth/Qwen3-4B-Instruct-2507-GGUF:Q4_0 (reports before 2026-09-25 measured the base Qwen3-4B GGUF under that name), geniex-cpu-9b is the CPU lane with the 9B distill, and six lab-* entries reach the lanes through the gateway. Every hub path the wrappers, Build-Windows.ps1, Invoke-Lint.ps1 and the workflows name still exists. The pin also moves CI_IMAGE_LINUX_TAG from latest-cross to latest, and the CI image ref gate (run-lint-gates.sh, actionlint + CI image refs) failed on the one place this repo spelled the old tag out: a comment in ubuntu-26.04-amd64-arm64.yml, which now names ci-image-ref.sh instead. tests/unit/benchmark/test_serving_evidence.py holds three sha256s equal: the tools prompt’s raw bytes, the registry’s pin for them, and system_prompt_sha256 in both P8.1 reports.

  • The speed runner’s correctness probe splits integrity from capability, and --correctness-only’s exit code now reads the integrity items only — a behaviour change. Its OK/DEGRADED/BROKEN verdict blamed “broken kernels or an over-aggressive quant” for any wrong answer. On 2026-09-24 that read Llama-3.2-3B and Phi-4-mini as BROKEN (3/6) and the Qwen3-4B-Instruct-2507 GGUF and Qwen2.5-Coder-7B as DEGRADED (5/6) on healthy lanes: every miss was strawberry, Canberra, the 5-machines puzzle or 9.9 vs 9.11, and every model answered both arithmetic items. Each probe item now has a kind.

    • Integrity items decide the verdict, the --correctness-only exit code, upgrade_check’s speed step, bench_sweep’s gate and the viewer’s banner: 23 * 17, 17 squared, plus four new one-step items (100 − 37, the next number after 2, 4, 6, 8, days in a week, a word copied back from the prompt).

    • --correctness-only exits 0 when only capability items are wrong or cut, where it exited 1 on any wrong answer and 2 on any cut one. It exits 1 on a wrong integrity answer, or when no integrity answer came back even if capability items answered, and 2 when an integrity answer was cut.

    • Capability items are printed and recorded apart, as capability: n/m -- not a kernel verdict. A quant that costs only reasoning items now reads OK (Qwen3-4B’s Q2_K, 4/6, lost two reasoning items); it shows in bench_compare’s capability line against the same model’s earlier report, not in the verdict.

    • The four new items had not been asked to any model when they were added; the first --correctness run on each lane is their check. Live check, 2026-09-25 (--correctness-only on every campaign model, benchmarks/benchmark_results/2026-09-25-probe-kinds/): all ten – the NPU instruct 4B and Qwen3-8B, the thinking and instruct 4B, Coder-7B, Llama-3.2-3B, Phi-4-mini, the 9B and 2B distills on the CPU lane, the instruct 4B on the GPU lane – answer all six integrity items, the four new ones included, and read OK; their capability scores run from 4/4 down to Phi-4-mini’s 0/4.

    • The report’s correctness block keeps its old fields, counted over every item (a new report’s score/total count 10 items, an older one’s 6), and adds integrity, capability, verdict and a kind per item.

    • Older reports are split by prompt, and bench_compare compares a speed report’s integrity items paired by prompt: the first old-to-new comparison pairs the two arithmetic items only, and capability answers are listed when they move, never judged. An integrity verdict that becomes BROKEN is a REGRESSION whatever the pairing shows. Two paired items cannot separate even a total loss (2 worse / 0 better is p=0.5), and before kinds the unpaired score read 6/6 → 0/6 as a REGRESSION.

    • report manifest writes both kinds’ counts for older reports too (correctness.annotate), so the viewer’s banner judges the integrity answers of every report; a manifest built before 2026-09-25 is judged on every answer until it is rebuilt.

    • The probe moved to orchestrant/benchmark/correctness.py, which the speed report’s tool_sha256 now covers, so the first comparison against a speed report written before this prints BENCHMARK SOURCE CHANGED.

  • bench_compare.py is under 800 lines: the suspect-case block and --dir are modules of their own. is_control, suspect_cases, mark_suspect_cases, its _recount_* helpers and measured moved unchanged to benchmarks/compare_suspect.py; pair_directories, baseline_path/BASELINE_DIR and the --dir loop to benchmarks/compare_dirs.py (1011 → 762 lines; its file-size.allow row is gone). bench_compare still serves every moved name, so no caller had to change an import. Neither module imports bench_compare: the --dir loop is handed its compare() and load() instead, so bench_compare.py --dir never loads a second copy of itself. The split alone changed no output: output and exit codes are byte-identical before and after it over 532 runs on the tracked reports — every 2026-09-2* report against itself, same-benchmark pairs of speed, tools, coding, lanes, contract, chat and agent reports, --dir in both directions between 2026-09-23-geniex-upgrade and 2026-09-24-upgrade-check-v070 (they share no report names, so that pair exercises only the “no report names in common” exit) plus each directory against itself, --baseline, --save-baseline and the error paths.

  • With a control among the candidates, bench_tools, bench_coding and bench_chat hash compare_suspect.py into tool_sha256. Its recount sets every other row’s passed/total/effective_n/effective_k, group tables and walls before the report is written, so an edit to it moves a score the way a grader edit does. No fingerprint covered it while it sat inside bench_compare.py, and hashing that file would have been wrong: most of it is the comparison, which decides no report’s numbers. Runs without a control keep their file set. Their tool_sha256 still moves once, as after any edit, because bench_tools.py, bench_coding.py and bench_chat.py themselves changed (a default bench_tools run: 898fcac8c796e51a → b76294d033397b9b, the --turn-growth fix below included). The first comparison of any bench_tools, bench_coding or bench_chat report against one written before this prints BENCHMARK SOURCE CHANGED; a control run’s line also names the added file. Re-save those baselines and read the next upgrade_check --dir with that in mind. No tracked report or baseline has a control.

  • The prompt-variant helpers live in benchmarks/bench_variants.py, and mark_suspect_cases() owns the recount. variant_spread, variant_spread_lines, variant_report_fields, phrasing_agreement and VARIANT_FIELDS moved unchanged out of bench_tools.py. bench_tools.rescore_variants is gone: mark_suspect_cases() re-derives effective_n/effective_k and the spread through variant_spread() whenever a report carries variant_case_count, so the producers call nothing after it. effective_k still means observed through the prompt as written. Under --prompt-variants, bench_tools and bench_coding hash bench_variants.py into tool_sha256; bench_coding’s hash never covered the variant arithmetic before.

  • Decision, 2026-09-24: the workflows follow the fleet naming convention. windows-2025.yml is windows-x64.yml (“Windows x64 · build + test”; its concurrency group follows the file name), and the display names are Lint gates, Submodule pins and Benchmarks, spelled as every repo in the family spells them. Triggers, jobs and job ids are unchanged, so check-run names are too; the README badge follows the new file. ubuntu-26.04-amd64-arm64.yml keeps its name for now: splitting it into linux-x64.yml and linux-arm64.yml needs a hub reusable-lane input that is not on hub main yet.

  • No tool fingerprints provenance.py any more (review OPS-9). The determinism probe moved to orchestrant/benchmark/determinism.py (re-exported from provenance), and only the tools that run it hash it. source_changed_during_run is now recorded as false when checked and unchanged, where it used to be absent. The first comparison against any baseline saved before this change prints BENCHMARK SOURCE CHANGED for every tool, because each one’s file set or source changed (contract.py, lanes.py and, for the speed runner, hostload.py were all edited); re-save the baselines after merging.

  • bench_agent refuses an override file in any directory from the protected tests up to the workspace root, not only in the tests’ own directory: pytest loads the rootdir’s conftest for tests/ too, which the flat fixtures never had to consider. Flat fixtures are unaffected. The report’s tasks_run now counts distinct tasks (unchanged at --repeats 1) and trials_run counts rows; tool_sha256 covers the two medium-fixture modules.

  • orchestrant.benchmark.report.build_manifest gives every entry runtime, base_url, energy, cpu_threads, backend, model and timestamp (run_fields()), and takes the host record from the first real hardware block (host_hardware()) rather than from whichever file sorts first. The viewer’s per-prompt drill-down prints cut instead of - for a reply stopped at max_tokens.

  • ruff skips third_party/ and .claude/ (extend-exclude in pyproject.toml): the submodule is ANTfrastructure’s code under its own lint profile and agent worktrees are other checkouts, and a bare ruff format at the root rewrote both on 2026-09-24. Explicit paths still reach them.

  • The Python 3.13 test legs are gone, on both lanes. test-python-versions in .github/workflows/ubuntu-26.04-amd64-arm64.yml and $PythonVersions in scripts/windows/Build-Windows.ps1 are now 3.14 3.14t, and the docs index links and includes the 3.14 report instead of the 3.13 one. ANTfrastructure’s ONNX Runtime single-source rule (2026-09-23) moves a synced venv’s ORT onto the image’s chain wheels, which are cp314 only, so a 3.13 leg inside the images fails its sync; the images carry CPython 3.14 only (owner decision). The classifiers keep 3.13, and ruff/ty still target it.

  • third_party/ANTfrastructure → 35960a87, and NO re-sync with it. The shared-config templates did not move between 3c83a5e3 and this pin, and the drift gate is what says so rather than this sentence assuming it: sync-shared-config.sh --repo-root . --check reports both declared assets OK and Shared config in sync. What the pin carries for this repo is the bandit argument fix and the Windows lane’s two new inputs, both below.

  • bandit reaches benchmarks/, frontend/, bench/ and examples/ for the first time, and BANDIT_EXCLUDES / -BanditExcludes owns its -x list. Both hub drivers spelled the extra paths as one -r per target, and bandit’s -r is a store_true flag against a single nargs='*' positional, so the gate died with unrecognized arguments: benchmarks frontend bench examples (exit 2) every time the knob was set. Measured here against bandit 1.9.4, reported upstream, fixed upstream in 17a4733f on the Linux driver and its Windows twin; the two comment blocks that documented the breakage are deleted with it. With the knob working, the hub’s default exclude list leaves 1025 findings — 973 of them in benchmarks/tests, 959 of those B101 (assert used in a test). The default already drops this repo’s top-level tests/ for that reason and its entries are anchored at the working directory, so the lab’s own suite has to be named: both lanes now pass the hub default plus benchmarks/tests, which is the same judgement pyproject.toml already records for ruff (benchmarks/tests/*.py ignores S101). 52 findings remain (43 low, 9 medium, 0 high, over 18699 lines) and they are NOT triaged: 33 in benchmarks/ and the 19 under orchestrant/benchmark that the extra-paths entry above already recorded as the static-analysis lane’s pre-existing red, beside vulture’s 18, codespell’s 5 and ty’s 6. They are the same owner decision, not a new one, and the shape of it is now visible: pyproject.toml already ignores ruff’s twins of these exact codes in these exact trees (S101, S105, S108, S310, S603, S607 for benchmarks/*.py; S110, S112, S310, S603, S607 for orchestrant/benchmark/*.py) as a frozen port baseline with a tracked follow-up. bandit has no per-file-ignore table, so saying the same thing to it means either a --skip list or ~52 inline # nosec twins — a suppression-policy call, which an adoption pass does not get to make.

  • The PowerShell lint job is lint-powershell: true on the hub’s reusable Windows lane, and .github/workflows/powershell-lint.yml is deleted. The hub grew the lint-powershell / lint-path inputs (acf85514) because this repo and OxidANT hand-wrote the same job within a week. The deleted file agreed with the lane on everything that matters — windows-2025, the same pinned checkout SHA with submodules: true and fetch-depth: 1, PSScriptAnalyzer pinned to 1.25.0, the hub’s own Invoke-Lint.ps1 with -FailOnAnalyzer — and differed only in the directory, which is lint-path: scripts/windows. Two things the input does not carry were kept rather than dropped with the file: the concurrency group moves to windows-2025.yml, where it now also covers the container build that never had one, and the gate loses its paths: filter, so it runs on every push and pull request to main instead of only on scripts/windows/** — strictly more coverage, never less. scripts/windows/Invoke-Lint.ps1 stays, for the reason scripts/linux/run-lint-gates.sh stays: it is the dev-box twin of a lane that cannot call it. The separate README badge goes with the workflow.

  • third_party/ANTfrastructure → 3c83a5e3, and the shared config re-synced with it. The pin carries the knobs above plus the reusable workflows below. Hub 08137b27 rewrote shared/linux/templates/antfrastructure.sh, which this repo vendors as scripts/linux/lib/antfrastructure.sh, so the shared-config drift gate goes red the moment the pin moves: both halves are in one commit. The body gained a three-place ANTFRASTRUCTURE_DIR search (explicit environment answer, submodule, plain antfrastructure-tools sibling) and a not-found message that branches on whether .gitmodules declares the submodule at all.

  • lint-gates.yml and submodule-pins.yml are uses: lines now. Both jobs were copies, and both headers said so — the lint one carried the instruction verbatim (“replace this job with a uses: when ANTfrastructure grows a reusable lint lane”). 108 lines of workflow become 16 lines of configuration; the runner pins, the SHA-pinned checkout, the uv install, the Pester 3.4.0 measurement and the pin-suite wiring all move upstream. What stays is this repo’s own: when each lane runs, and ratchets: true.

  • Every code and prose cross-reference names the tree its page is in. The hub’s doc-links gate takes --root now, so this repo can grade its own pages; it found twenty dangling references, all of them already wrong for a reader. The hub’s pages are spelled third_party/ANTfrastructure/docs/... and the lab’s benchmarks/docs/...; AGENTS.md no longer points a hub heading at its own file; two bare § 1n / § 1m in benchmarks/README.md now name the GenieX page they belong to; and the two generated-file rules in .gitignore are anchored (/docs/source/README.md), which is both more precise and no longer shaped like a pointer.

  • Build-Windows.ps1’s Write-Log is Write-LogInfo. It shadowed a cmdlet PowerShell ships (PSAvoidOverwritingBuiltInCmdlets), which is the one finding the new PowerShell lane had; fixing it first is what lets the lane start green instead of starting with an exception. The name now also matches its three siblings.

  • onnxruntime-genai / onnxruntime-genai-cuda follow ANTfrastructure again: 0.14.0 → 0.15.2. Three pins carried the comment “keep in sync with ANTfrastructure ONNXRUNTIME_GENAI_VERSION” while that key had already moved to v0.15.2. 0.15.2 resolves onnxruntime 1.29.0, matching the hub’s ONNXRUNTIME_VERSION=v1.29.0. onnxruntime-genai-directml stays unpinned — PyPI publishes nothing at that version for it — and its comment now says so instead of naming the superseded v0.14.0.

  • ruff config: CPY001 (missing-copyright-notice) is explicitly disabled. It and PLR0917 left preview in ruff 0.16, so pinning ruff==0.16.4 took a select = ["ALL"] project from 3 findings to 63 with no code change. This project states its licence once in LICENSE, not per file; the four PLR0917 findings were fixed in the code instead. vulture now runs at min_confidence = 100, and codespell skips generated Cython .c output and knows nd (tvm.nd.array) and DocumANTation — each with the reason in pyproject.toml.

  • Renovate’s “moves with ANTfrastructure” rule covers every spelling of those pins. matchDepNames named only ruff and onnxruntime-genai-cuda, so the .pre-commit-config.yaml rev (which the pre-commit manager reports as astral-sh/ruff-pre-commit, not ruff) and the plain onnxruntime-genai pin could still be bumped unattended — the exact drift the rule’s own description says it prevents.

  • Decision, 2026-09-15: the repository declares all four of its topics, and nothing is split out. It is the orchestrant package, the family’s LLM benchmark lab (benchmarks/ plus the orchestrant.benchmark runner), the template a new Python AI project starts from, and the Reflex viewer in frontend/. pyproject.toml’s description and keywords, README.md § About The Project and AGENTS.md § 1 now all say so; no code moved.

  • Decision, 2026-09-15: the one large tracked binary is documented, not rewritten out of history. resources/models/yolov26m.onnx (78 MiB) stays tracked because it is the YOLO monitor’s default --model; the reason and the explicit refusal to run filter-repo/filter-branch/BFG/LFS are in the new resources/models/README.md, and .gitignore now excludes *.onnx, *.gguf, *.pt and *.pth with a single exception for that file, so nothing new joins it.

  • Build-Windows.ps1 calls the hub’s Windows Python drivers instead of re-inlining them. Static analysis is now third_party/ANTfrastructure/windows/scripts/python/Invoke-CiStaticAnalysis.ps1 and both packaging steps are Invoke-CiPackaging.ps1, each launched as a child process with -RepoRoot (and -PackageName orchestrant, without which the driver would derive the distribution name). Tool-list parity with the Linux lane is structural now rather than a rule restated in a comment. The pytest matrix stays local: the hub’s Invoke-CiTests.ps1 was removed upstream in ANTfrastructure 6f5aef72.

  • The Windows bench demos are soft, like the Linux lane’s. bench/demo_*.py failures log <demo> skipped instead of failing the step, matching ci_tests.sh:113-120; the unit tests above them stay hard.

  • .github/copilot-instructions.md is Copilot’s slice only, in English, ~85 lines instead of 200: typing policy, the uv/ruff/ty commands, the commit format and Do/Don’t, with the gate mechanics linked to AGENTS.md § 4 rather than retold. The archive/ directory it named does not exist.

  • tests/fuzzy/ is gone, with its AGENTS.md entry. It held one .gitkeep and never a hypothesis or atheris test; git history keeps it.

  • Dependabot is kept deliberately, and its comment is true now. The claim that the config had “never actually opened a PR” was false (#13-#33), and its retirement condition — the shared Renovate preset not being on ANTfrastructure’s default branch — no longer holds. It stays as the only unattended watcher, because the Renovate GitHub App is installed nowhere in this family.

Fixed#

  • codespell is clean over the static-analysis scope. The last of the lane’s codespell findings was openai_api.py:821 Ans ==> And, which is the speed table’s "Ans(s)" (answer-seconds) column header, not a typo. It is excused in pyproject.toml by an ignore-regex for that exact literal, not inline: openai_api.py’s bytes are part of the speed runner’s tool_sha256. ans/Ans stay reportable everywhere else. codespell 2.4.2 over CI’s paths: exit 0.

  • vulture is clean over the static-analysis scope. Its 16 findings were tests/unit/benchmark/test_v1_api.py asking for the side-effect fixtures wait_for_ollama and wait_for_model as parameters it never read. The 15 test methods now name them in @pytest.mark.usefixtures (method level, so the error-path tests do not start waiting for a model). The available_models fixture keeps its parameter for ordering, because pytest 9 rejects marks on fixtures, and dels it, which is vulture’s marker for a kept argument. The same requests run in the same order. vulture 2.16 over CI’s paths: exit 0.

  • ty is clean on Linux and on a win32 target. It had 5 errors.

    • gpu.py imports pynvml through importlib.import_module. An import statement declares the name as the module, so ty rejected the None fallback. Its # type: ignore had been dead since ty 0.0.25. The suppression-free form keeps every pynvml is not None guard enforced: an unguarded use is still reported.

    • gpu_amd.py’s _AdlApi declares _lib/_msvcrt in its class body. The declarations are annotations only; no attribute is created. __init__ binds them after a non-Windows raise that a Linux-targeted checker cannot see past.

    • provenance.py’s two resource.getrusage calls carry a ty: ignore[unresolved-attribute]. The Windows lane will report them once it reaches ty, and they are already guarded by except Exception at runtime.

    • Checked with ty 0.0.59 in a venv synced like CI’s (--all-extras), with the default and --python-platform win32 targets. Runtime is unchanged: 36 GPU tests pass.

    • pyproject.toml’s ty comment no longer says the Windows lane syncs without extras.

  • ruff check is clean on Linux: no EXE001 (22 before). The files flagged had a #!/usr/bin/env python3 line but were tracked 100644. One of them, orchestrant/benchmark/gateway.py (added in a387f2e), was first linted by CI on b25f0eb.

    • The 16 that keep the shebang get the executable bit, set with git update-index --chmod=+x, which works with core.fileMode=false.

      • Ten are hashed into a tool_sha256: bench_agent, bench_chat, bench_coding, bench_embeddings, bench_tasks, bench_tools, bench_variants, geniex_toolcall_shim, lanes and openai_api.

      • inspect_gguf.py is sent to the model as --context-tokens padding (PAD_SOURCES).

      • provenance.py’s line count is frozen in file-size.allow.

      • bench_compare, bench_sweep, nas_census and upgrade_check are entry points run as python3 <path>.

    • The 6 helpers lose the shebang: client.py, gateway.py, stats.py, report.py, compare_lanes.py and compare_speed.py. They are in no fingerprint set and no PAD_SOURCES list, and every documented way of running them is python … or -m.

    • No tool_sha256 moves, and the wheel still ships 0644 .py. The sdist now carries 0755 for lanes.py, openai_api.py and provenance.py.

    • Why nobody saw them: ruff compiles its EXE rules out on Windows and skips them under WSL, so every check before the Linux container read clean.

  • benchmarks/prompts/tool-disambiguation.md is the bytes P8.1 measured in every checkout. The blob was LF (1786 bytes, sha256 30296646…) while P8.1 read the Windows working copy (CRLF, 1823 bytes, 970a8e4f…): a WSL, CI or fresh LF checkout had other bytes, and the gateway, which pins the raw bytes, would refuse to render from it. A -text rule in .gitattributes stores and serves the CRLF bytes unconverted.

  • A cut reply with no <think> marker has an unknown thinking share, not 0 %. Qwen3 templates open <think> in the prompt, so a reply cut before </think> carries no marker. The 9B distill’s two CUT rows of cpu-9b-classic-r3.json read 0 % beside 42–96 % on the seven that finished. answers.thinking_share records such a reply as thinking_char_share: null with a thinking_share_note, for the speed runner and for bench_coding, whose log prints think=  ?%. A finished reply with no marker keeps 0.0. answers.row_thinking_share and the viewer treat an older report’s cut 0.0 as unknown. The summary line and the viewer’s Think columns average the known shares and say how many are unknown ((N unknown), ? per prompt). The rule is per row, so the cut rows of instruct runs read unknown too — 37 in 15 tracked reports — though their 0.0 was probably true.

  • A tool call written as text fails the cases that want none. Llama-3.2-3B writes its calls as text JSON: cpu-llama3b-tools-r1.json passed all seven restraint and irrelevance cases as “no call”, and in its --accept-text-json run every one was a call written as text. This is shown for no_tool_arithmetic, whose reply both runs share byte for byte, and inferred for the other six. Those cases, and the follow-ups use_result, long_result and deep_history, now read the reply with the flag’s own parser whether or not the flag is set. Such a row fails with (written as text) and the start of the text after any </think> in detail, and records recovered: true. grade_followup loses its accept_text_json parameter. geniex_toolcall_shim.py is hashed into every case-suite report’s tool_sha256, so a baseline saved before prints BENCHMARK SOURCE CHANGED.

  • orchestrant-bench contract no longer times out a slow lane’s cold prefill (defect 7). Every request had 600 s, and Ollama at 4 threads (11.7–11.9 tok/s on the 9B) needed ~613 s for the 8000-token prefix check. --timeout sets each request’s timeout (default 600). The prefix-cache and overflow requests get at least their prompt’s tokens / 6 s, about half the slowest cold prefill measured: 1334 s at 8000 tokens, while the 2000-token default keeps 600. That is a floor: an explicit --timeout can raise it but not lower it. The output cap keeps its 1800 s floor. The stream-usage and /v1/completions stop checks, which had a fixed 300 s, now take the same timeout. The report’s config records timeout_s, prefix_timeout_s and overflow_timeout_s.

  • A reply that arrives in one burst has no per-row decode rate. Ollama sent three 8–12-token replies of the roadmap run’s ollama-t8-4b-instruct-speed-answer.json in one burst (latency == TTFT), and their 0.36–0.72 ms decode windows stored 9,733–26,712 tok/s. The viewer’s per-prompt table, bench_compare’s per-prompt pairing and the summary’s per-request range all read those rows. A speed row now also records decode_s, its decode window. Where that window is under 50 ms, or the reply has under 2 tokens, decode_tok_per_sec is null and decode_rate_note says why (answers.decode_fields). The shortest real window in the 25 tracked speed reports is 174 ms, and 50 ms is also above three ticks of the 15.6 ms clock time.monotonic had on Windows before Python 3.13. The floor withholds a real rate too where a short reply decodes fast: an 8-token reply keeps one only up to 140 tok/s. The pooled Decode figure reads decode_s, so it counts the same rows as before: every tracked speed report pools to the same figures, and the t8 run still reads 24.7 tok/s. Reports written before this change keep their stored per-row values, and the viewer’s per-prompt table and the summary’s per-request range still show them: the t8 run still reads 21.6–26712.0. bench_compare’s per-prompt pairing does not use them: answers.row_decode_rate reads such a row’s window back as (completion_tokens - 1) / rate and pairs no rate under 50 ms. Paired as stored, the t8 run’s three bursts set the noise band, and a rerun that lost 20 % on the other six prompts read noise +/-40% and passed.

  • bench_tools --turn-growth records why a turn failed. Turn 9 of the 9B run (cpu-9b-turn-growth.json) recorded only HTTP Error 400: Bad Request and logged ERROR HTTPError. A turn that fails with an HTTP error now adds http_status and response_body (the first 500 characters) to its row, and the log line prints both. error still holds the status line, and nothing else a row records changes. The body is read by orchestrant.benchmark.client.http_error_detail, the reader post_json’s callers can share. The case suite’s errored rows now record them too (next entry).

  • bench_tools records why a case errored, and its turn-growth loop answers every call. All three draws of the NPU lane’s long_result_find_failure in the upgrade check of 2026-09-24 (geniex-npu-tools.json), and its one draw in npu-tools-variants.json, recorded request failed: HTTP Error 400: Bad Request and logged ERROR HTTPError. An errored case row whose request failed with an HTTP error now adds http_status and response_body (the first 500 characters, read by client.http_error_detail), and its log line prints both on one line. detail, the score and a row that failed any other way are unchanged. The turn-growth loop answered only a turn’s first call (tool_calls[0]), so a turn with two calls sent the next request an assistant call with no tool message, which an OpenAI-compatible server may refuse. Every call now gets its own tool message, one per tool_call_id in call order; a call without an id is given one on the echoed call and its answer alike. Each turn records tool_call_count, and its line reads N tool_calls when N > 1. A turn is still one request and wall_s is its time. approx_context_tokens is still content characters over 4 and now counts every answer, so after a multi-call turn it reads higher than the old loop’s history would have. Whether this caused the 9B’s turn-9 400 is not known: its replies were not stored. The 9B does make several calls in one turn on that lane — it passed all three parallel cases of cpu-9b-tools-opencode.json, behind the same preamble — so a two-call turn 8 is possible.

  • One speed summary, printed the same everywhere (roadmap OPS-6). The speed runner’s table, orchestrant-bench report summary / report table and the Reflex viewer each averaged a run’s rows their own way and printed different headline tok/s for one report — for the tracked v070-npu-speed.json, Tokens/sec 18.3 avg and T/s: 18.3 (a mean of per-request rates, which the viewer charted as “overall”), Overall … 25.4 tok/s (the 427 prompt tokens counted as output) and Decode only 19.7 tok/s avg. orchestrant/benchmark/speed_summary.py now computes each figure once: Decode, the tokens after each first one over the seconds spent decoding them; Overall, completion tokens over the summed request time (each request’s latency, sent one after another); Prefill, prompt tokens over the summed TTFTs; TTFT, a mean. The rates are pooled across requests, like the CPU-rail J/token. Every printer reads it, the viewer through a speed block that report manifest now writes per run; that report reads Decode 19.6 and Overall 19.3 in all of them. Pooled, because a mean of per-request rates let 8-token replies weigh as much as 256-token ones: it put the GenieX v0.6.1 → v0.7.0 NPU decode loss at −13.4 % where the per-prompt median bench_compare prints is −14.7 % (pooled −14.8 %), and read 19.2 tok/s for a 2048-token CPU run that decoded at 13.9. Errored requests (any error key, even an empty message) leave every figure and are counted in the summary header and the report lines; the viewer now counts such a row as errored too, where it had listed it as a served request, averaged it into Answer and left it out of the Errors count. The runner’s Tokens/sec min/avg/max line is gone; the per-request range is printed beside Overall and Decode. report.summarise() returns the summariser’s keys (requests, errored, decode_tok_s, overall_tok_s, …) in place of n and tokens_per_sec. The viewer’s columns are now “Decode tok/s” and “Overall tok/s”, and its model card shows “Avg overall tok/s”. A manifest written before this change shows “-” there; rebuild it with orchestrant-bench report manifest. The published decode figures this moves are corrected in benchmarks/docs/geniex-v0.7.0-cpu-npu-2026-09-24.md. tests/unit/frontend/test_viewer_job.py fails any viewer test that imports the package, which the viewer CI job’s environment cannot.

  • bench_compare exited 0 on a speed verdict it had not judged. compare_speed.py’s per-row check printed a CPU lane’s decode slower, NOT judged when the new run’s own requests ran over 0.3 other cores, or faster, NOT judged against a loaded baseline, and when neither start shut the load gate the pairing still exited 0, “no regression detected”. Such a decode verdict is now withheld through the pairing’s gate (WITHHELD for load: … decode tok/s (its requests' load)), so the run exits 4 unless --allow-load-difference: the tracked GenieX v0.6.1 → v0.7.0 CPU speed pair (+9.2 %, faster, NOT judged) now does. A flat rate against a loaded baseline is unchanged, NOT judged too: a busy baseline understates the old rate and hides a real drop as well as it fakes a gain. A slower new run against a loaded baseline is still judged — the real drop is only larger.

  • bench_compare read a changed lane set as a runtime slowdown. When two lanes reports ran different sets of lanes, every tok/s was judged anyway: a dropped lane read as the aggregate *** SLOWER ***, and an added one as the surviving lane slowing (NPU 22.9 → 8.8 tok/s beside the CPU lane). No tok/s is judged across a changed lane set now (NOT judged: the lane set changed) – neither a regression nor a load withholding, so exit 4 keeps one meaning – and a lanes pair with nothing else like-for-like is NOTHING COMPARED (exit 3), not “no regression detected”. The upgrade_check MANIFEST legend now reads its exit-4 reasons from compare_verdict, so it names the per-request case too.

  • bench_sweep measured a backend’s own URL when the candidate overrode it. A candidates entry naming a backend and a base_url was gated at the override, and then every tool got --backend alone and measured the registry’s URL. Both flags are passed now: the URL wins, and the backend’s entry still supplies headers and keys. With --baseline, _sweep.json records conditions_differ (bench_compare exit 4) beside regressed.

  • bench_sweep never passed --repeats to bench_agent (the P7.4 review): a sweep asked for three draws measured the agent once. bench_tools, bench_chat and bench_agent now share one argv builder, and --repeats below 1 is refused before anything runs (bench_agent rejects it; bench_coding, bench_tools and bench_chat would measure nothing).

  • bench_coding recorded a wrong non-Python reply as CUT. looks_truncated() checked an unclosed final fence with Python’s compile() for every language, and no PowerShell, bash, CMake or Dockerfile answer parses as Python. So a wrong reply that stopped on its own (finish_reason: stop) and forgot its closing fence was left out of the rate, the interval and the rank as unmeasured, instead of counted FAIL. The probe is now Python-only. For other languages the finish reason, the token cap and whether the fence is closed decide. Python grading is unchanged. Older reports cannot be re-graded: rows store neither the finish reason nor the raw reply.

  • upgrade_check recorded no model files for any lane. It called runtime_info(base_url) without the lane’s model, before and after its steps. A GenieX runtime therefore named no model_files, and weights re-pulled under the same id mid-check read as the same lane. Both probes now pass the lane’s backends.json model, and a lane whose provenance.model_files_notes() reports a change during the check (MODEL FILES CHANGED behind …, an edited genie_config.json or HTP extensions file) fails the check.

  • hostload.LaneProcess reported a listener whose pid is hidden (another user’s process) as “no local process listens on port N”. It now says the pid is hidden and records listens_here, so a lane inside WSL is never attributed to a Windows process on the same port.

  • bench_coding’s thinking_char_share read a reply cut off inside <think> as 0 % thinking. It followed only a closed </think>; it now uses the speed runner’s rule (answers.split_answer: an unclosed <think> is all thinking). Found on Qwen3-8B W4A16 on the NPU lane, CUT on every coding task with think=0 %.

  • A suspect case’s seconds leave every wall statistic (roadmap P1.3’s known gap). mark_suspect_cases() never recomputed wall_measured_s, the field bench_compare’s timing verdict prefers, so after a case the control also fails was excluded, the full wall was divided by the reduced attempt count: 26 s over 2 kept attempts read 13 s each, where the kept rows say 3 s. median_wall_s was also only redone when total_wall_s was present. Now total_wall_s, wall_measured_s and avg/median/stdev_wall_s are recomputed over the kept rows. unmeasured_wall_s is left alone on purpose: it goes with the errored and truncated counts, which are not recounted either, and no verdict reads it.

  • --prompt-variants inflated effective_n: it counted each (case, variant) pair, or each attempt, as its own observation. A case’s phrasings are now one observation per round, observed through the prompt as written, and this still holds after a control’s suspect cases are removed (mark_suspect_cases re-derives it through bench_variants.variant_spread).

  • bench_agent refused a conftest.py or pytest.ini added beside the protected tests but accepted a new pyproject.toml, .pytest.ini or setup.cfg. Any of them could deselect fix_failing_test’s red test through addopts, and the run scored a PASS. All six pytest config files are override files now, and --self-test has a pytest config added cheat row.

  • tool_sha256 missed two graders and depended on the checkout path. bench_coding’s now covers bench_tasks.py (the extended and language sets) and bench_tools’ covers geniex_toolcall_shim.py under --accept-text-json; before, an edit to either moved scores with no BENCHMARK SOURCE CHANGED. Files are ordered by name, so the same source no longer hashes differently depending on where the checkout lives.

  • The benchmark viewer page never compiled. import frontend.frontend passed, but building the page raised five errors in turn: a foreach over an Any-typed var, a Python if on a Var, a table cell passed ten arguments, .length() on an untyped chart item, and card() passed a width= it did not accept.

  • The viewer never found its manifest when started as documented (cd frontend; reflex run): relative paths were read from frontend/. They are now read from the repository root.

  • The viewer’s Hardware card showed a contract report’s provenance (“? cores / ? threads”, “? GB”) whenever that report sorted before a speed run.

  • Two GenieX v0.7.0 (and v0.6.1) defects the lab had read as properties of the models. temperature: 0 is treated as “unset” on both lanes and the default sampler runs (top_k: 1 gives greedy decoding; temperature: 0.01 is honoured as a low temperature); and an identical request sent twice in a row takes a cache path that changes the reply — the llama.cpp lane prefills 0 tokens and samples the first token from the previous reply’s logits, the QAIRT lane reuses part of the dialog. Every --repeats attempt after the first, the determinism probe and the contract’s T=0 and seed checks measured that path; the QAIRT lane’s “order dependence” was this. bench_tools and bench_coding now send a throwaway request between repeats of one case (client.spacer, recorded as config.repeat_spacer), and the probes are spaced. Live probes and their output are in the run directory’s v070r2-probes/.

  • Thinking share and “time to a finished answer” were computed over cut-off replies: an unclosed <think> scored 0 %, the summary averaged only the rows that closed it, and time to the token cap was the ranking metric. On the v0.7.0 CPU lane that printed 86 % where the run was ~95 %, and 12.5 s “to a finished answer” for a run in which 6 of 9 replies never left <think>.

  • Net energy against one 5-second idle baseline. Two NPU runs 15 minutes apart read 1.26 and 1.84 W, which turned a +21 % gross difference into a published “+70 %”. The baseline is now taken before and after the requests, rows are netted against the mean, and a drift over 0.2 W is reported.

  • Per-request energy edge cases: the EMI energy and timestamp counters are read in one PDH collection (two queries could pair a new stamp with the previous second’s energy), a counter that went backwards or stood still is not a sample, --idle-seconds 0 no longer divides by zero, and a lane process that exits reads as unknown rather than 0 CPU-seconds.

  • lanes summed per-lane rates over unequal windows (published 0.65×/0.66× for NPU+CPU; 0.54×/0.56× delivered). The report’s aggregate tok_per_sec is now delivered throughput (the sum stays as summed_tok_per_sec, and bench_compare compares sums when one side is an older report); it sends a fresh prompt per phase (the “together” request was a cache hit) and counts thinking tokens.

  • Contract answers on insufficient evidence: an empty /v1/completions reply was “stop honoured”, any 5xx mentioning “context” was a clean overflow, a 0.4 s cold prefill decided the cache rows, and thinking in reasoning_content was “no <think>”. The first three now answer inconclusive or no; thinking in reasoning_content/reasoning (or counted in reasoning_tokens) answers yes. contract --base-url detected the model on the module default URL, not the lane it probed. power_mode_understood asks whether the value is validated, times the reload it causes and ends with a plain request so the lane is left as launched.

  • effective_n counted agreeing repeats as independent trials. When every repeat agrees on pass/fail the case is the unit, whatever the text did.

  • tool_sha256 depended on line endings (a Windows and a WSL checkout of one commit disagreed); it is normalised for every tool. The speed runner also takes it at the start and names a mid-run change; since the run-start record under Added, every other tool does too.

  • Energy was recorded for remote lanes, from this host’s rails; the meter now runs only when the lane’s process is local. A counter that went backwards restarts the curve instead of unmetering the rest of the run, and a single idle baseline reports net_reliable: null, not true.

  • mark_suspect_cases and the viewer undid the counting rule: excluding a control’s suspect cases reset effective_n to attempts for a lane whose repeats agree, and the viewer rounded passed × n / total into a count; both now use the cases and effective_k.

  • cp1252 crashes on Windows: a redirected contract --diff or bench_compare died on → with exit 1 — their “changed”/”REGRESSION” code. Every CLI now writes UTF-8.

  • bench_tools --turn-growth dropped --tools and --context-tokens, so an opencode-preamble turn-growth run measured the eight default tools.

  • bench_sweep put the repository’s parent on PYTHONPATH for the tools it runs.

  • The determinism probe measured the wrong thing. It sent two identical requests back to back, and on GenieX an identical follow-up takes a cache path that changes the reply (above): two open-ended T=0 requests to the v0.6.1 QAIRT lane came back different, while the old 8-token “ready” reply happened to survive it. It now asks for 48 open-ended tokens with an unrelated request between the draws. Spaced, the QAIRT lane is reproducible — its bundle samples from a fixed seed, re-seeded per request — and the probe records it deterministic, so bench_compare’s strict per-case mode applies there; the llama.cpp lanes, which sample at T=0 (“unset”), record not deterministic.

  • cpu_percent never saw the request. It averaged a sample taken before the request and one taken after it; a CPU lane pinning 7.5 of 8 cores read as idle. It is now integrated over the request wherever the lane shares the harness’s host (cpu_percent_method says which).

  • orchestrant-bench speed no longer guesses a model from a listing. GenieX answers /v1/models with its whole local cache, so --base-url alone benchmarked — and made the lane load — whichever cached model sorted first. Several listed ids are now a refusal that names them.

  • Windows hosts: ~33 s of dead time per prompt, ~40 s per report, and a paid host probed on every run. A refused localhost connect costs 2–4 s on Windows; the sampler asked four Glances URLs twice per prompt and busy_lanes() asked every registry entry in turn — including the one marked probe: false, whose discovery request costs money. A failed Glances is now asked once per run, the lane probe is parallel and honours probe: false, and System Idle Process (pid 0) no longer ranks as the busiest process.

  • The Linux static-analysis gate could never start, so it graded nothing. Every Linux lane run from 2026-09-12 to 2026-09-15 ended static analysis (orchestrant) FAILED (6 of 6) with six identical error: Failed to spawn: codespell / bandit / vulture / ruff / ty lines — not one finding, on either arch. The 231-package uv sync landed correctly in .venv_static_analysis, because uv_sync_project reaches it through _CURRENT_VENV_PATH; the tools then looked somewhere else. uv_venv_create deliberately does not activate, and this wrapper clears VIRTUAL_ENV to keep the sync off the image’s root-owned /opt/venv, so the driver’s uv run --active had no active environment, fell back to the project default .venv, created it, installed the 28 base dependencies and found none of the analysers — they are declared in the test EXTRA, which that fallback sync does not install. It could only ever pass on a box where .venv_static_analysis already existed, since uv_venv_ensure activates on its reuse branch; on clean CI it could not. scripts/linux/ci_static_analysis.sh now exports UV_PROJECT_ENVIRONMENT (the driver’s own VENV_DIR, computed by mirroring detect_workspace) and UV_NO_SYNC=1 — the second is not optional, because uv run would otherwise re-sync that environment with the DEFAULT extras and uninstall the tools it is about to spawn. All six analysers now read this tree. They are not silent about it: ruff check and ruff format are clean, and codespell, bandit (52 findings — 43 low, 9 medium, 0 high), vulture and ty report real ones. Triaging those is a suppression-policy decision and is deliberately left open rather than excluded away.

  • The pytest console script can collect tests/unit/frontend again. tests/unit/ and tests/unit/frontend/ are packages but tests/ is not, so under pytest’s default prepend import mode the basedir of tests/unit/frontend/test_benchmark_data.py was tests/ itself: the repo root never reached sys.path, and its from frontend.frontend import benchmark_data raised ModuleNotFoundError: No module named 'frontend'. That is the two-error collection failure that failed the Windows lane’s Python 3.13 test step, and it would have failed the Linux lane’s too as soon as the static-analysis step above stopped aborting the job before the tests ran. python -m pytest hid it by putting the CWD on sys.path regardless — which is why benchmarks.yml never saw it and the CI drivers, which call uv_run pytest, always did. Fixed with pythonpath = ["."] in [tool.pytest.ini_options] (and minversion raised to the 7.0 that option needs, so a pytest 6 cannot ignore it silently). An __init__.py in tests/ fixes the same import and is the wrong fix: it renames these modules to tests.unit.*, which collides with benchmarks/tests/ — also a package, also imported as tests — in the single session benchmarks.yml runs over both trees. Measured rather than assumed: that swap turns the green benchmarks job into ModuleNotFoundError: No module named 'tests.unit'.

  • CI grades the tree as committed. The lanes ran ruff check --fix and a bare ruff format, which repair the checkout CI is about to delete: every auto-fixable finding was invisible, and formatting could never fail. Both now run ruff check --no-fix and ruff format --check --diff.

  • Real type and correctness findings the silent gate had been hiding. orchestrant/monitoring/gpu.py called eleven pynvml attributes through a module | None that the PYNVML_AVAILABLE bool never narrowed; pipeline/capture/gstreamer.py read .stdout off a Popen | None and called .readinto behind a hasattr that narrows nothing; smoke/checks.py called .tolist() on the non-tensor arms of InferenceSession.run’s return union; streaming/app.py cached its Flask app on a function attribute. Several of these were “handled” by # type: ignore[...] comments in mypy syntax, which ty does not read.

  • The viewer render() API takes its nine telemetry arguments by keyword. They were positional-or-keyword (ruff PLR0917); every call site already passed them by keyword except one internal wx.CallAfter.

Security#

  • orchestrant/pipeline/capture/gstreamer.py carries explicit, justified # nosec B404/B603 markers on the gst-launch spawn instead of relying on bandit’s result being discarded. Bandit is now part of the gate, so a NEW finding fails CI.


0.0.28 - 2026-09-06#

Changed#

  • Project renamed Orchestr-ANT-ion → OrchestrANT. The old name spliced ANT into “orchestration” with hyphens; every other repo in the family (ANThology, OxidANT, AccelerANTgine, OmniAccelerANT) capitalises an ANT the base word already contains, with no separators and no Kataglyphis- prefix. What moved:

    • distribution Orchestr-ANT-ion → OrchestrANT

    • import package orchestr_ant_ion → orchestrant, so from orchestr_ant_ion.pipeline import X becomes from orchestrant.pipeline import X

    • console script orchestr-ant-ion-smoke → orchestrant-smoke

    • repository Kataglyphis/Kataglyphis-Orchestr-ANT-ion → Kataglyphis/OrchestrANT


0.0.27 - 2026-07-14#

Added#

  • IREE (iree.dev) is now a declared dependency of the ml-ai* extras: iree-base-compiler + iree-base-runtime (guarded platform_machine != 'riscv64'). PyPI ships cp312-abi3 wheels for x86_64/aarch64 that install on Python 3.14, so iree.compiler + iree.runtime are now actually present for the existing check_iree smoke to exercise (MLIR compile + local-task run, abs(-5)=5). On riscv64 there is no PyPI wheel — ANTfrastructure source-builds the runtime wheel into /opt/wheels (compiler stays absent there, so check_iree degrades to optional-fail, non-gating). Kept in sync with ANTfrastructure IREE_VERSION (v3.11.0).


[0.0.26] - 2026-07-14#

Added#

  • Wheel smoke: check_opencv now round-trips JPEG (.jpg) alongside PNG — JPEG is the app’s live MJPEG streaming codec, so it is exercised directly rather than assumed from the PNG result.

  • Wheel smoke: check_onnxruntime asserts CPUExecutionProvider is in get_available_providers() (catches an execution provider silently dropped from the on-target build).

  • Wheel smoke: new optional checks check_opencv_dnn (cv2.dnn module + protobuf link via blobFromImage), check_opencv_codecs (TIFF/WEBP/OpenEXR round-trip; surfaces per-arch codec drops), and check_opencv_freetype (cv2.freetype text rendering — validates the source-built freetype on riscv64). All three are non-gating (WARN) so a per-arch feature gap is visible without failing the smoke.


[0.0.22] - 2026-07-12#

Added#

  • orchestr_ant_ion.smoke — a shipped wheel smoke-test module. Each check does real work (torch autograd + a linear forward/backward, torchvision ops.nms, an embedded ONNX Add inference, an OpenCV encode/decode/cvtColor round-trip, Pillow, a torch↔numpy ABI bridge) rather than a bare import, so a mislinked compiled extension is caught even when import succeeds. Run it with:

    python -m orchestr_ant_ion.smoke        # text report, exit 1 on failure
    python -m orchestr_ant_ion.smoke --json # machine-readable
    

    Also exposed as the orchestr-ant-ion-smoke console script. Container images (Kataglyphis-ANTfrastructure) invoke it under emulation to verify the ML stack.

  • LiteRT is checked as an optional runtime (WARN, not a gate failure) and probes both module names (ai_edge_litert upstream / tflite_runtime custom).

Changed#

  • Bumped PyTorch to 2.13.0 and TorchVision to 0.28.0 across every backend extra (pytorch-cpu / -cu130 / -rocm71 / -custom, the torchvision build-deps, and the riscv64 source git refs @v2.13.0 / @v0.28.0).

  • Regenerated uv.lock for the new torch/vision pins.


[0.0.21] - 2026-07-12#

Added#

  • pytorch-custom optional-dependencies extra — a “bring your own wheels” PyTorch backend. torch/torchvision are declared as plain pins with no index or git source override, so a prebuilt/custom wheelhouse satisfies them directly:

    uv sync --extra pytorch-custom --find-links /path/to/wheels
    # or: export UV_FIND_LINKS=/path/to/wheels && uv sync --extra pytorch-custom
    

    Ideal for platforms without upstream binaries (e.g. riscv64) or your own optimized torch build — zero source builds, and no per-package --no-install-package / force-reinstall dance. Mutually exclusive with pytorch-cpu / pytorch-cu130 / pytorch-rocm71 (wired into [tool.uv] conflicts).

Changed#

  • Scoped the resolved lock to the environments whose PyTorch is resolvable from public indexes via [tool.uv] environments (linux non-riscv64, macOS, Windows). riscv64 has no upstream torch wheels, so it now resolves fresh at uv sync time and picks the custom wheels up from --find-links; uv sync --frozen falls back to a live resolve automatically.

  • Regenerated uv.lock (resolves 262 packages; verified consistent with uv lock --check).


[0.0.20] - 2026-07-11#

Added#

  • Container/runtime-oriented ML dependency extras ml-ai-webgpu, ml-ai-nvidia, and ml-ai-rocm, mirroring the ONNX Runtime / PyTorch backend combinations that Kataglyphis-ANTfrastructure previously patched into pyproject.toml at install time. Selecting a backend is now a first-class extra rather than an install-time patch.

Changed#

  • Regenerated uv.lock for the new extras (resolves 261 packages; verified consistent with uv lock --locked).