The current source checker reports updateAvailable with behind null when GitHub
compare cannot count staged commits; the app-update predicate demanded an integer
behind and refused every HEAD->NEXT leg. Require behind > 0 only for the historical
shape without updateAvailable.
v2026.6.19's DMG bootstrap runs the same install.sh that writes .install_method,
so its macOS leg saw a dirty tree. Share the Linux driver's guarded exclude through
source-driver.sh and apply it before every app-driven macOS update.
Packaged Electron rejects NODE_OPTIONS --require, so the preload never reached the Desktop source check. Wrap only the E2E installation launcher and route its source-check call to the real staged Git main with an explicit branch; other launcher calls remain unchanged. Keep production channel resolution fail-closed.
The HEAD -> NEXT Windows legs take every git off PATH so the product must
provision its own; readInstallationCommit spawned bare `git` and died with
spawnSync git ENOENT before the chat checkpoint could judge the product.
route already means "which legs run"; a leg name is the most specific
route. Presets keep their meaning (all, the linux trio, windows-desktop,
macos-desktop, the bundled three); any other value selects legs by name,
so a leg name, a fragment of one, or the job name GitHub shows
("<leg> / e2e") runs just those legs. A route that selects nothing fails
instead of producing a green empty run.
The generator is now the one interpreter of route for source legs: the
linux/windows/macos jobs run when it selected legs for them, replacing
three hand-kept preset lists. Dispatch route becomes a string input (a
choice cannot take a leg name) and reaches the gen step through env, never
interpolated into the script.
- tools/browser_tool_install.py: keep pm-clean's frozen old-updater stub; main's
UTF-8 decode fix touched only the npx prefetch body it replaces.
- tests/hermes_cli/test_update_scoped_reconciliation.py: keep pm-clean's test
subset (catch-up rides the PM completion owner) and take main's gateway-less
host evidence (#120740): the updated seed that holds the host at a running
gateway, and the two gateway-less matrices for the source change that merged
cleanly into update_cmd_fleet.py.
Independent-review follow-up for the group room failed-turn fix.
- Group room poll: a retained error newer than the pre-submit snapshot
(turn start replaces it with a fresh started_at) is this turn's failure
and wins over transcript text. The core closer writes no failed-turn row
behind a tool row, so "said X, called a tool, provider 401" left X as the
newest assistant row and the room posted it, dropped the 401 and re-drove
the member to the round cap (live repro on the PR head).
- Both pickers end the turn at a failed_turn row instead of scanning past
it; with the retained error gone (backend restart) the row's notice is
reported through the failed path instead of a silent pass / dropped
stranded marker. The stranded harvest treats a retained error as the
stranded turn's own the same way.
- REST cold-load/paging (/api/sessions/{id}/messages, /messages/around)
type legacy untyped notice rows like session.resume does, via one
read-side helper in agent/turn_failure_copy.py.
- Gateway closer: the fresh-session closure test now asserts the row's
display_kind through a real SessionDB round-trip (red without the
run_turn.py stamp).
- E2E: mock trigger that says text + calls a tool, then 401s; spec asserts
the room reports the error and never posts that text.
Install/update completion rewrites the root install-stamp.json (fresh
builtAt) after products are built, and the stamp was still a shared
web/desktop source input, so every install left its own web_dist
"missing, stale, or damaged" and the post-install probe failed on every
installer E2E leg. Nothing in either build reads the root stamp;
desktop's baked stamp is already tracked as a prepared input.
Change-detectors, tautologies, source-reading tests, redundant duplicates,
mock-echo tests and dead/unrunnable tests. Per-test rationale in the lane
ledger (category + reason for every removal).
The v2026.6.19 Desktop starts its source backend through the host Python.
Windows cannot expose that process environment or working directory. Carry
the app's verified source root into the existing strict provenance check.
A PM update can leave a locked historical executable beside the current
command-file launcher. Settle the updated runtime through the command file and
use verbatim cmd.exe arguments so paths with spaces remain valid.
Verified with the focused Desktop smoke tests, the tests-js typecheck, syntax
checks, and a native Windows ARM command-file launch probe.
Run deferred source dependency and bundle replacement work under the same
sanitized environment used by Electron before attaching Playwright. This
keeps the expected process boundary outside the smoke session.
Expose the updater feed helper in its declaration contract. Declare the
JS test workspace's type and native fixture dependencies so the minimal
Termux install can typecheck and run its check suite without hoisted deps.
electron-builder.config.cjs copied CLOUDFLARE_R2_PUBLIC_URL into the
generic publish provider after only trimming a trailing slash. That URL
ends up in app-update.yml, which every installed client trusts for the
life of the build, so an http://, credentialed, or query-bearing var
would have shipped silently.
update-feed.cjs (the feed contract) now owns feedBaseUrl(): https only,
no credentials/query/fragment, spelled as the URL parser re-serializes
it (trailing slash tolerated, like the Python side). Both the Windows
and macOS publish entries read the validated value.
Merge origin/main at 8e806ae1b2. Keep native helper compilation in
prepareDesktopNativeDependencies and keep bundling/beforePack consume-only.
Bind helper sources and headers into preparation identities and cache keys;
copy admitted executable resources beside node_modules and preserve signing
semantics in product freshness checks.
Verified desktop typecheck, focused native/packaging/UI and gateway/cache
tests, and the real Linux preparation/copy/Xvfb execution path. Incoming
upstream anti-slop findings remain unchanged; no baseline was raised.
Merge fallout (my resolution errors, all caught by CI):
- hermes_cli/backup.py + gateway.py: `theirs` on those hunks re-imported clusters HEAD had
already moved to backup_restore.py / kept in the facade. backup.py loses the 349-line
duplicate (main's #110179 fix is ported into backup_restore._import_db_member); the
systemd service-unit cluster returns to gateway.py (PM's _prepare_service_launcher /
_pm_managed_node_dirs / _systemd_command have no home in main's extraction) with main's
utf-8-sig read. gateway_service_unit.py is dropped.
- gateway/run.py: main's plugin-update chore is not profile-scoped (the housekeeping
ordering test pins the scope/drain sequence).
- pyproject + 30 test files: `import yaml` -> `import hermes_yaml as yaml` (pm-clean has no
pyyaml); gateway/config._bundled_platform_manifest_name reads through hermes_yaml.
- tests re-seamed onto pm-clean's shape: residency admission (installed_engine),
supervisor child env (binary is a constructor argument), update import guard
(update_cmd_deps is gone; our probe already scrubs PYTHONPATH — both #115032 invariants
pass), shallow-count git responses (stash path asks `status --porcelain -z`); dropped
tests for retired code (_run_node_bootstrap/_ensure_tui_node, Windows resume demotion).
- tests/tools/test_local_env_blocklist.py: restore the two helpers the suite-reduction
commit dropped and the blocklist import.
Real fixes:
- pm: classify_uv_failure/ResolutionConflict move beside the uv runner (pm.environment,
stdlib-only). pm.workspace imports tomllib at module level and cannot load on the 3.10
bootstrap python that streams uv output in the Docker arm64 image.
- tools/browser_tool.warm_agent_browser_npx_cache: back as a permanent definition — it is on
the frozen old-updater surface, and the revert-scheduled compat pointer does not count.
- hermes_cli/memory_setup: the dashboard's pip row uses pm.environments.
running_from_selected_environment for installed vs restart_required.
- scripts/windows-build-deps.ps1: export DISTUTILS_USE_SDK/MSSdk so setuptools trusts the
primed MSVC environment instead of asking vswhere (`env -i` test runner on win32-arm64
compiling ruamel-yaml-clib); run_tests.sh forwards them.
- tests/pm/test_windows_build_deps.py: start the protocol test from a parent env without the
toolchain variables the runner job already exports.
- tests/conftest.py scrubs HERMES_BUNDLED_PLUGINS (Nix-wrapped hermes on the dev host);
tests/home_io_guard.py treats sys.path site-packages under the real home as the
interpreter's installation (PM-activated developer shell).
- tests-js: four `curly` lint errors from main's new scripts.
Production:
- agent/bedrock_adapter.py, agent/vertex_adapter.py: pm.ensure_import ran at
module import. In any process that imports these modules without a committed
PM selection (CI's build_environment test venv, a fresh checkout) that sync
rebuilt the dependency environment mid-process and replaced sys.path with a
generation missing the caller's own packages (anthropic, aiohttp vanished).
The extra is now ensured at first client build / credential request.
- plugins/platforms/matrix/adapter.py: a complete install needs no
ensure_and_bind round trip; only a partial one syncs.
- tools/browser_tool.py: drop the facade's duplicate warm_agent_browser_npx_cache
shim; the compat pointer already resolves to browser_tool_install.
Test harness:
- tests/home_io_guard.py: PATH-entry probes (shutil.which) and the running
interpreter's own installation (stdlib reads, realpath ancestry, fixture
symlinks into it) are not Hermes state; a patched Path.expanduser must not
crash the guard. run_tests.sh no longer filters PATH — the guard owns it.
- tests/tui_gateway/conftest.py: import hermes_bootstrap before any file opens
a MagicMock hermes_constants window (6 files exited the process at boot).
- tests/hermes_cli/conftest.py probe_root: scratch checkouts the import guard
probes need hermes_bootstrap.py (the launcher imports it).
- tests/pm/_fixtures.py stage_host_python: a copied relocatable python needs
its stdlib beside it (No module named 'encodings' on CI).
- tests/install/e2e-assets/smoke-env.mjs: dependency-free env shaping so the
source-build-env probe runs under bare node (main deleted the Playwright
entry it was imported through).
- adapt main's new tests to branch seams (model_metadata_http, launch
completion tail, CI toolchain exports uv after python, source_launch
hermes_cli stub, systemd_notify single marker).
- check_no_tmp_literals: resolve scratch via tempfile/os.tmpdir; the termux
container mount point is one marked variable per script
- ruff TID251: desktop E2E fixtures may reach PM internals like tests do;
the pm.runtime_stage ban message no longer names a module that never existed
- auth_codex: build the capped httpx stream subclass on first use so importing
hermes_cli.auth_codex no longer forces httpx (the lazy proxy in auth_constants
was defeated by a module-scope base class; broke lanes without httpx)
- desktop-smoke: launchApp is a parameter; the bundle-env test substitutes a
refusing launcher instead of letting Playwright spawn a dying binary
(3 unhandled rejections failed the tests-js lane)
archive-inputs became a validate step (b1aa63b918) and the build gates grew
inputs.channel with channel builds; publication now reaches build-<platform>
through assemble-win32-bundle. The desktop-builder fixture also needs the
msix-shared helper product-identity.cjs requires and the preview-guest preload
entry the electron bundler compiles.
The runner maps a hyphenated input to INPUT_LOCK-SOURCE, not INPUT_LOCK_SOURCE.
The registration step read the underscored name, saw nothing, and threw
"invalid lockSource" from every job that composed setup-pm, so the whole CI
run failed before any consumer ran. The unit test used the same wrong
spelling, which is why it stayed green; it now drives the real mapping.
assertBackendOrigin demands evidence of the tree a module-launched backend
imports from. On Windows there is none to read: the venv launcher hands the
interpreter over as a system python, so argv never names the tree, and the
platform exposes neither the process's cwd nor its environment. Every desktop
leg there died with
Source backend listener imports a different source tree
(cwd=(unreadable), HERMES_PYTHON_SRC_ROOT=(unset),
executable=C:\hostedtoolcache\...\python.exe, ...)
while the backend was in fact the installation's own: its app log says
`[backend] `serve` supported for Hermes at <root> (venv: <root>/venv)` and it
came up and served.
The two facts that matter are already established before the assertion runs:
localBackendProcess only returns a listener that is a descendant of the app
process the driver launched, and the driver asserts the root that app reported
resolving against options.root. So pass that reported root through as evidence
and accept it -- but only when the platform supplied no process evidence at all
(no cwd, no PYTHONPATH, no VIRTUAL_ENV), which keeps the platforms that can read
one exactly as strict as they are today.
Verified: tsc -p tests-js/tsconfig.json --noEmit clean; the new invariant test
covers the Windows shape, its control row (nothing readable and no report is
still a different tree), a report of some other tree, and the case that matters
most -- readable evidence naming another tree is NOT rescued by the app's report.
The only failing test in that file is the pre-existing headless Electron launch.
The backend-origin assertion accepts a command that names the installation
root, which is the LINUX shape. On macOS `<root>/venv/bin/python` is a symlink
to the framework binary and the app resolves it before spawning, so argv names
that binary and never the root; on Windows the install's venv copy is bypassed
for the toolcache interpreter. Twelve macos legs and their windows twins died
with "Source backend listener imports a different source tree" while the
backend was the installation's own: its log reads
`[backend] serve supported for Hermes at <root> (venv: <root>/venv)`, and it
came up and served on a real port.
The app binds that backend to the tree by ENVIRONMENT -- main.ts puts
`[ACTIVE_HERMES_ROOT, process.env.PYTHONPATH]` on the backend's PYTHONPATH and
VIRTUAL_ENV names its venv, which is how `import hermes_cli` resolves from the
installation. macOS reads a process environment through `ps eww`, so the
assertion now also accepts an installation-root entry in PYTHONPATH, or a
VIRTUAL_ENV whose parent is the root. Windows exposes no equivalent reader, so
a Windows backend stays exactly as strict as it was.
Verified: `vitest run tests-js/scripts/desktop-smoke.test.ts` passes including
the new test (macOS shape accepted; control row with no environment evidence
still a different tree; evidence naming another tree still rejected), and
`tsc -p tests-js/tsconfig.json --noEmit` is clean.
The update window's checkpoint submitted the ROOT checkpoint's prompt: the
backend turn for the window's fresh session carried the earlier checkpoint's
exact text, so the witness could never match the prompt the smoke typed. The
app persists its composer draft across launches, so the window boots with the
previous checkpoint's text already in the composer.
Select-all + Delete it and refuse to type until the composer reads empty, so
the checkpoint proves its own input rather than inheriting a draft.
The draft-clear poll passes vacuously: the composer reads '' when the editor
never accepted the keystrokes, so a window whose chat is inert looked exactly
like a window that sent and cleared. Assert the echoed prompt first, so the
checkpoint fails fast and says which half broke.
On failure, also write the renderer's own evidence (`desktop-chat-<phase>-renderer.log`:
timeline-free console lines plus thread/composer DOM state) — from the mock's
side a swallowed send and a send that was never made are identical.