Commit Graph

190 Commits

Author SHA1 Message Date
Teknium
cdce56faf2 plugin-catalog CI: enforce rule 17 — scan pinned entries for unscannable payloads
The admission gate ran the install scanner (dangerous fails) but nothing refused
a compiled binary, committed bytecode, or an archive hiding either — the shapes
the scanner cannot read. This adds that enforcement to pinned-source-validate.

- scripts/scan_plugin_payloads.py (stdlib-only, so the no-install validator and
  third-party CI can run it): flags compiled binaries by magic header (ELF,
  Mach-O, PE/MZ, WebAssembly — not by extension, which a rename defeats),
  .pyc/.pyo bytecode (incl. sourceless/hash-forged caches that run instead of
  the .py beside them), and archives whose members are not reviewable source.
  Plain-text source bundles and inert assets (fonts, images) pass.
- plugin-catalog-ci.yml: after `hermes plugins validate`, each changed entry is
  scanned; a finding fails it. An entry may list `attested_binaries:` paths,
  each verified with `gh attestation verify --repo <repo> --source-digest <sha>`
  against the pinned commit; verified paths are dropped before the scan, any
  other binary still fails. Repo-controlled filenames are newline-stripped in
  the log so none can forge a workflow command.
- validate_plugin_catalog.py: `attested_binaries` is a known key, shape-checked
  as a list of relative paths.
- Tests: unit tests for the detector and two new workflow-driven bypass cases
  (bundled .exe, committed .pyc) in the confinement harness, which runs the real
  `run:` block.

Full-content re-sweep of all 543 pins: only tabro carries a native binary (it is
delisted in the companion PR). Three entries (crawl4ai, hermes-vision-inline,
mermail) commit __pycache__/*.pyc build litter and would fail rule 17 on their
next pin bump — pre-existing, not touched here.
2026-10-11 03:03:39 -07:00
Teknium
c152ba5a82 docs(ci): prompt-surface changes need one core approval, not two
The prompt-surface-review ruleset was meant to require one other reviewer;
it is now set to one hermes-agent-core approval. CODEOWNERS, the CI comment,
the staleness-test message and the developer docs said two.
2026-10-11 02:26:46 -07:00
Teknium
ae3598c7cd feat(ci): reviewers see every change to the system prompt and tool schemas
A one-line code change can rewrite what every session sends to the model -- a
guidance constant moving tiers, a toolset gaining a tool, a dynamic rewriter
editing a description -- without the diff showing it. The rendered prompt
surface is now committed and reviewed like code.

- scripts/ci/prompt_surface.py renders the system prompt
  (build_system_prompt_parts) and the agent's tools[] through the production
  code for nine surfaces (cli x claude/gpt/gemini/hermes, cli in a code
  workspace, desktop, telegram, discord, cron) into
  tests/fixtures/prompt_surface/. A fresh home in a child process; clock,
  host OS, network, tool availability, env probe and machine paths pinned,
  so a laptop and CI produce the same bytes. Each tool schema is stored once,
  with a per-surface copy only where a surface serves a different schema.
- tests/ci/test_prompt_surface.py fails when the committed snapshot is stale,
  so a surface change must carry the regenerated snapshot in the same PR.
- prompt-surface-diff.yml (advisory, PR-only, stdlib-only, no PR code runs)
  posts per-surface token deltas, prompt hunks and per-tool
  description/param changes into the unified review comment.
- CODEOWNERS requests hermes-agent-core on the snapshot; a new
  prompt-surface-review ruleset requires two of their approvals.
- Docs: prompt-assembly.md (how it works), contributing.md (what to do when
  the test fails).
2026-10-11 01:21:18 -07:00
Maciej Lisiewski
038dd659b7 ci: limit python cyclomatic complexity to pre-commit hook 2026-10-10 16:37:15 +02:00
Maciej Lisiewski
0006a5e229 ci: fix PLW1509, PLW1510, PLW2101 linter errors 2026-10-10 15:20:13 +02:00
Teknium
a274d3388f fix: core modules import again on Python 3.11-3.13 (re-quote eager forward references)
#135348 ran ruff's UP037 autofix with ruff.strict.toml's target-version = "py314". On 3.14
annotations are lazy (PEP 649), so stripping quotes is safe there; on 3.11-3.13 a function or
variable annotation is evaluated when the def or statement runs. Every unquoted annotation that
names the enclosing class (`def clone_for_agent(self) -> ContextEngine`), a class bound later in
the module, or a TYPE_CHECKING-only import now raises NameError on import: 300 modules on main
(gateway.config, run_agent, hermes_state, cron.jobs, the Telegram adapter, ...) fail to import
under 3.11, the update path crashes mid-`hermes update` on any install whose interpreter is
older than 3.14, and the e2e-upgrade suite's retry leg fails with
`NameError: name 'ContextEngine' is not defined`.

- Re-quote exactly the 79 annotations (36 files) that name something unbound when they are
  evaluated (including tui_gateway/server.py's `_TurnScopes`, which only reaches that module's
  globals through register() after import); no other text changes.
- agent/turn_scripted_prelude.py: `Generator[...]` gets its third argument back. d76cce4f11
  applied UP043 (default type args, valid only from 3.13) to a module-level alias, so
  `Generator[Y, S]` raises TypeError on 3.11/3.12 and takes gateway.run, agent.conversation_loop
  and tui_gateway.{server,ws,entry} down with it.
- ruff.strict.toml target-version = "py311", the requires-python floor. With it UP037 leaves
  these quotes alone and only strips the two that are genuinely redundant, which this commit
  strips; any future version-gated fix stays valid on every supported Python.
- tests/ci/test_strict_ruff_target.py pins the target to the requires-python floor (red on main).
2026-10-09 14:25:07 -07:00
Teknium
ddfaf76cc6 test(ci): a hung replayed workflow step fails in 30 s instead of hanging the Windows lane
The Windows-only lane killed tests/ci/test_update_ci_routing.py at the
runner's 300 s cap, mid test_replay_rejects_ineffective_required_step, with
no output. workflow_steps._run used subprocess.run(capture_output=True,
timeout=30). On Windows, run() on timeout kills bash and then calls
communicate() with no timeout. Git's bash.exe launcher spawns usr/bin/bash.exe,
which keeps the inherited pipe open, so that read never ends and the
30 s timeout never surfaced.

Capture to files instead of pipes, so the timeout raises and names the step.
A POSIX probe of the same shape (kill the child while a grandchild holds the
pipe) shows the pipe read still blocked after 10 s; the file-backed run
raises at 2 s. On PR #135560, whose diff touches no CI code.
2026-10-09 14:22:08 -07:00
Maciej Lisiewski
9eab688b42 ci: ruff UP017 fixes 2026-10-09 17:34:36 +02:00
Teknium
ffcd3621a9 ci: E2E suites run only on release builds
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.
2026-10-08 19:48:31 -07:00
Maciej Lisiewski
aa2d120cba ci: prefix unused variables with underscore (RUF059) 2026-10-08 19:22:03 +02:00
Maciej Lisiewski
d76cce4f11 ci: apply all automatic linter fixes available 2026-10-08 17:22:54 +02:00
Teknium
818c13be1d fix(release): give the Nix release build its commit and push Docker images under the attempt ref
Two stable-release failures on rc.3-v0.21.6 (run 37753841932):

- nix: the stamped release source is a `path:` flake with no git metadata,
  so self.rev is null, install-stamp.json carries no commit and the packaged
  `hermes --version` reports "unknown". Pass the commit as `?rev=$GITHUB_SHA`
  (verified on Nix 2.35: path:<tree>?rev=<sha> sets self.rev).
- docker: the versioned images were pushed under the final vX.Y.Z, which does
  not exist until publish. rc.1 created `:v0.21.6`, and every later attempt hit
  "versioned Docker manifest differs" against it. scripts/releases/docker.py
  (published_digest, promote_stable) already reads the attempt-ref tag, so pass
  claim-tag to both docker.yml calls.
2026-10-08 03:40:51 -07:00
teknium1
1ab7983641 test(ci): the CI replay does not run Git Bash under x64 emulation on Windows arm64
Mechanism: Git for Windows on arm64 ships native git.exe but an x86-64 MSYS
usr/bin/bash.exe, which runs under the WoA x64 emulator. With the os-tests
lane's 16 parallel workers, a replayed step's bash dies before running a line
with 0xC000026F (STATUS_WX86_INTERNAL_ERROR) or 0xC0000005 and empty
stdout/stderr, so an unmodified baseline replay fails as a bare
AssertionError (jobs 112635777217 and 112764629268, two different nodeids).

Repro (throwaway workflows on the real runners, previous commit's diagnostics):
- windows-latest-32-arm-core, 16 copies of the file's replay tests at once,
  4 rounds: 21/64 copies red, every failure 0xC000026F.
- Same load, windows-latest-32-core (x64): 0/64.
- Passing SYSTEMROOT/WINDIR or the full Windows env to the step: 12/48 and
  17/48 vs 15/48 base, so the stripped env is not the cause.
- One process, 32 threads, 600 replays: 0 crashes; it takes concurrent
  processes, i.e. host-wide emulator load.

Seam: _NATIVE_WINDOWS_TOO keeps platforms("linux", "windows") and adds a
skipif for win32 + native_arch() == arm64. The x64 Windows row still runs the
replay on the Windows layout (python.exe-only venv, Git Bash), Linux arm64
keeps it (native bash), and the routing guard that pins the marker passes.
2026-10-07 07:49:08 -07:00
teknium1
effe2ffa14 test(ci): the CI replay names a silent step's exit code
workflow_steps.py is a helper module, not a test module, so pytest does not
rewrite its asserts: a replayed bash step that exits non-zero with empty
stdout/stderr fails as a bare 'AssertionError' with no exit code. Two
Windows arm64 os-tests runs (jobs 112635777217, 112764629268) failed this way
on unmodified baseline replays, and the NTSTATUS needed to tell a crashed
child (0xC0000142 / 0xC0000005) from a bash failure was lost. Put rc (decimal
and hex), the bash used, and both streams in the message.
2026-10-07 07:49:08 -07:00
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