Commit Graph

2826 Commits

Author SHA1 Message Date
Teknium
a0a49926f4 fix(check): blob-size exempts untracked copies and reads --head's allowlist (review)
- An untracked file whose bytes the base tree already holds (git hash-object
  matches a base blob) is no longer an offender, matching what staging the
  same copy already did.
- With an explicit --head, the allowlist comes from that revision
  (git cat-file <head>:.github/blob-size-allowlist.txt; none = no
  exemptions), so `--base A --head B` judges B by B's own exceptions.
  Working-tree mode still reads the file from disk.
- Tests for both: untracked copies of a base binary and base text file pass
  while an edited one is still caught; --head honours head's list over a
  conflicting checkout, a head without the list exempts nothing, and
  working-tree mode reads the disk list.
2026-10-11 17:16:57 -07:00
Teknium
9c5a59c63f feat(check): run the blob-size guard in scripts/check
The guard judges a diff, so it gets the same base/head as the ratchet:
--staged judges the index, --commit the pushed tip, and a plain run the
working tree plus untracked files against the merge-base with
origin/main. `python scripts/check --only blob-size` reproduces CI.
2026-10-11 17:16:57 -07:00
Teknium
db5e618205 feat(ci): reject PRs that add binaries or new files over 500 KiB
Every blob a merge adds stays in every clone forever. In the last 90
days main received a 2 MB PR screenshot, a 2 MB default.tar.gz, three
PR infographics, a Windows cache database written by a test and a 1 MB
fuzz corpus at the repo root; nothing looked at file size.

scripts/ci/check_blob_size.py judges each path changed base -> head:
a binary (git's own --numstat detection) whose new content is over
500 KiB, added or modified, and any newly added file over 500 KiB.
Edits to existing text files stay with the code-health FILE_LINES
ratchet. Content the base tree already holds (a pure move, a copy)
adds no bytes and is not judged. Exact-path exceptions live in
.github/blob-size-allowlist.txt (CODEOWNERS-reviewed), seeded with the
one generated asset over the limit that legitimately changes.

It runs as its own unconditional workflow, like the case-collision
check, because asset-only PRs skip the Python and frontend lanes.
2026-10-11 17:16:57 -07:00
Teknium
1fd1827726 fix(release): the Store submit reads the submission with 'get', not 'status'
rc.12-v0.21.7 failed 'Submit the verified Store package' with
StoreError: unreadable submission status: ''.

store.submit parsed 'msstore submission status' as JSON. For a packaged
(MSIX) app that command prints a Spectre markup line ('Submission Status =
...') plus status tables, never JSON, and prints nothing when no submission
is pending (msstore-cli Commands/Submission/StatusCommand.cs). The first real
Store submission therefore always failed. 'msstore submission get' serializes
the submission as JSON (GetCommand.cs) and is already what submit reads after
the upload, so the in-flight check reads that instead.

The test fake now answers 'status' the way the CLI does; with it, main fails
5 tests with the production error.
2026-10-11 13:31:55 -07:00
ethernet
a12c3417c8 fix(release): abandon only the approved attempt, and only after the new cut is viable
Review findings on the abandon prompt:

- The prompt approved one ref but `abandon()` re-read the remote and cleared
  every outstanding attempt of that version, so a concurrent operator's
  replacement attempt could be cancelled and its draft deleted. The remote
  is now read again after the answer, and anything other than the same single
  attempt is refused; only that attempt is abandoned.
- Abandonment ran before the ancestry, final-tag and draft-body checks, so a
  yes could destroy the old attempt and then refuse the replacement. Those
  checks now run first; the prompt is the last step before the claim. The
  attempt number ignores markers, so abandoning does not change the tag.
- The prompt test now varies stdin and stdout separately and asserts `input`
  is never called unless both are terminals.
2026-10-11 15:22:52 -04:00
ethernet
74d788fcc9 feat(release): offer to abandon a blocking attempt from an interactive terminal
`release.py release` refuses while any attempt is outstanding and prints
the abandon command for the operator to copy. When stdin and stdout are
both terminals it now asks `Abandon <ref> and cut a new attempt? [y/N]`
right after that guidance. Yes goes through the same `abandon` as the
subcommand (cancel runs, delete the draft, write the marker) and the cut
continues as the next attempt; the default is no, and anything
non-interactive still refuses as before.

The confirmer and `inspect` are parameters of `release()`, not globals,
so only the CLI entrypoint opts in.
2026-10-11 15:22:52 -04:00
Teknium
8f05f890c5 ci(remote-backends): debian 13 for the test sshd, container setup as a mounted script
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.
2026-10-11 11:55:56 -07:00
Teknium
ecf7d948e6 ci(remote-backends): fail on skips from the runner's parsed counts (review)
The lane's skip check grepped the progress line for an "<n>s," suffix, but
_print_progress prints skips before expected failures, so a skip beside the
SSH suite's xfail ("23✓ 1s 1xf, ...") passed unnoticed.

- run_tests_parallel.py: new --fail-on-skip. After the run, any parsed
  pytest skip count fails the exit code and lists the skipping files, the
  same way the zero-collected guard does; no display format involved. The
  skip and failure reports move out of main() into helpers (health ratchet).
- remote-backends.yml: runs scripts/run_tests.sh --fail-on-skip instead of
  tee + grep.
- Tests: a skip next to a strict xfail passes without the flag and fails
  with it; a skip-free run passes with it; the workflow test pins the flag.
2026-10-11 11:55:56 -07:00
Teknium
438d10200c ci: run the SSH and Docker terminal backends against a real sshd and daemon
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.
2026-10-11 11:55:56 -07:00
Teknium
0d3b11c21c ci(release): refuse Intel-less retirement targets and define opt-in platforms once
- Retiring a preview channel now requires the stable head to carry every
  package the preview ships. An Intel preview could otherwise be pinned to an
  arm64-only stable head it has no package in, and its client would fail to
  resolve the retirement for good.
- The docs and release.py's next-steps text said Intel Macs stay on the last
  Intel release's feed. They do not: promote rewrites the one shared macOS
  stable feed with the arm64 leg, so Intel Macs get no update until a release
  opts Intel back in. Only the Termux APT channel stays on its previous release.
- New scripts/releases/optional_platforms.py holds the claim flag, jobs,
  handoff, target and smoke of each opt-in platform; stable.py, channel
  releases, release artifacts and the entrypoint read from it. stable.py is
  back under 1,000 lines. No behavior change.
- Test: a macOS Intel receipt against a baseline without Intel plans no
  upgrade arm (baseline=none); against a baseline with Intel it plans one.
2026-10-11 11:38:33 -07:00
Teknium
5f9b77cce0 ci(release): stop building macOS Intel and Termux in stable releases by default
Stable releases no longer build the macOS Intel (darwin-x64) desktop bundle
or the Termux APT package unless the release is cut with an opt-in flag.
Intel macOS is not supported on main, and its macos-15-intel bundle leg
takes 76-88 minutes, which is most of the release's wall time.

    python scripts/release.py release --commit SHA --include-macos-intel
    python scripts/release.py release --commit SHA --include-termux

The flags are written into the signed claim as includeMacosIntel and
includeTermux, like skipBundles/skipTests. admit emits them, and the
candidates-darwin-x64, transitions-darwin-x64 and macos-packaged-x64 jobs
(Intel) and candidates-termux and termux-checks (Termux) run only when
their flag is on. The gate requires those jobs to report skipped when the
flag is off; the skip flags keep their meaning.

Claims cut before these keys existed (every rc.* tag so far) have neither
key. validate_claim treats a missing key as false and still refuses a
non-boolean value or any unknown key, so sequencer discover/reconcile keep
working over the historical tags.

Without the opt-in platforms the candidate manifest, receipts, mac feed,
protected R2 head, APT promotion and downloads table all work with those
rows absent: the macOS feed carries the arm64 leg alone, the APT indexes
are left on the previous Termux release, and the unbuilt smoke is recorded
as skipped (a passed smoke for an unbuilt target is refused). A release
that opts Intel back in after one that left it out has no Intel baseline,
so its Intel upgrade arm is skipped like a first release.

Preview channels, canary and the PM bundle artifacts are unchanged.
2026-10-11 11:38:33 -07:00
Teknium
74f77ab9b0 Code health blocks new wall-clock sleeps in tests (HX013), adds a shared wait_until helper (#136644)
* feat(code-health): HX013 blocks new wall-clock sleeps in tests

A fixed time.sleep()/asyncio.sleep() is a bet that the code under test finishes
in time; on a loaded runner it loses and the test flakes. Main has ~1,470 of
them in tests/ and nothing stopped new ones (HX rules exclude tests).

HX013 runs only on test paths (Rule.include) and flags time/asyncio/anyio/trio
sleeps with a delay that is not a literal 0, plus a fresh threading.Event().wait(x).
Existing sleeps are grandfathered by the ratchet's base-relative compare; moved
code keeps its debt; '# health: allow HX013 -- <why>' waives a deliberate delay.

The fix text points at tests/waiting.py (wait_until / async_wait_until), a shared
poll-with-deadline helper replacing the ~60 private copies across the suite.

* fix(code-health): HX013 passes the interval of a poll under a deadline

The frozen replay (PRs merged 2026-10-08..09) hit three private poll helpers
(while not done and monotonic() < end: sleep(x)): that is the compliant shape the
fix text recommends, not a fixed delay. A while loop that reads a clock and can
end on a non-clock condition now exempts its sleep; a loop that only waits out
the clock, or polls with no deadline, still counts. tests/waiting.py needs no
waiver any more; tests/AGENTS.md names the rule and the helper.

* fix(code-health): HX013 passes a round-bounded poll loop

The second frozen window (2026-10-06..07) hit 'for _ in range(200): await
asyncio.sleep(0.01); if idle: return', a poll bounded by its round count: load
stretches it but cannot make it return early. A for-range loop whose body can
leave on a non-clock condition now exempts its sleep; a range loop of fixed
delays, or a poll over arbitrary items, still counts.

* fix(code-health): HX013 exempts only a real poll's own interval

Review fixes for the HX013 poll-loop exemption, plus a file split:

- Split: the HX013 checker moves to scripts/code_health/py_rules_sleep.py,
  and the AST helpers it shares with py_rules (Ctx, _FUNCS, _dotted,
  _call_name, _deferred_parts, _eager) move to a leaf module py_ast.py.
  py_rules.py drops from 1,034 to 877 lines (928 on main); CHECKERS still
  registers HX013.
- Clock-only conditions: only a test operand that reads something other
  than the clock counts as a condition. Combined clock checks
  (`monotonic() < a and monotonic() < b`), `not`/constant operands, and
  locals assigned only from clock arithmetic (`now < end`) no longer
  exempt the sleep. `and`/`or`/`not` follow their logic, so `while not
  done() or monotonic() < end` still waits out the clock.
- Loop ownership: a `break` ends only the loop whose body holds it (a
  nested loop's break does not count for the outer loop; one in the
  nested loop's `else:` does). Nested defs, lambdas and classes are
  skipped. A sleep in a loop's `else:`, `while` test or `for` iterable is
  not that loop's interval.
- Retry polls: a `try` body's return/break counts as conditional when the
  body calls something that can raise (`try: return probe() except
  OSError: sleep`). A body that only sleeps still counts as a fixed delay.

On origin/main this flags 2 more grandfathered sleeps and exempts 12 retry
polls the old rule missed, so the count goes from 923 to 913.
2026-10-11 11:27:22 -07:00
Teknium
76c1a50eb1 fix(release): an artifact download that fails its checksum is fetched again
rc.11-v0.21.7's Windows x64 MSIX smoke failed with 'Artifact checksum
mismatch' on the 2.6 GB package. The stored object was correct (re-downloaded:
size and SHA256 match the receipt exactly), so the runner received bad bytes.
_download_url retried HTTP and connection errors but raised on a completed
body that failed its hash, so one bad transfer failed the job and its
dependents until a whole-run rerun.

A size/SHA256 mismatch now counts as a bad transfer and is fetched again
within the same three attempts. A stored object that really differs from its
receipt fails all three the same way, and the verified destination is never
replaced.
2026-10-11 06:06:05 -07:00
Teknium
7bc05085f0 fix(ci): docs drift handles repeated placeholders, scopes its warning filter
Review follow-ups: `providers.<id>.models.<model>.timeout_seconds` now
matches (a second <...> segment no longer ends the pattern), the YAML
warning filter is scoped with catch_warnings, and the slash-command list
drops a `hidden` filter CommandDef never had.
2026-10-11 05:09:46 -07:00
Teknium
4e8fe569e4 feat(ci): new config keys and slash commands must ship with docs
DEFAULT_CONFIG changed 183 times in 30 days and nothing checked that a new
key reached the docs; /pause (the global emergency stop) went undocumented
for two months.

scripts/ci/check_docs_drift.py lists every DEFAULT_CONFIG leaf and every
registered slash command and finds the ones the docs never mention. A key is
documented by its dotted path, a placeholder path in inline code
(`auxiliary.<task>.timeout`, `display.pet.*`), or a YAML example nesting it
under its parents; a slash command by a `/name` row in
reference/slash-commands.md.

The 132 keys undocumented today are baselined in docs_drift_baseline.json;
tests/ci/test_docs_drift.py fails only on NEW gaps, so it never blocks an
unrelated PR, and documenting a baselined key never fails. Deterministic,
stdlib + repo imports, ~1s.

/pause is documented here, so the slash-command baseline starts empty.
contributing.md says what to do when the test fails.
2026-10-11 05:09:46 -07:00
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
efd17a70b5 fix(release): staging checks each metadata row's commit and tag, not stamp-only fields
rc.10-v0.21.7's "Stage verified stable candidate artifacts" job failed with

  ValueError: Built package provenance does not match the release

assemble() ran stamp_matches() on each metadata row. stamp_matches() is the
check for an install-stamp.json and requires baseVersion, but a metadata row
is what record() writes (platform, arch, tag, commit, identity, version, ...)
and never carries baseVersion. So every bundled stable release failed here
once all packages were built. record() already verified each package's own
stamp (including baseVersion) when it wrote the row.

The row check is now what the row actually contains: commit and tag. The
staged-candidate fixture wrote a baseVersion into its rows, which is how the
mismatch stayed hidden; it now writes exactly the keys record() does. With
the corrected fixture, main's code fails 8 tests with the production error.
2026-10-11 02:56:13 -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
Teknium
b30f95ac4a fix(release): discover orders attempts numerically so rc.10 is the version's record
Review on #136434 (@jquesnelle): discover() sorted records by version only,
so attempts inside a version kept git ls-remote's NAME order (rc.10 between
rc.1 and rc.2). reconcile indexes records by version with the last one
winning, so once a version reached rc.10 it handed rc.9 (burned, no release)
to publish_attempt, which refuses with "Final tag release database ID is
invalid". v0.21.7 is at rc.9, so its next cut is exactly rc.10.

- Sort by (version, attempt).
- The test fakes list refs sorted by name, as git does; insertion order hid this.
- rc.10 test (fails without the sort), and a same-commit retry test (rc.1
  abandoned, rc.2 re-cut at the same commit wins).
2026-10-10 23:58:25 -07:00
Teknium
3e1d9dc59a fix(release): an abandoned attempt is burned before its version's final tag is checked
Every Stable Release Publication run since v0.21.6 has crashed in
sequencer.discover():

  ValueError: v0.21.6 points at a different commit than rc.1-v0.21.6

v0.21.6 took four attempts: rc.1-3 were abandoned (each has its
abandoned-rc.<N> marker) and rc.4 won and became v0.21.6. discover() tested
"does the final tag exist" before "is this attempt abandoned", so for rc.1 it
compared v0.21.6's commit (rc.4's) with rc.1's and raised; the abandoned
branch was only reachable for versions that never got a final tag. Any
version whose winner was not rc.1 broke every later reconcile, including the
next release's publish.

The abandoned marker is now checked first: an abandoned attempt is burned
whatever its version became. A NON-abandoned attempt whose final tag points
elsewhere still raises.
2026-10-10 23:58:25 -07:00
ethernet
9456670d2f Merge pull request #135743 from NousResearch/harder-linter-fixes-part3
Harder linter fixes part 3
2026-10-10 17:55:39 -04:00
elvindu
013e94b38a fix(install): name the marker owner as the Desktop hand-off pid
The posix hand-off hands marker line 1 to a custodian subshell
(marker_refresher_start) and then exported HERMES_UPDATE_HANDOFF_PID="$$" —
the hand-off script's own pid.

`hermes update` adopts a live claim only when the owner is its own pid, the
HERMES_UPDATE_HANDOFF_PID value, or an ancestor
(update_lock.UpdateLock._is_partner). The custodian is a *sibling* of the
hand-off script, so it is none of the three and the ancestry walk can never
reach it: every desktop-initiated update refused the claim its own script had
just made and exited 2 ("Another Hermes update is already running ...
process <custodian>"), leaving the Desktop to relaunch and retry forever.

Export $MY_PID instead: the custodian once the handover succeeded, and the
script's own pid when it did not.

Fixes #134268
2026-10-10 10:44:34 -07:00
kshitijk4poor
31d1bd3491 fix(scripts): count only committed attempts as batch compactions in the micro-compaction report
Review thread on #136050 (agent/conversation_compression_telemetry.py,
"Refusals inflate batch counts"). Since #135807 every exit of
`compress_context` (blocked by a guard, skipped, aborted, refused) logs one
`context compression attempt telemetry:` line, and
`scripts/micro_compaction_report.py` counted every such line as a batch
compaction. Keep only records whose `commit_status` is `committed`.
2026-10-10 20:18:10 +05:30
Maciej Lisiewski
bda3aea496 ci: remove unused variables (linter) 2026-10-10 15:20:14 +02:00
Maciej Lisiewski
0006a5e229 ci: fix PLW1509, PLW1510, PLW2101 linter errors 2026-10-10 15:20:13 +02:00
Teknium
3eb7ed08d1 fix(skills-index): stop shipping skills.sh rows that cannot install
The builder shipped ~1.9k skills.sh rows per run with no resolved_github_id.
Most were rows whose repo 404s or whose fully-read tree has no matching
SKILL.md dir (renamed/removed upstream); every client install of one spent
~40 GitHub calls and failed as stale_index.

- fuzzy match ignores both '-' and '_' (pdb-database slug vs pdb_database dir)
- a single-skill repo (root or generic skills/ SKILL.md) resolves like the client does
- repo 404 or complete tree with no match: the row is dropped, counts logged
- truncated tree: bounded contents/<candidate>/SKILL.md probes, row kept if none hit
- transient errors keep the row unresolved, as before
2026-10-09 18:19:17 -07:00
Maciej Lisiewski
13b699106d fix: termux script error 2026-10-09 20:30:20 +02:00
ethernet
040dbb9053 Merge pull request #135348 from NousResearch/harder-linter-fixes-part2
Harder linter fixes part 2
2026-10-09 13:08:10 -04:00
Maciej Lisiewski
2ff58849f4 ci: ruff UP037 fixes 2026-10-09 17:34:38 +02:00
Maciej Lisiewski
9eab688b42 ci: ruff UP017 fixes 2026-10-09 17:34:36 +02:00
Maciej Lisiewski
32b70e9733 ci: FURB188, FURB192 linter fixes 2026-10-09 17:31:52 +02:00
ethernet
25a096f317 fix(release): treat any missing stable baseline as no upgrade arm
A release that skipped bundles never writes the stable baseline pointer,
so its absence is normal at any point, not only before the first bundle
release. Drop the final-tag scan. A 404 on the default pointer now always
plans no upgrade arm. A supplied baseline-manifest that does not exist
still blocks, because that is a bad input.
2026-10-09 01:41:35 -04:00
ethernet
4e23d79de4 fix(release): let the first bundle release pass without an upgrade baseline
No stable release has shipped signed packages yet. Every final tag so far
skipped bundles or predates the claim pipeline. So
releases/stable/release-candidates.json does not exist, and the three
"Pin actual OLD and NEW signed packages" jobs refused with "No published
stable package baseline".

transitions now treats a 404 as a first release only when no final stable
tag records a candidate manifest and no baseline-manifest input was given.
It still validates the receipt, emits empty matrices and baseline=none.
The packaged upgrade arms skip on that output. The gate accepts their skip
only when their own planner succeeded with baseline=none, so a missing
baseline after a real bundle release still blocks.
2026-10-09 01:37:59 -04:00
Teknium
3b0dc776b6 fix(tests): the test runner kills detached processes a test leaves behind
`_kill_tree` SIGKILLs the per-file pytest process group, which misses any
child that left the group with `start_new_session=True` (an auto-started
`gateway run`, a setsid server). Those are reparented to init and run
forever: one host accumulated 215 orphan gateways (~52 GB RSS, up to 21 h
old), and every `hermes update` listed each as "another Hermes home — left
alone".

After every attempt the runner now SIGKILLs any process whose environment
still points into that attempt's unique temp root (PYTEST_DEBUG_TEMPROOT /
TMPDIR, or a HOME / HERMES_HOME a fixture created under it). The root is
unique per attempt, so the match names only that attempt's processes.
Linux-only (/proc); the process-group kill stays the fallback elsewhere.
2026-10-08 19:48:33 -07: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
ethernet
8ac5c74432 Harder linter fixes (#135281)
* ci: fix B004 linter rule

* ci: prefix unused variables with underscore (RUF059)

* ci: don't cast int to int

* ci: remove unnecessary if/else where both blocks yield same result

* ci: RUF024

* ci: fix RUF017, RUF018, RUF022

* ci: RUF013, RUF015

* ci: RUF007

* ci: RUF009

* ci: address code review
2026-10-08 18:56:50 -04:00
kshitijk4poor
bc0758c393 fix(desktop): a renderer built under other VITE_*/NODE_ENV settings is never reused
Vite inlines every VITE_* variable into the renderer and NODE_ENV selects its mode, but the
desktop receipt recorded only sources and prepared inputs. After `VITE_PERF_PROBE=1 npm run
build`, the next plain build reused (and productCurrent certified) the renderer that still
carries the perf probe; a NODE_ENV change had the same gap.

The desktop build inputs now record the renderer environment (VITE_* plus NODE_ENV, unset
read as vite's production default), so both rendererCurrent and productCurrent require it to
match. Reported by Greptile on #135092.
2026-10-08 15:20:58 -07:00
kshitijk4poor
3fc37481eb refactor(desktop): one receipt reader, one output list, no native double copy on renderer reuse
rendererCurrent and productCurrent share hostReceipt() for the schema/product/host checks. The stamped outputs come from bundleElectronMain's own electronOutputs list, so a renamed or added bundle can never be copied stale from the previous dist. The reuse copy skips node_modules/ and native/, which copyNativeTree recopies from nativeDeps right after, and a --typecheck build never reuses a renderer that skipped tsc.
2026-10-08 15:20:58 -07:00
kshitijk4poor
dcabf76310 fix(install): a pinned fresh clone pins, with retries, before it publishes
With --no-checkout the file download moved out of the retried clone into the single unretried pin checkout, which ran after the empty clone was already published: a throttled pin left INSTALL_DIR with no index, and the next rerun stashed a 'delete every file' autostash. The pin now runs in the staged tree inside the deferred-checkout retry loop (which also stops materializing the tip first), and nothing is published until it succeeds.
2026-10-08 15:20:58 -07:00
kshitijk4poor
2f6716dd7d perf(install): a pinned install checks out only the pin, not the branch tip first 2026-10-08 15:20:58 -07:00
kshitijk4poor
1a4d7768c5 perf(desktop): reuse the compiled renderer when only the install stamp changed 2026-10-08 15:20:58 -07:00
Teknium
dbbf98371d perf(install): a pinned install.ps1 clone checks out only the pin, before it publishes
Mirrors the install.sh change from #135092 on Windows. With -Commit (what
the desktop bootstrap passes), the fresh clone runs with --no-checkout and
the pin is checked out with retries inside the staging dir, so the branch
tip is never materialized first and a refused or failed pin never publishes
a checkout. A rerun over an existing checkout keeps pinning after the fetch.
2026-10-08 15:15:33 -07:00
ethernet
3d7f1090d4 fix(desktop): build-bundle.ps1 parses the claim timestamp and rejects a missing -Python
The tagger-line lookup indexed into a single string, so the epoch was the
character 't'. A -Python path that does not exist left LASTEXITCODE at 0, so the
script printed done and listed a previous build's package.
2026-10-08 18:02:26 -04:00
ethernet
aac3440bac fix(desktop): build-bundle cleans under the lock, restores the shell, never signs
Review findings on the local build scripts:

- Cleanup ran before desktop.py took the checkout lock, so a second run deleted
  the outputs of a build that was still going. desktop.py gains --clean, which
  removes this checkout's outputs after build_lock is held. Both scripts pass it
  instead of deleting files themselves.
- build-bundle.ps1 left RELEASE_CLAIM_* and HERMES_PAYLOAD_* in the caller's
  session. It now saves those variables and restores them in the finally block.
- The scripts said "unsigned" but inherited signing credentials. The ps1 removes
  AZURE_* for the build. The sh removes CSC_* and APPLE_* and sets
  CSC_IDENTITY_AUTO_DISCOVERY=false, the switch the desktop rebuild command uses.
  The wording now says what is ignored.
- The Store bundle step needs the claim timestamp, and the script deletes the
  claim. The ps1 prints it as HERMES_RELEASE_EPOCH, and BUILDING.md documents it.
2026-10-08 18:02:26 -04:00
ethernet
57949d6630 feat(desktop): build-bundle scripts for local test bundles
scripts/build-bundle.sh (macOS, Linux) and scripts/build-bundle.ps1 (Windows)
build the checked-out commit through scripts/bundles/desktop.py --commit. That
path needs no tag and no push.

Both scripts refuse a dirty tree before any work starts. Both delete the
previous build output first. The staged Windows payload is read-only, so a
plain delete in a reused checkout fails with WinError 5.

build-bundle.ps1 -Store builds the Microsoft Store MSIX. The driver admits
Store packaging only for a stable tag. The script makes a local claim tag
rc.<N>-vX.Y.Z for the next patch after the newest published stable tag. It
counts local and remote attempt refs and starts at 900, so it cannot collide
with a real attempt. A finally block deletes the tag. The script never pushes it.
2026-10-08 18:02:26 -04:00
Maciej Lisiewski
9d9a3840d1 ci: address code review 2026-10-08 22:16:26 +02:00
ethernet
7ad6fd69d7 fix(termux): repin the termux pool and drop the libc++ shadow shim
The payload pinned libc++ 29 while the build container's apt cmake needs 30,
so the two disagreed and the shim in the previous commit papered over it. The
pool has since retired 17 of our pins, so repin them all with
`hermes pm update --termux`, which fixes the mismatch at the source.

Repinned: libc++ 29 -> 30 plus 14 more runtime libs (fribidi, glib, harfbuzz,
libaom, libbluray, libexpat, libheif, libmp3lame, libpng, libvidstab, openssl,
pcre2, readline, vulkan-loader-generic) and the bionic lock rows for uv
0.12.15 -> 0.12.23 and ffmpeg 8.1.2-5 -> 8.1.3. The pool serves ffmpeg 8.1.3
only, so that row moves back from 9.0.1 as the pool has it.

Staging with the new table succeeds: 84 packages, 269 .so, same counts as
before. The libc++ SONAME is unchanged. The one unresolved DT_NEEDED
(libharfbuzz-cairo -> libcairo) is present on the old table too.

The shim is removed. What stays from it is the in-container
`cmake -E capabilities` check, which fails in 2 minutes if the table drifts
from the pool again, instead of after 30 minutes of compiling.
2026-10-08 15:50:12 -04:00
Maciej Lisiewski
4ee5e8e0f3 ci: RUF007 2026-10-08 21:35:58 +02:00
ethernet
5d8eafbc88 fix(termux): keep apt's libc++ ahead of the payload's on the build linker path
The wheelhouse build stage still compiled cmake from PyPI source (pillow ->
pybind11 -> cmake bootstrap, exit 11) after the Dockerfile and the apt install
began upgrading libc++ to 30. LD_LIBRARY_PATH put the payload's pinned libc++ 29
first, and a library found there beats the binary's own RUNPATH, so apt's cmake
died with 'cannot locate symbol _ZNSt6__ndk113__hash_memoryEPKvm' and
scikit-build-core read that as 'no cmake'.

Probed in the pinned termux-docker image under qemu with the same apt package
list: payload libc++ 29 first fails, a one-symlink dir to the system libc++ in
front of it starts cmake and clang. The build now makes that dir after apt runs
and fails fast if cmake cannot start, instead of burning 30 minutes compiling
cpython, cryptography and pillow first.
2026-10-08 15:31:36 -04:00