Commit Graph

176 Commits

Author SHA1 Message Date
alt-glitch
cbffbeecf4 ci: package __init__ files on the update path start the update E2Es
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.
2026-10-06 20:30:06 -04:00
Teknium
0d69d07bb2 test(ci): an import-time class body is not the update running plugin-install code
The journey-home guard counted every non-<module> frame in plugins_cmd*.py as
the migration running plugin-install code. d6813a5dd7 gave
plugins_cmd_install.py its first class statement (_ConsentRefusal), whose body
runs at import as a frame named after the class, so a plain import (via
plugins_cmd.py's module-level import) read as a call and the guard failed with
no change in what the update does: migrate_all_homes() still returns [] and
calls nothing in the module.

Frames called by their own file's <module> frame (class statements,
module-level comprehensions) are import-time execution and are skipped; a real
call into an install function is still recorded.
2026-10-06 13:02:14 -07:00
Teknium
b5a5b9b086 ci(update): route the inline-source parser and retire the landed siblings' allowance
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
2026-10-06 13:02:14 -07:00
Teknium
c52a900f13 test: the CI replay puts git on the step's PATH on native Windows too
The replayed steps call git. On Linux os.defpath reaches it; on Windows the
replay's PATH only reached it through the located bash's directory, and Git
for Windows ships a bash.exe in usr\bin with no git.exe beside it. The
Windows arm64 lane resolved that bash and 13 replay cases failed with
"git: command not found" (1e547a814c3 had just put them on the native
Windows lanes). git's own directory now rides along like bash's.
2026-10-06 13:02:14 -07:00
Teknium
9f8d7bf604 ci: route the ZIP recovery module and the SSH gate's corpus test like their siblings
`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.
2026-10-06 13:02:14 -07:00
Teknium
fdae2fc175 fix: plugin-install modules no longer start the update journeys they never reach
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.
2026-10-06 13:02:14 -07:00
Teknium
50bff6b151 fix: the crash-cell oracle judges the marker with update_lock itself
test_crash_cells carried its own marker parser and identity rule, and it
decides the orphaned_update verdict. It disagreed with the product on three
inputs: a float started_at (product: malformed, copy: live), a create time
1.5 s off (product: live within its 2.0 s tolerance, copy: dead at 1.0 s) and
a run: line before delegate: (product: delegate live, copy: dead, it read
line 4 only). The self-check claimed parity but never called update_lock and
used the test's own pid, which the product judges by exact incarnation.

_marker_live now asks update_lock._parse_marker(...).owner_live() /
delegate_live(); the crash cells run in a pytest process on the same Windows
host as the marker's pids, so the product's process table reads them as the
updater does. The self-check marks a separate live process with an integer
started_at, compares the oracle to judge_marker, and adds the three rows.
The ORPHAN_RELEASE_GRACE poll is unchanged.
2026-10-06 13:02:14 -07:00
Teknium
c63f9bff4e fix: the native Windows lane runs the CI replay and its dispatch controls
tests-os[windows] imports only the files list_os_marked_tests.py emits and
keeps `-m "platforms and not integration"` tests its host admits. The replay
regression for the Windows interpreter layout (venv holding python.exe alone,
Git Bash) carried no platforms marker, so the Windows selector omitted the
whole file and green Windows jobs never ran it.

The replay test, the R15 dispatch chain and the disabled/advisory/no-command/
empty-selection controls now carry platforms("linux", "windows"): Linux keeps
its coverage, the Windows selector lists the file (194 -> 195 files) and the
Windows -m filter collects 35 of its nodeids. macOS is left out on purpose:
the replay has not been run on bash 3.2. A routing test pins both gates via
the real selector and its spec semantics.
2026-10-06 13:02:14 -07:00
Teknium
855a4b21aa fix: the CI replay starts python3/python and coreutils on native Windows
workflow_steps replaced PATH with the interpreter dir plus os.defpath. A
Windows venv ships only python.exe and os.defpath carries no Git usr/bin,
so replaying the ubuntu `gate-lanes` step (`python3 - <<PY`) died with
`python3: command not found` and the selection steps lost sort/tr/paste/find;
on Linux a missing python3 silently fell through to the host interpreter.

The replay now resolves bash via hermes_platform.resolver, adds that bash's
dir (Git usr/bin on Windows, already on defpath on Linux) after os.defpath,
and drops a disposable python/python3 shim to REPLAY_PYTHON only for a name
the interpreter dir lacks, mirroring setup-python putting both on PATH.
Production YAML is unchanged; on a Linux venv no shim is written.
2026-10-06 13:02:14 -07:00
Teknium
448add284a ci: the Windows update E2E fails when the crash journey ran no cells
run_tests.sh runs one pytest per file and reports a file whose tests were
all deselected (pytest exit 5) or all skipped as passed. 8ce4ce35fc2 made
the -m selection and platforms('windows') marker a tests/ci check, but any
other zero-execution path (the HERMES_E2E_WINDOWS_INSTALL opt-in lost from
the step env, a new skip condition) still ran zero crash cells green.

The crash journey now writes crash-cells-ran.txt (each journey step and
ok/failed) into HERMES_E2E_ARTIFACTS once it has run, and a new required
step "Every crash cell ran" after the run step throws when the manifest
is missing. The manifest is uploaded with the other artifacts.

Test (tests/ci, every PR): the step exists after the run step, is neither
disabled nor advisory (workflow_steps.required), reads the same
HERMES_E2E_ARTIFACTS and names the module's manifest constant; and
_record_cells_ran writes exactly the steps that ran, nothing without the
env. Red on the start workflow (no step) and with the step made
continue-on-error.

Review 5411223855 (F52: collected > 0 / executed-cells assertion).
2026-10-06 13:02:14 -07:00
Teknium
9cf530c405 test(e2e): a crash cell that breaks the shared machine no longer takes the later cells down
All six crash cells share one module-scoped machine. A cell whose
follow-up update failed (torn checkout, .hermes-update-in-progress or
.git/index.lock left, or a step that raised) handed that state to every
later cell, which then reported the first cell's breakage as its own.

_run_cells now runs the cells in order and, before each one, checks what
the previous cell left: the checkout exactly its target, no marker, no git
lock, follow-up rc 0. When it is not, the harness stops the machine's
processes, drops the lock and the marker and resets the checkout with the
harness's git to that cell's target (else to the HEAD it left), and notes
the restore and its reason in machine.timings, so the evidence of any
later failure names the culprit. The broken cell keeps its own verdict:
its result was captured before the restore. If the harness cannot reset,
the next cell fails "not run: <cell> left the shared machine unsound
(...)" instead of running on it. This is harness plumbing like the stale
index.lock clean-up _crash already did; every target only appends
statements to modules, so the venv still runs the restored tree.

Tests: a broken cell, a raising cell and a sound cell in sequence (each
later cell sees the reset checkout, no marker, no lock, and the timings
name the culprit), and an unrestorable machine failing the next cell
with the culprit's name. Both fail on the start sha (no _run_cells).

Review 5411223855 (crash-cell cascade).
2026-10-06 13:02:14 -07:00
Teknium
9341068b63 test(e2e): the mid_git hold verdict counts looks, not only seconds
The mid_git cell names an unmodelled git (the hold filter ran, but no
merge/reset/checkout with .git/index.lock appeared) after 30 s of wall
time. While the filter holds, git, its op and index.lock cannot move: the
state is static and the poll only has to observe it, but on a starved
runner a tree walk can take seconds, so 30 s could be a handful of looks
and a slow runner could false-fail the cell. The verdict now also needs
HOLD_SETTLE_LOOKS (60) complete looks; a normal runner passes 60 looks
well inside 30 s, so its behaviour is unchanged, and the wait is still
bounded by _kill_when's UPDATE_TIMEOUT.

Test drives _GitHold.point with a 10 s-per-look clock: the start code
raised on the 4th look (40 s), now it raises only after 60 looks.

Review 5411223855 (crash-cell timing).
2026-10-06 13:02:14 -07:00
Teknium
8c4571f8b4 fix(update): a crash cell counts the kill when taskkill reports the update itself terminated (CI5)
What: _kill_when judges the kill by `_kill_delivered`: taskkill rc 0, or its stdout carrying the
update root's own SUCCESS line ("with PID <pid> (child process of ...").
Why: the update's git now runs inside the update's kill-on-close job. Killing the update closes the
job, a member dies while `taskkill /T /F` is still walking the tree, and taskkill exits 128 even
though it terminated the update (rc 1 is the code /F forces). The mid-fetch and tree-moved cells
then reported "the update was not running when killed" on the strict composite run (artifact
receipt: outcome interrupted, transcript stops at the fetch). The mid-git cells passed only because
their git is frozen by a hold. "Not found" for the root stays a refusal.
Red: test_kill_point_counts_only_an_update_killed_while_running[None-128-...SUCCESS...-True-True]
fails on the old rc-only check. Green: tests/ci + tests/e2e/core/windows_update 546 passed.
2026-10-06 13:02:14 -07:00
Teknium
ae91994515 fix(update): CI routes each update-path file to the suites that exercise it
- 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.
2026-10-06 13:02:14 -07:00
Teknium
d6e201432b fix(update): the orphan-marker known failure expires once its fix is in the tree
ORPHAN_MARKER_GAP excused the orphaned_update cell's marker assertion with no
expiry. PR CI never runs that cell (Windows, opt-in) nor strict acceptance,
so after #132354 (hand-off script names its update child as the line-4
delegate) and #132365 (the judge honours a live delegate) land, a regression
of exactly that contract would still pass as an expected failure.

Now the fix's footprint is read from the tree (ORPHAN_MARKER_FIX):
- while any half is absent, _orphan_gap_excuse applies known_failure as before;
- once every half is present the excuse is off, so the cell is strict on any
  runner whatever the wrapper says;
- tests/ci/test_windows_update_crash_oracles.py (runs on every PR) fails with
  "delete ORPHAN_MARKER_GAP ..." once the fix is in the tree, the same expiry
  shape as _LANDS_IN_SIBLING_PR, and fails if the Python half already on this
  branch (update_lock.delegate_live) loses its footprint, so a rename cannot
  keep the excuse alive.
Checked against the scripts branch: this tree + its marker.ps1/windows.ps1
reads no half missing.

Review CI1.
2026-10-06 13:02:14 -07:00
Teknium
d153acfe85 fix(ci): route the update path's new sibling modules and the SSH marker corpus reader
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.
2026-10-06 13:02:14 -07:00
Teknium
9c2f73d688 fix(update): a Windows update E2E test the workflow would deselect fails CI (review F52)
What: a tests/ci guard evaluates the workflow's own -m expression (windows-install-update-e2e.yml) against every test in tests/e2e/core/windows_update/test_*.py and requires platforms('windows').
Why: run_tests.sh reports a fully deselected file as passed with 0 tests, so dropping the crash file's platforms marker would silently run zero crash cells.
Red: mutant test_crash_cells.py without platforms('windows') -> [test_crash_cells.py] FAILED 'the workflow deselects it' (19 passed, 1 failed).
Green: 20 passed, 0 failed.
2026-10-06 13:02:14 -07:00
Teknium
7cfa9b6243 fix(update): the orphan cell accepts exit 120 only with the run's own success receipt (review F72)
What: _orphan baselines <HERMES_HOME>/logs/update_receipts before the hand-off starts; the cell passes on rc 0, or on rc 120 only when the newest receipt written since says outcome success.
Why: CPython replaces ANY exit status with 120 when the final flush fails, so a failure exit after the success line read as a completed update.
Red: pre-fix cell -> [120-failed] and [120-None] accepted; receipt helper missing (9 passed, 3 failed).
Green: tests/ci/test_windows_update_crash_oracles.py 12 passed, 0 failed.
2026-10-06 13:02:14 -07:00
Teknium
46efbcbe28 fix(update): the crash cells' marker oracle applies update_lock's v1 age ceiling (review F51)
What: the test's _identity_live takes the marker age and, for an identity without a creation time, is live only within hermes_cli.update_lock.UPDATE_MARKER_MAX_AGE_SECONDS.
Why: the oracle read a ct-less (v1) marker for a live pid as LIVE past the product's 20-minute ceiling, so it could disagree with the verdict update_lock gives every other updater.
Red: pre-fix test_crash_cells.py -> [1260-False-False] FAILED (6 passed, 1 failed).
Green: 7 passed, 0 failed.
2026-10-06 13:02:14 -07:00
Teknium
4da5a08c84 fix(update): a crash cell counts its kill only when taskkill found the update running (review F14)
What: _kill_when requires proc.poll() is None after the point is seen and taskkill_tree's rc 0 (it now returns the CompletedProcess); otherwise a harness AssertionError naming the point.
Why: tree_moved's point (HEAD at target) persists after a clean exit, so the cell could pass on an update that was never interrupted.
Red: pre-fix _kill_when/_machine -> exited-0 + truthy point and taskkill rc 128 cases accepted (2 passed, 2 failed).
Green: tests/ci/test_windows_update_crash_oracles.py 4 passed, 0 failed.
2026-10-06 13:02:14 -07:00
Teknium
1e2026f5d9 fix(update): a sibling-PR corpus allowance expires once its consumer lands (review F56)
What: the shared-fixture guard fails when a _LANDS_IN_SIBLING_PR entry's file exists ('sibling landed: delete its allowance').
Why: the allowance never expired, so after #132345/#132354 merge a later deletion of a listed corpus consumer stayed green.
Red: guard without the check -> test_sibling_allowance_expires_once_its_consumer_is_in_the_tree DID NOT RAISE (5 passed, 1 failed).
Green: 6 passed, 0 failed (-k 'sibling_allowance or shared_fixture').
2026-10-06 13:02:14 -07:00
Teknium
ef2f37fa57 test(ci): admit only named sibling-PR corpus consumers while they are pending
The strict corpus guard requires every listed consumer of the update-marker
corpus to exist. On this stacked PR the Electron reader lands in #132345 and
the two hand-off script readers in #132354, which merge before this PR, so
the guard went red here although the composite has all five.

Absence is now admitted only for consumers named in _LANDS_IN_SIBLING_PR
with the PR that lands them. A missing consumer no sibling lands, a fixture
whose every listed consumer is absent, and an allowance entry that names no
listed consumer all still fail. Once the siblings are on main the table can be
emptied and the full strict check applies unchanged.

(cherry picked from commit cfdfa6beeb4a1d8f17eb0ef7b078547b3c39cd5d)
(cherry picked from commit 3fb57d4a4e8805da89338ac0f469924171bcc0d1)
2026-10-06 13:02:14 -07:00
Teknium
259fe73b0b fix(ci): route update dependencies and reject selected consumer skips
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)
2026-10-06 13:02:14 -07:00
Teknium
03a8449cd7 test: make update CI replay reject ineffective consumers
Execute gate-lanes and safe selection scripts instead of assuming copied outputs or counting reachable jobs. Check required step conditions, advisory flags, matrix cells, real Windows file enumeration and corpus graph existence. Keep pytest and Playwright at argv-receipt boundaries; no native suites are launched.

(cherry picked from commit d4da1a0266bf2a1ad12309fda35d24302b6b7106)
(cherry picked from commit a295255e8901c57d0c8a768c7b65677553e4f433)
(cherry picked from commit 4fcb270f842c26f05420c2bcfd2d8b69a2f42fa9)
2026-10-06 13:02:14 -07:00
Teknium
fb5c7e2781 fix(ci): enforce strict acceptance for delivery gaps
(cherry picked from commit 8c22ecbd465f80771eeb6d87ddea2be21e06ad55)
(cherry picked from commit a5ff463ce85b2b372661856525aff27d230f9226)
(cherry picked from commit 9f7a4d177929ca32cf1d25afd32dbb5005338db0)
2026-10-06 13:02:14 -07:00
Teknium
f469749922 ci: route the remaining update-path files to every suite that exercises them (m11)
- apps/desktop/electron/update-marker-gate.ts -> desktop_updater (the gate's
  live-marker probe is the script's marker contract); e2e_desktop_update as before
- apps/desktop/electron/remote-lifecycle.ts -> desktop_updater besides frontend
- apps/bootstrap-installer/src-tauri/src/marker.rs -> e2e_upgrade and
  e2e_desktop_update besides rust (the Tauri claim honours the held-checkout rule)
- hermes_cli/_launchers.py -> e2e_desktop_update besides e2e_upgrade

(cherry picked from commit b9dec76a042c71d29ded5dda35090a382645d9d3)
(cherry picked from commit 9dfbb21f5c03f56ffd0c7790995585641861d6d3)
2026-10-06 13:02:14 -07:00
Teknium
38345d0eae test(ci): a listed corpus consumer may land in a sibling PR
The shared-fixture test also failed when a listed consumer was absent while
the fixture was present. The batch lands them in separate PRs: this branch,
stacked on the commit-point lane, has the corpus and the pytest/cargo
readers but not the Electron or hand-off readers, so the PR's own Python
lane went red. Routing a path that is not in the tree yet costs nothing;
the check that loses coverage (a reader in the tree the table omits) stays.

(cherry picked from commit 8d387483ccc8a20cb82ff4b60a97c747184aab9c)
(cherry picked from commit 24518bc30e8d67af6e165f1a59d28a5f6a2978bf)
2026-10-06 13:02:14 -07:00
Teknium
2d7307931d ci: a shared fixture selects every lane that runs one of its consumers
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)
2026-10-06 13:02:14 -07:00
Teknium
aa71979a9a test(e2e): strict acceptance note names the owners it was run with
(cherry picked from commit 0ad1048351518e3a453e4f5e20e0fb9b74e54e21)
(cherry picked from commit ef09b3c62de0404d389f5e8f53bf9bf024acb653)
2026-10-06 13:02:14 -07:00
Teknium
53847b7d41 test(e2e): strict acceptance mode makes a batch's known gaps hard failures
known_failure XFAILs a gap while its fix is an open PR, which is right
on a PR and wrong on the acceptance run of the integrated batch: there
it accepts exactly the gap the batch claims to close (the final
composite's Windows run passed with `test_crash_cells.py (3✓ 1xf)`).

HERMES_E2E_STRICT_ACCEPTANCE=<owner>[,<owner>] (or 1 = every gap) makes
the matching known_failure / known_gate blocks re-raise the gap's own
assertion. It is set only from the `strict_acceptance` dispatch input of
ci.yaml (threaded through tests.yml / tests-os.yml to every E2E step)
and of windows-install-update-e2e.yml; pull requests never set it.
run_tests.sh forwards it through its env -i allowlist.

Red on 8cc78279a36: test_pending_fixes_strict.py (a real pytest process
per mode) — the batch-owned gap XFAILs under strict mode.

(cherry picked from commit ca1e7e749a61263181e89aa687dec366d95fda18)
(cherry picked from commit 7768bf3ef8e15fb7f228ebad619d6a9a85ad4100)
2026-10-06 13:02:14 -07:00
Teknium
5a36d5ac34 ci: route update owners by the import graph and dispatch every set lane
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)
2026-10-06 13:02:14 -07:00
Teknium
008479c7ea ci: make the Desktop update E2E required and drop a dead wine2e nodeid
e2e-desktop-update ran with continue-on-error, so a PR touching the
Desktop update path could merge on a red "Update now" hand-off. It has
passed every main-push shard on record (48/48 over the last 12 main CI
runs), so it now gates the merge whenever the e2e_desktop_update lane
fires, like the Linux e2e-upgrade shards and the Windows install +
update journey already do for e2e_upgrade (skipped = success otherwise).

windows-venv-e2e.yml listed
tests/hermes_cli/test_runtime_repair.py::TestWindowsRuntimeSelfLock, a
file that never existed on main; run_tests.sh dropped it silently. The
in-use-runtime replacement it meant to prove now lives in pm and is
tests/pm/test_held_entry_windows_live.py.

Tests pin both: every real-update suite reaches all-checks-pass, is
gated on its lane and is never continue-on-error; every path the
wine2e lane runs is an existing test file.

(cherry picked from commit f4791428341afaf67cc53775653dde197591fa4f)
(cherry picked from commit 5d3a4b505fe5c3858a6bb7f3554c97eb272231ca)
2026-10-06 13:02:14 -07:00
Teknium
424d52c524 ci: start the real-update suites for every file on the update path
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)
2026-10-06 13:02:14 -07:00
alt-glitch
6bcdd6ceea chore(release): retire HERMES_SKIP_INTRO 2026-10-07 01:00:00 +05:30
Teknium
3f524a2459 ci: version the required check so CI greens from before a new required job stop counting
The protect-main ruleset requires a check by NAME, and a green check run on a
PR head never expires. After the code-health ratchet joined the required set,
32 PRs whose last CI run predated it merged on their old "All required checks
pass" green without the ratchet ever running; 20 of them would have been
blocked (62 findings, incl. 15 new blind `except Exception`).

Renaming the gate to "All required checks pass (v2)" and pointing the ruleset
at the new name voids every older green: each open PR needs one fresh CI run,
which now includes the ratchet, before it can merge, whoever merges it.
The ci.yaml comment documents the bump as the standard step whenever the
required set gains a job every PR must pass.

live_comment.py keeps both names in its infra list (old runs still render),
and a test pins that the gate's current display name never shows as a job.
2026-10-06 03:57:34 -07:00
Teknium
519a13aaa6 ci: Desktop session open/resume changes run the core E2E
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.
2026-10-05 23:36:55 -07:00
Brooklyn Nicholson
158fd638da test(desktop): every rail layout keeps a door to the active default; rail changes run the core E2E
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.
2026-10-03 17:17:25 -05:00
ethernet
1af98f58ba Merge pull request #131726 from NousResearch/ethie/codeowners-ci-review
ci: replace the ci-reviewed label gate with CODEOWNERS
2026-10-02 18:49:46 -04:00
ethernet
059efe8b29 ci: replace the ci-reviewed label gate with CODEOWNERS
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.
2026-10-02 15:22:49 -04:00
ethernet
9a746d407b test(ci): drop desktop bundle smoke tests that only read the workflow
Five tests loaded desktop-bundled-release.yml and asserted on its shape or
evaluated its `if:` expressions with a mini evaluator. They restate the YAML,
so they fail on correct edits and pass on subtly wrong ones. The tests that
execute the workflow's shell steps against a fake R2 stay.
2026-10-02 15:10:43 -04:00
teknium1
b12e672e23 test: consolidate the catalog-gate confinement tests to behavioural cases
Every case executes the workflow's real run: blocks against fixture repos.
One parametrized arm per closed bypass (traversal and symlinked subdirs, a
symlink hidden behind an in-clone link, tag-object and trailing-newline
shas, log forging via repo or filename, a symlinked entry file, the spaced
self-updater path, parser import shadowing), one multi-entry case for
stdin drain and parser-crash inheritance, and the legit flows that must
keep passing (subdir entry, repo root, null subdir, in-clone symlink, sha
bump, contributor email map beside an entry, README/docs/CI-only PR).

The changed-entries fixture is a real merge commit with origin/main at
its first parent, so the same cases run against main's merge-base step:
CATALOG_CI_WORKFLOW=<main copy> turns all 14 bypass/guard arms red while
every legit arm stays green. Dropped the two tests that only read the
workflow YAML shape, and folded the validator additions into one
parametrized field test.
2026-10-01 10:46:45 -07:00
beardthelion
7e52bd0fa9 fix(ci): close remaining pin-gate escapes found in review
- Scan the whole clone for escaping symlinks, not just PLUGIN_DIR:
  find does not descend into a symlinked directory, so an allowed
  in-clone link (plugin/ext -> ../lib) hid a nested escape.
- Require catalog admission PRs to be data-only in both jobs; the
  gate's enforcement code resolves from the PR checkout, so a diff
  that also touches tooling could neuter the gate vetting it.
- persist-credentials: false on both checkouts: `hermes plugins
  validate` runs pinned-repo code and must not see the job token.
- Broaden the self-updater patterns: releases/download and codeload
  fetches, and the Node fs write sinks (writeFileSync, renameSync,
  copyFileSync, cpSync, createWriteStream).
2026-10-01 10:46:45 -07:00
teknium1
eb9cb6c8e1 fix(ci): make the -I shadow guard load-bearing; widen clone timeout to 600s
Follow-up to the admission-gate confinement:

- tests/ci: test_parser_imports_cannot_be_shadowed_by_checkout_files planted
  `re.py`, but `re` is imported during interpreter startup, so a cwd copy never
  shadows it and the test passed even with `-I` stripped from the workflow.
  Plant `shlex.py` instead (the parser is its first importer): with a shadow
  whose quote() swallows the rejection, the gate returns rc 0 / PASS without
  `-I` and rc 1 with it, so the test now actually guards the fix.
- workflow: `timeout 120 git clone` -> 600. The clone is deliberately full
  (the pin may not be the tip) and listed repos exceed 700 MB
  (hermes-jackal-verified 1.43 GB, hindsight 741 MB); 120s would false-fail a
  legitimate re-pin on a slow runner while 600s still bounds a hostile URL
  well under the 30-minute job cap.

Attribution: beardthelion@users.noreply.github.com is already in the legacy
AUTHOR_MAP, so no mapping file is needed.
2026-10-01 10:46:45 -07:00
beardthelion
8444e9eb18 fix(ci): confine plugin-catalog admission gate to the pinned clone
The pinned-source job joined a PR-controlled subdir onto the clone root
with no validation, accepted file:// repos and ref names as the "pin",
and followed committed symlinks out of the clone, so the gate could
validate attacker-planted files instead of the declared commit's
contents. Harden the step end to end:

- validate repo (https://, whitespace-free), sha (40-hex, commit object),
  and subdir (plain relative path) in the parser before any use
- run the parser under python3 -I against installed ruamel.yaml so the
  PR checkout cannot shadow its imports, capture parser output, and
  reset shell variables per entry so a dead parser cannot replay the
  previous entry's fields
- confine PLUGIN_DIR and every symlink beneath it to the clone root
- include renamed and typechanged entry files in the changed set
- feed entries over fd 3 and keep plugin-executed commands off stdin so
  validate cannot drain the remaining list
- NUL-delimit both self-updater grep stages and sanitize the echoed
  filename list so crafted names cannot forge workflow commands
- mirror the same repo/subdir rules in the runtime catalog loader and
  the site extractor, and use \Z anchors so a trailing newline cannot
  split the verdict between gates
2026-10-01 10:46:45 -07:00
emozilla
5c08ad68f7 ci: run the slow lanes on a PR only when it touches their paths
The E2E suites, the Windows E2E jobs, the Docker image build and the Nix
flake check took about 120 of the ~140 larger-runner minutes each PR push
spent, and fired on nearly every PR. The 32-core pool backed up behind
them.

On a pull request each slow lane now runs only when the diff touches the
suite, its harness, or the code it guards (classify_changes.py
_E2E_LANES, _DOCKER_PATHS, _NIX_LANE_PATHS), or when the PR carries the
run-e2e label. Every push to main still runs all of them, and a release
run still forces every lane on.

Adding run-e2e reruns the head commit's CI, Docker and Nix runs; detect
reads the live labels because a rerun replays the original event.

Replayed over the last 200 merged PRs, the modelled 32-core cost of one
push drops from about 120 to about 10 runner-minutes.
2026-09-30 14:37:52 -04:00
ethernet8023
792ba0a264 fix(release): read a tag's record, not its signature
git keeps an annotated tag's signature inside the message body, so
`git tag -l --format=%(contents)` returns the JSON record followed by an
armored block, and `json.loads` of the whole message raises "Extra data: line 2
column 1" at the armor's first byte. Claim tags, final receipts and build
receipts are created on a host whose git signs tags, so stable admission
(`python -m scripts.releases.stable admit`), ordered reconciliation
(`python -m scripts.releases.sequencer`) and tagless build receipts each
refused to read their own tag.

Add `versioning.tag_record()` as the one reader of a tag's record — the text
before the armor, unchanged for an unsigned tag or a detached signature — and
route every `%(contents)` read through it in stable.py, sequencer.py and
commit_build.py. commit_build imports it inside the function so its module
surface stays stdlib-only for the isolated checkout-admission step.

The reads inside the release tests use the same reader, and the one fixture
that creates a plain final tag says so explicitly, so the release suite passes
on a host whose git signs tags by default.
2026-09-29 14:55:15 -04:00
Mark S.
ca705dbf7e fix(ci): card-tool contract test reads the as-const list and runs on desktop edits (#128010)
#127828 changed CARD_TOOL_NAMES in apps/desktop/src/lib/tool-render-class.ts
from `new Set([...])` to `[...] as const`. The pytest that pins that list
against the gateway's _TOOL_LIFECYCLE_UI_TOOLS located it with a regex that
only matched `new Set([`, so it has failed on main since with
"CARD_TOOL_NAMES not found in tool-render-class.ts". The gateway set still
covers every desktop card name; only the test's parser was stale.

The PR merged green because it touched only apps/, which skips the Python
lane, so the one test that reads that file never ran on it.

- The parser accepts both declaration forms and fails on an empty list
  instead of passing a vacuous subset check.
- tool-render-class.ts joins _PY_RELEVANT_CONTRACT_FILES so an edit to it
  runs the Python lane, with a classifier case for the entry.
2026-09-29 12:27:26 -04:00
ethernet
b5d583c4ac feat(release): start the gates and the signed candidates together
Every gate and every candidate now needs only admit, so the signed macOS
and Windows builds and the docker image stop waiting behind the full CI
run. acceptance stays the one join: it still needs ci, docker and all six
candidates, so what can be promoted is unchanged.

The four gates that skipped under --skip-tests only because ci skipped
(nix, termux-checks, windows-live, install-e2e) and pm-bundle needed their
own condition. SKIPPED_BY requires the gate to observe 'skipped', so
dropping the edge alone would have left them running and blocked the
release instead of failing it.
2026-09-25 12:50:53 -04:00
ethernet
2ab7d9bfe3 chore(ci): remove publish-e2e-evidence pipeline
gh v2.99 gained a native --attach flag for issues, PRs and comments, so
the custom trusted-publisher chain (gh-image extension + GH_IMAGE_SESSION_TOKEN
workflow_run job + attachment-upload script) is superseded.

Remove:
- .github/workflows/publish-e2e-evidence.yml (workflow_run publisher)
- scripts/ci/publish_e2e_evidence.py + its tests
- e2e-evidence-* artifact upload + evidence staging in e2e-desktop.yml /
  e2e_screenshot_status.py, incl. the 'inline evidence is publishing...'
  marker placeholder in the CI review comment status

Keep: the review-comment screenshot/diff counts and artifact links produced
by e2e_screenshot_status.py.
2026-09-24 23:53:17 -04:00
ethernet
428d2d6a86 ci(classify): tests/_fixtures re-arms the desktop-updater lane
The conftest split moved its platform gating and live-system guard into
tests/_fixtures/. tests/conftest.py re-arms the desktop-updater lane
because it owns that skip logic; the modules it now delegates to must
too, or an edit there skips the lane that exercises it.
2026-09-24 14:40:09 -04:00