pnpm 12.15.0 (native executable) installs every JS workspace from one
pnpm-lock.yaml, imported from package-lock.json with the same resolved
versions. website/, the WhatsApp bridge and the photon sidecar stay separate
npm projects.
Configuration moves from .npmrc and the root manifest into
pnpm-workspace.yaml: the 14-day minimumReleaseAge and its excludes,
overrides, allowBuilds (the old allowScripts set, with strictDepBuilds
failing any unlisted build script), engineStrict, sideEffectsCache: false
(in-place rebuilds must not write into the store) and
verifyDepsBeforeRun: false (a `pnpm run` never installs).
The pnpm version is pinned in pm/lock.json and in root devEngines.
pmOnFail: ignore keeps that pin out of pnpm-lock.yaml: a leading pin
document makes osv-scanner read zero packages. scripts/build/node-deps.mjs
enforces the exact version instead, and every lock reader refuses a
multi-document lock. A root preinstall refuses `npm install`; devEngines
onFail is warn so npx (MCP servers, the tsc linter) keeps working inside
the checkout.
User-machine installs run --frozen-lockfile --trust-lockfile (release age is
checked when locking, in CI, not per user). node-deps removes npm-era
node_modules trees before the first pnpm install. Nix builds the store with
fetchPnpmDeps and a hash file that CI checks.
main already ships `hermes plugins update` (no name = update all) and the
opt-in `plugins.auto_apply` cadence. This makes both reachable from every
surface and closes the gaps reported on the original PR:
- Catalog installs now ride `plugins.auto_apply` too (it moved only git
rows, so catalog plugins stayed on their install-day pin). The update
path still refuses a widened pin or a too-new requires_hermes unattended.
- One updater per plugin: a cross-process lock around update_one and the
dashboard/Desktop update, so the gateway's auto-apply tick and a manual
update never run on the same plugin at once; the loser changes nothing.
- `hermes update` names, per profile, the catalog installs its new catalog
pins left behind and re-arms that profile's plugin check, so an opted-in
gateway applies them on the next tick.
- New CLI verb `hermes plugins auto-update [on|off]`; `/plugins update` and
`/plugins auto-update [on|off]` in the classic CLI and the TUI (slash
worker).
- RPC `plugins.manage` actions `update_all` and `auto_update` (contract +
regenerated apps/shared outputs) for Desktop.
Off by default (legacy behaviour); all writes are profile-scoped through
get_hermes_home()/the scoped RPC.
Review: the fork mode spread its server state, lock, process creation and
process-group rules through the runner, so _run_one_file_once had to know that
a forked child calls setsid() while a subprocess needs getpgid(). The runner
now calls start_pytest() and gets the process and its group back; the fork
server, its lock and the Popen shim live with it. Behaviour is unchanged.
classify_changes lists the launch module and the fork child as test harness
next to run_tests_parallel.py, so editing either starts the lanes a runner
edit starts (the fork child was missing since it was added).
scripts/whatsapp-bridge/*.test.mjs (allowlist, delivery_status,
bridge.native, ...) ran in no workflow: the bridge is a standalone npm
package outside the root workspaces, so run-workspace-checks never sees it.
js-tests.yml now installs the bridge's own lockfile (npm ci
--ignore-scripts; two suites import baileys) and runs
`node --test scripts/whatsapp-bridge/*.test.mjs`. The classifier selects
the frontend lane for scripts/whatsapp-bridge/ and
plugins/platforms/whatsapp/, so a bridge-only or adapter-only PR runs it.
No E2E trigger.
The gateway inspection reported which halves a source ships but not the
commit it read, so for an unpinned Install from Git the reviewed tree and
the installed tree could differ if the source moved in between. Return
the inspected HEAD as `sha` (a catalog entry's is its reviewed pin) and
carry it through inspectAgentPlugin, so the install can be held to it the
way the native Desktop probe's will be. A cached answer keeps the sha it
read.
The component-detection rules now live in two inspectors (Python and
Electron). tests/fixtures/plugin_component_detection.json holds one set
of cases both run, registered as a shared fixture so editing it runs the
pytest and vitest consumers.
The Desktop hands `hermes update` the system proxy in both spellings
(HTTPS_PROXY and https_proxy). The .env load replaces only the spelling
the file uses, and urllib and curl prefer the lower-case one, so the
update still went to the system proxy and the .env NO_PROXY stopped
applying. After the .env load, `hermes update` now undoes everything the
Desktop added when .env sets a proxy (the Desktop injects nothing when it
sees one). A .env NO_PROXY alone replaces the user own entries in every
injected NO_PROXY variant and keeps the bypass the Desktop added; a `*`
there drops the injection.
Review S1 (kshitijk4poor) on #144894.
main_desktop.py (an update entry point) now imports it to name and close a
process holding release/ on a failed swap, so tests/ci/test_update_ci_routing.py
requires it in _UPDATE_DEPENDENCIES.
source_build imports hermes_cli/desktop_build_reuse.py on the update path, so
tests/ci/test_update_ci_routing.py requires it routed beside desktop_build_lock.py
(its desktop-reuse.mjs script is already covered by the scripts/build/ prefix).
update_channel now writes through hermes_cli.config_backend, so the CI
classifier lists it as an update dependency. The MoA transient-read test
patched hermes_cli.config.fast_safe_load, which the seam no longer calls;
it now patches utils.fast_safe_load, the parser the FileBackend uses.
Review round on #144647 (head d016f04694d):
- CI routing: the refresh (slot claim, bounded runner, stale check, detached spawn) moves to
hermes_cli/git_commit_graph.py, routed to e2e_upgrade with the update pipeline. The update path
imports it instead of all of worktree_ops; worktree_ops keeps its repack seams and imports the
shared slot claim and runner from it (no cycle).
- W1: the -w pass runs in whatever repo it was launched from, so its refresh sweeps only the
object-store locks (clear_stale_git_locks(names=OBJECT_STORE_LOCK_NAMES)); index.lock / HEAD.lock
there belong to the user's git. The install checkout's update sweep is unchanged.
- W2: timeout/exit reaping SIGTERMs the git tree first and SIGKILLs only after a 5 s grace
(kill_process_tree(grace=), default unchanged for every other caller), so git deletes
commit-graph-chain.lock and tmp_graph_*. The age-gated tmp sweep now also takes
objects/info/commit-graphs/tmp_graph_*.
- N1: a breakaway-forbidding Windows job gets the no-breakaway retry, and a spawn that fails gives
the 6 h slot back.
- N2: the detached child logs a failed write (git's fatal line) to the home's agent.log/errors.log.
- core.commitGraph=false in the target repo skips the write.
- N3: a successful no-op write stamps hermes-commit-graph.fresh, so an idle repo is not rewritten
every slot.
- N4: test_update_finish's completion child and every in-process hermes_cli test stub the
detached spawn; no writer outlives a test run.
- Greptile: the refresh test now runs on a treeless clone over a Bloom graph and asserts the
write adds no pack (red when --no-changed-paths is dropped).
hermes update rebuilt the TUI, the web UI and the desktop app but never apps/desktop/dist-webapp.
A gateway with webapp.enabled kept serving the bundle built before the update, which then talked
to the updated backend with an outdated contract.
The update now rebuilds that bundle when any home sharing this install has webapp.enabled, with
prepare_webapp_renderer, the function hermes webapp uses (a current bundle is reused). Like the
other products it is attempted on its own: a failure is reported with the rest at the end and
never skips another product.
Carried from BearHuddleston's #93508 onto main. The Webapp relay and the
profile-incarnation fence stay out: carried code follows main's
named-profile rule and refuses a missing or deleted profile home.
- Webapp routes: one-use launch tickets, window handoff, and gateway
WebSocket tickets minted for the signed-in browser
- Browser surface policy and the SPA document policy
- A host terminal for the browser that survives reconnects
- Staged browser uploads and file tickets; file.attach takes a staged upload
- hermes sessions adopt
- Webapp bundle build helpers and the renderer freshness check
- Regenerated gateway contracts
Co-authored-by: emozilla <emozilla@nousresearch.com>
post_update now imports hermes_cli/config_version_stamp.py (it converts legacy
AGENT_BROWSER_* settings before stamping), so a change to it must start the
e2e_upgrade lane like every other module the update path imports.
apps/desktop/e2e/core/reconnect-turn-state.spec.ts, real Electron + real
gateway, only the LLM scripted:
1. primary socket blocked mid-turn (loopback proxy that resets and refuses);
the turn finishes on the gateway while disconnected; after the redial the
reply renders exactly once (transcript oracle) and no Stop/spinner stays.
2. socket drop while a held turn runs, then a Control+Enter follow-up right
after the redial: both turns run in order, oracle holds, nothing spins.
3. gateway SIGKILL mid-turn: supervisor brings a new owner up, recovery
fences the turn `unknown`; the lost-turn card + Discard are on screen
with "1 Queued — paused"; a follow-up is queued ("2 Queued — paused")
and does not run ahead; Discard lets it run; oracle holds.
The classifier now routes the queue panel to the core lane, since the
lost-turn card lives there.
Refs #134889 (NF2)
Refs #140337
The round-1 health offset moved safe_url_for_log and the scoped-lock
release pair into siblings but re-exported them from gateway.status /
gateway.platforms.base, and status_scoped_locks read gateway.status
privates at call time while gateway.status re-exported the sibling's
privates (_unusable_lock_dir, _warned_unusable_lock_dirs). AGENTS.md:
no re-export shims for internal moves.
- gateway/status_lock_dir.py: the unusable-lock-dir report
(ScopedLockDirUnusable + public unusable_lock_dir / _recovered /
reset / reported). gateway.status imports it; it imports nothing back.
- gateway/status_scoped_locks.py: only the release pair, reading
gateway.status at call time; gateway.status no longer imports it.
- Every importer points at the real home: base.py, run_bootstrap.py,
the Feishu and Slack adapters, and the tests that patch them
(gateway.status_scoped_locks.release_*). safe_url_for_log comes from
gateway.platforms.base_log_safety; base.py imports it only for its
own use.
- classify_changes: status_lock_dir.py rides with gateway/status.py on
the e2e_upgrade lane.
code_health: 0 blocking (status.py stays under its main value).
Review finding by @unsupportedpastels on #140490 (minor 4) and by
@alt-glitch on #140490 (suggestion 4)
Refs #134889 (W2)
Refs #140337
hermes_cli/update_cmd_maint.py and update_host_obligation.py import the new
root module, so the update path runs it; list it beside its root siblings
(hermes_bootstrap.py, hermes_constants.py) in the update owners.
Refs #134889 (W8)
Refs #140337
A PR that changes only plugin-catalog entry YAML (plus contributor email
maps) ran all 23k Python tests on Linux, macOS and Windows, ~25 min, and
unrelated core flakes kept blocking one-file catalog additions.
The classifier now has a `catalog` lane. A diff made only of catalog entry
data turns `python` off and `catalog` on; ci.yaml then runs
catalog-tests.yml (every test file that reads the catalog or the email
maps, discovered by scripts/ci/catalog_test_files.py), plus the contributor
check. The lane is required by all-checks-pass whenever detect selects it.
plugin-catalog/README.md keeps the full suite (a docs-page test mirrors
it), and any non-catalog file in the diff brings the suite back. The
structural and pinned-source admission gates in plugin-catalog-ci.yml are
unchanged.
Review nits from #136602: the sshd and docker-backend image moves to the
current stable debian:13-slim (digest-pinned index), and the inline bash -c
setup becomes scripts/ci/test_sshd_container_setup.sh, mounted read-only and
run in the container. The new script joins the remote_backends path gate.
The SSH backend's real-host tests skipped on every CI run (nothing set
TERMINAL_SSH_HOST), and every Docker backend test mocked subprocess or the
docker CLI, so no CI run had executed either backend end to end.
- remote-backends.yml: starts a throwaway sshd container on 127.0.0.1 with a
per-run keypair (scripts/ci/start_test_sshd.sh) and runs
test_ssh_environment.py plus the new test_docker_backend_real.py against it
and the runner's Docker daemon with a digest-pinned debian:12-slim. A file
that skips fails the job, so the lane cannot go green over zero coverage.
- New classifier lane `remote_backends` (tools/environments/, tools/terminal_tool*,
the real tests, the sshd harness, run_tests.sh). ci.yaml calls the workflow
only when it fires, plus every push to main; required_results makes a selected
lane mandatory, like the update consumers.
- The real tests read HERMES_TEST_SSH_* / HERMES_TEST_DOCKER_IMAGE (forwarded
by run_tests.sh), because the hermetic conftest blanks TERMINAL_SSH_KEY as a
credential before any test can read it.
- New real SSH cases: exit code/stderr, cwd persistence with spaces vs per-call
workdir, quoting across the ssh hop, SendEnv passthrough through a real
AcceptEnv, file tools write/read on the remote.
- New real Docker cases: exec + exit codes, cwd vs workdir, exported env
persistence, docker_env/docker_forward_env reach the container and
nothing else does, quoting, file tools write/read, /root surviving a
container replacement.
- The existing one-shot test is xfail(strict): TERMINAL_SSH_PERSISTENT=false
is read into ssh_config["persistent"] but SSHEnvironment never consumes it,
so every SSH session persists exported env. Its _cleanup also cleaned the
raw task id, not the resolved env key, so one test's env leaked into the next.
tests/gateway/test_native_reply_crash_recovery.py (81edebdb07) drives the
real python-telegram-bot SDK and skips in the ordinary pytest slices, which
omit the messaging extra. test_messaging_gated_selection requires every such
file to be named by the e2e job's "Run messaging-gated gateway cases" step
and to start the e2e lane when edited; add it to both.
The round-7 downgrade guard (hermes_cli/update_downgrade_guard.py) is on the update path and reads
state.db through hermes_state_holders.read_only_db_uri, so an edit to it must start e2e_upgrade
(tests/ci/test_update_ci_routing.py).
8 conflicts, all from main's lint sweep (#135281 "Harder linter fixes" + the RUF series:
implicit-Optional, unused-unpacked-variable, next(...) for [...][0]). No other main commit touches
a conflicted file. Resolution: the branch side of each file plus main's exact autofix
(ruff 0.15.10, main's ruff.strict.toml, --fix --unsafe-fixes) on it. ruff.strict.toml takes main's
copy (the newly enabled rules). tests/cron/test_bot_chat_delivery_child_env.py stays deleted
(1cc9cb9199 retired its lane).
Pull requests and pushes to main no longer run any E2E suite: ci.yaml's detect gate
forces every e2e* lane off unless the run is a stable-release run (inputs.release) or a
manual dispatch, whatever the diff, the update-path classifier or the run-e2e label
selects. That covers the Python e2e and e2e-upgrade jobs, the Windows E2E and Windows
install + update journey in tests-os, and the Desktop core/update suites.
install-e2e.yml drops its pull_request (path-filtered `pr` route) and 12-hourly
schedule triggers; it runs from the stable-release gate (workflow_call) or by dispatch.
live-providers drops its nightly schedule and keeps the release-tag push + dispatch.
Why: a one-row change to left_core_migration.py held a PR for 40 minutes on the
Windows install + update E2E; the suites cost most of the larger-runner minutes and
the release gate already runs all of them strictly.
The ordinary `test` slices install no `messaging` extra, and the e2e job,
the only one that does, selects only tests/e2e. Four gateway cases probe for
the REAL Discord/Telegram distribution and skip without it, so they ran
nowhere: both tests in test_native_role_reauthorization.py,
test_telegram_fifo_unknown_and_current_authorization_survive_sigkill and
test_crash_mid_turn_then_platform_message_gets_one_pause_notice_and_reset_unpauses.
The e2e job gains a step that runs those three whole files (run_tests.sh drops
::node selectors) with -rs and fails when the summary reports any skip, so a
missing SDK can never read as green. The classifier starts the e2e lane when
one of them is edited. A CI test derives the gated set from the source (any
tests/gateway file probing PathFinder.find_spec for discord/telegram/slack)
so a new gated file must be added to the step.
Local run of the step: with the messaging venv 5 passed, 0 skipped (rc 0);
without it 1 passed, 3 skipped -> ::error:: and rc 1.
Review: https://github.com/NousResearch/hermes-agent/pull/106742#issuecomment-6067585232
Main (#133931) made source_build.py import hermes_cli/web_build_limits.py
without adding it to _UPDATE_DEPENDENCIES, so tests/ci/test_update_ci_routing.py
is red for e2e_upgrade and e2e_desktop_update on every merge ref that includes
it. Same one-line route main's fix/ci-routing-web-build-limits (6b5a089a00e)
adds.
#133931 made source_build.py import hermes_cli/web_build_limits.py but did not add it to
_UPDATE_DEPENDENCIES, so tests/ci/test_update_ci_routing.py has been red on main and on
every PR whose merge ref includes it: a change to the module would skip the
e2e_upgrade and e2e_desktop_update lanes that run the build it shapes.
The code-health split moved restore_cron_prompt_fields_if_degraded out of backup.py;
update_cmd_config imports it, so the update E2E suite must start when it changes
(tests/ci/test_update_ci_routing.py).
The pool-era host-backend-attach.ts is deleted on the branch; the attach-time version check
it carried now lives in local-gateway.ts (ensure compares code_sha with the checkout).
Importing a/b/c.py runs a/__init__.py and a/b/__init__.py first. The
update-lane routing listed hermes_cli/local_runtime/processes.py (the
v2026.9.24 updater imports it after the pull) but not the package init
that import runs. #134116 changed what that init loads, its PR never ran
e2e-upgrade, and main went red until #134242.
classify_changes now routes the package inits above every module the
e2e_upgrade and e2e_desktop_update lanes own, and above each shared hub,
because a hub's inits run whenever the hub is imported. An init that is
itself a hub stays out. The routing guard's import walker counts the
package inits a module's imports run, so the next unrouted one fails it.
This routes eight inits that were missing: local_runtime, gateway, cron,
tools, tools/environments, plugins, hermes_platform,
hermes_cli/observability.
Two ratchets this suite arms for its own siblings fired once they landed:
- the Windows pause (#132338) now imports gateway/status_inline_source.py
to recognise inline-bootstrapped gateways, so a change there starts the
e2e_upgrade suite like gateway/status.py (only two importers: not a hub)
- the marker-corpus consumers #132345 and #132354 ship are on main, so
_LANDS_IN_SIBLING_PR empties as its comment asks
`hermes_cli/_early_recovery_zip.py` (split out of `_early_recovery.py` on the
commit-point PR) is imported by the update path, so a change to it must start
the update pipeline and install E2E suites. Both lists and the install-e2e path
filter now match the `_early_recovery` prefix, which also routes any later
sibling. `remote-lifecycle-v2-marker.test.ts` reads the update-marker corpus
and is now a listed consumer; `remote-update-marker-programs.test.ts` exists in
this tree, so its sibling-PR allowance is gone.
Replaying the last 131 non-merge main commits (4787e4d56f) through the
classifier: main selects e2e_upgrade 5 / e2e_desktop_update 3, the CI branch
12 / 10. Four of the seven new hits per lane touched only plugins_cmd.py or
plugins_cmd_install.py. migrate_all_homes reaches them only when a home's
configured memory provider left core (the catalog install); no upgrade or
Desktop update journey configures one, so those suites cannot observe the
change, and the unit lane already runs on every .py edit. Measured on a
fresh home: memory_provider_migration, pm/plugins_state, agent/memory_provider
and plugins/memory run; plugins_cmd_install executes only its module body,
plugins_cmd is never imported.
Kept, because a fresh-home update runs them: memory_provider_migration.py
(migrate_all_homes on every source build), plugins/memory/__init__.py (the
post-update Honcho profile sync), pm/install.py (sealed() /
lazy_installs_allowed() gate every update's default-tool install), and
scripts/build/ (frontend-common.mjs is imported by freshness/web/tui/desktop).
After: 8 / 6. A guard test fails if the journey-home migration starts running
plugin-install code, the signal to route those modules again.
update_cmd_commit._admitted_python now resolves python3.N through hermes_platform.resolver
(cd471c9c55a), so the update path imports the resolver package and a change to it must start the
upgrade E2E, like hermes_platform.host already does.
What: scripts/ci/classify_changes.py names hermes_cli/git_pack_tidy.py in the update pipeline, so a
change to it runs e2e_upgrade.
Why: the derived routing guard (test_every_module_an_update_entry_point_imports_starts_its_suite)
found it imported by the update path and unrouted: main's partial-clone tidy (a0f47657b9) runs in
every update's pre-fetch via update_cmd_check.
- hermes_cli/web_routers/status.py (review CI2): /api/health `commit` is what
host-backend-attach.ts compares to the checkout before attaching after an
update, yet an edit ran only the python lane. Now also e2e_desktop_update,
the lane host-backend-attach already starts.
- Tauri updater (review CI3): marker.rs started e2e_upgrade and
e2e_desktop_update, neither of which builds or runs the bootstrap installer
(no workflow of either lane touches apps/bootstrap-installer or cargo),
while its callers update.rs/paths.rs started none. All three now route as
one unit to the lane that compiles and tests them: rust (cargo test, via
.rs) and nothing slow. _TAURI_UPDATE_MARKER is gone.
- remote-update-marker-programs.ts (+ test) (review CI3): the SSH remote's
marker judge/gate ran only frontend; it is the marker.sh/marker.ps1
contract run remotely, and its importers (remote-lifecycle.ts,
managed-ssh-update.ts) are desktop_updater files, so it is one too.
- gateway/status (review CI3): the prefix also matched status_phrases.py
(turn-status wording); the named status_inline_source sibling does not
exist. Tightened to gateway/status.py.
Changed assertion: _R6_CASES pinned marker.rs -> (rust, e2e_upgrade,
e2e_desktop_update); the finding is that the two e2e lanes are the bug (they
never build marker.rs), so it now pins (rust,), and a new test pins the
three Tauri updater files off every slow lane.
The stack's health refactors carved main_desktop_tcc.py out of main_desktop.py
and status_inline_source.py out of gateway/status.py; both are imported on the
update path, so an edit to either must start e2e_upgrade (and, for the desktop
module, e2e_desktop_update). Route each facade by prefix so its <stem>_*
siblings are covered without naming files this branch does not have yet.
The desktop gate's SSH-v2 marker fix added remote-update-marker-programs.test.ts,
which reads the shared marker corpus: list it as a consumer, allowed absent until
#132345 lands, like the other gate-PR consumer.
Route the exercised subprocess and provider-migration dependencies to both
real-update journeys. Keep backup routing under an explicit dispatch test.
Use the aggregate's existing detect outputs to reject skipped or missing
update consumers while retaining legitimate unselected PR skips.
Verified with the real classifier and aggregate subprocesses against
b9dec76a042, composite ff5c8f1a64b, and fixed code. Canonical runner:
571 passed, 0 failed, 1 native-Windows skip across CI and marker corpora.
(cherry picked from commit a082ca3c62ed49382f173a07882d4280d0ba7c9c)
(cherry picked from commit ed9f2028256b8b1d6af81ef97cd472f30d2cb042)
(cherry picked from commit 6d5cc740b6a75d476802222c2f4754855a5712b0)
tests/fixtures/update_marker_corpus.json is read by pytest (update_lock),
cargo (marker_tests.rs), vitest (update-marker-corpus.test.ts) and the
marker.sh / marker.ps1 corpus tests. By path it selected only python, so a
corpus edit never ran the Rust, Electron or Windows PowerShell readers.
_SHARED_FIXTURE_CONSUMERS routes a fixture with its consumers' test lanes;
the routing test derives the consumers from the tree and replays the
classifier through the workflow files to each lane's job (review D14).
(cherry picked from commit 4893ef31833b5fcbdfc527beb70724ea63a3eb8f)
(cherry picked from commit 20c04a27b2a04ebaabb9a9df3693339243d9380e)
main added hermes_cli/left_core_migration.py, imported by source_build.py; the
R15 import-graph test caught it unrouted on the composite of all seven PRs.
(cherry picked from commit 338aabcdda316a92b8bfcae10f52730fd6fcb344)
(cherry picked from commit 5a839bb475d08f82293f07bcdb60ded859814476)
The classifier missed modules the update entry points import
(_subprocess_compat, desktop_build_lock, memory_provider_migration, the
scripts/build compilers) and never sent source_build/source_completion to
the Desktop update suite. ci.yaml gated tests-os on `python` alone, so a
TypeScript-only change to the Electron marker/result readers set
desktop_updater and still never reached the Windows hand-off tests.
- classify_changes.py: _UPDATE_ENTRY_POINTS / _DESKTOP_BUILD_ENTRY_POINTS,
an owner list of what they import, and an explicit _SHARED_HUBS set.
- tests/ci/test_update_ci_routing.py reads the entry points' real imports
(AST incl. lazy imports; ESM import/step() closure of the build scripts)
and fails until each is routed or is a real hub (>= 25 importers).
- It also replays the REAL classifier's output through the composite
action, ci.yaml detect outputs, job if:/with:, the called workflows' job
if: and the rendered Windows step, for every combination of the gated
lanes (tests/ci/_gha_expr.py models the expression subset and raises on
anything else).
- ci.yaml: tests runs for python|e2e|e2e_upgrade; tests-os for
python|desktop_updater|e2e|e2e_upgrade.
Red on 8cc78279a36: 30 of the new routing tests fail (R15 table cells,
16 lane combinations, ownership/closure tests).
(cherry picked from commit 3b17b5d2ad8081534bbeefcf5d8334cd79a7e6fd)
(cherry picked from commit 594f8b0c4ba32324da922d5289b0932299ffe7a9)
The e2e_upgrade lane (Linux e2e-upgrade + the Windows install + update
journey) only matched the update_* module family, so a PR touching
cmd_update in hermes_cli/main.py, _early_recovery.py, venv_sync.py,
relaunch.py, the gateway restart/pause surface, hermes_bootstrap.py or
the Desktop hand-off scripts merged without a real `hermes update`. The
e2e_desktop_update / desktop_updater lanes likewise missed main.ts, the
hand-off result reader, the install-kind / attach modules,
desktop_update_verify.py and main_desktop.py.
The added paths are the steps of the update pipeline (updater audit
code maps, step tables) plus the lock / marker / recovery state the
update_* modules import. Two invariant tests pin every such file to its
lane and fail when one is renamed.
(cherry picked from commit b02e2cc124e74033ccb4c6e1c7c939590ef65fac)
(cherry picked from commit 16068789ea98ce55e92eeb148ec9345d6778e781)
Every core spec drives open, resume and switch, yet changes under
apps/desktop/src/app/session/ skipped the suite on the PR; #132017
changed resume that way and main went red on remote-secondary.
A layout matrix (single/fleet × strip/condensed × local/remote active,
starting from a named profile or the all view) asserts the active
gateway's default is one click away in each. The four fleet+condensed
cases fail on main.
The core E2E lane only starts on PRs touching its guarded paths, so a
profile-rail edit would have met fleet-condensed-default.spec.ts only on
main. Add the rail sources to the lane.
Path-based review (workflows, composite actions, eslint/prettier config,
the MCP catalog, install-hook files) moves to .github/CODEOWNERS. GitHub
requests and enforces that review, so CI no longer classifies those paths,
checks a label, or reruns failed jobs when the label is added.
Removes review-labels.yml, emit_review_status.py, the rerun-review-labels
job, and the ci_review / ci_review_files / mcp_catalog classifier outputs.
The run-e2e rerun job in label-rerun.yml stays.
The supply-chain scan keeps its content patterns (base64 decode + exec,
obfuscated subprocess), which CODEOWNERS cannot express. It fails on its
own again, as it did before #68833 routed it through the label gate. Its
.pth and install-hook path checks are dropped in favour of CODEOWNERS.
Submitters only saw a stale 5-point summary on the catalog page (it even listed
a 'Released' requirement nothing enforces), while reviewers applied ten README
rules plus criteria written nowhere public (credentials, approvals, disclosure,
compatibility metadata, forks of bundled plugins).
- plugin-catalog/README.md: rules 11-15 write down those review criteria; the
rule list sits between admission-rules markers.
- New developer-guide page 'Submitting to the plugin catalog': before you
submit, the PR flow, the full rule block (verbatim mirror), pin updates,
delisting vs removal, asking for a hook.
- The /plugins 'Submit' links and the user-guide section point at it.
- tests/website/test_catalog_rules_mirror.py fails when the two rule blocks
differ; classify_changes makes a README-only edit run it.
- AGENTS.md, plugins/AGENTS.md and CONTRIBUTING.md reference both files.