Commit Graph

2610 Commits

Author SHA1 Message Date
ethernet
b4d5092d3f refactor(activate): split interactive activation from running a command in the env
`activate` doubled as the thing scripts sourced non-interactively, so it
carried `--` argument guards and every caller had to tolerate its prompt and
`hermes` function. Composition (setup sync, bootstrap Python, pm env) now
lives in scripts/_activation.sh, and there are two consumers:

- `activate`: interactive, reversible (`deactivate`).
- new `scripts/run-in-hermes-env CMD...`: applies the env to one process and
  execs into it. Replaces `scripts/_hermes-python`; every Python script's
  shebang and `scripts/run_tests.sh` (which re-executes itself under it,
  however it was launched) use it.

`pm.environments --format sh` emits the env as shell exports, so the inline
JSON-to-export Python is gone from `activate`. An install that did not build
the test environment now reads as stale for every consumer.

Tests are split along the same seams: activation support fixtures, bash/ps1
activation, env-script quoting, and the runner's staleness.
2026-10-02 18:15:08 -04:00
teknium1
35eacd248a fix(update): keep git's own lazy-fetch pack fold on in a partial clone
#131679 set maintenance.auto=false in the treeless install checkout to stop the
commit-graph lazy-fetch loop (#127711). That also switched off the post-fetch
`gc --auto` that git <= 2.53 runs after every lazy fetch, so packs piled up
between updates and only folded on a manual `hermes update` / `--check`
(nothing runs `--check` automatically).

The loop needs a commit-graph write, and gc.writeCommitGraph=false +
fetch.writeCommitGraph=false already stop gc and fetch from writing one.
maintenance.commit-graph.enabled=false closes the last writer (a commit-graph
maintenance task, e.g. from a user's global maintenance.strategy) while
leaving automatic maintenance, and its gc fold, on.

Checkouts that already got maintenance.auto=false from the first cut are
converged: the next update/--check unsets exactly that value.

Measured on a real tree:0 clone with a Bloom commit-graph, 100 lazy fetches
after one updater run:
  git 2.53.0: main 101 packs, this change 1 pack, 0 commit-graph writes
  git 2.55.0: 101 / 101 (stock 2.55 maintenance does not fold), 0 writes
2026-10-02 14:17:41 -07:00
ethernet
6880ffc1af fix(update): make the partial-clone pack fold and maintenance keys work together (#129712, #127711)
The two salvaged commits above disagree: #127721 persists gc.auto=0, which
turns the fold's `gc --auto` into a no-op (106 packs stay 106 on git 2.50.1,
2.53.0 and 2.55.0), and the fold itself is exposed to the loop #127721
guards against. Probed on all three gits against a blobless clone whose
commit-graph carries changed-path (Bloom) data:

- gc, `gc --auto`, `maintenance run --task=commit-graph` and
  fetch.writeCommitGraph each lazy-fetch the trees of every commit the graph
  has not seen (30 unseen commits: 30 fetches, 30 packs), and every lazy
  fetch spawns `git maintenance` again. gc.writeCommitGraph=false stops it
  (gc: 31 packs -> 1).
- Without a Bloom graph none of that happens, so the keys are a guard, not a
  fix for every partial clone.

Changes:
- keys: maintenance.auto, gc.writeCommitGraph, fetch.writeCommitGraph; gc.auto
  stays default. Idempotent (no rewrite when already set), `--local`, hidden
  console on Windows. install.sh and install.ps1 set the same three keys.
- consolidate_lazy_fetch_packs applies the keys before the fold, so installs
  that predate the installer change converge on their next update check.
- the fold runs under bounded_probe_run: subprocess.run(timeout=) kills only
  `git gc` and leaves `pack-objects` running (reproduced), and with
  GIT_NO_LAZY_FETCH.
- dropped the post-gc mark_unmarked_packs_promisor: gc keeps the .promisor
  marker on 2.50.1, 2.53.0 and 2.55.0.
- docs: updating.md troubleshooting entry.
2026-10-02 11:41:19 -07:00
whyyagswhy
73c17151f6 fix(install): disable auto maintenance in tree:0 clones (#127711) 2026-10-02 11:41:19 -07:00
ethernet
59dc1ad5ff fix(release): abandon force-cancels the attempt's in-progress runs
`release.py abandon` deleted the draft and wrote the marker but left the
attempt's Stable Release run going. A live run keeps building, and with
autopublish on it can publish the draft that abandon just cleared.

abandon now lists the claim ref's runs of stable-release.yml and runs
`gh run cancel <id> --force` on every one that has not completed. This
happens before the draft is deleted and before the marker is written: the
marker removes the attempt from the outstanding set, so a cancel failure
after it could never be retried. A published release is still refused
before anything is cancelled. The `delete` callable is renamed `execute`
since it now runs cancels too.
2026-10-02 13:43:18 -04:00
ethernet
386e31a56f fix(desktop): ship exact-size, theme-aware MSIX icons instead of one 44px tile
The MSIX package carried a single Square44x44Logo.png, so every taskbar and
Start slot on a HiDPI Windows 11 machine was Windows upscaling a 44px bitmap,
and with no altform variants Windows plated the icon itself. The generator now
emits the qualified catalog Windows resolves by itself: <Logo>.scale-{125,150,
200,400} for all four manifest logos at Microsoft's pixel sizes, and
Square44x44Logo.targetsize-{16..256} in the three required theme forms —
unqualified and altform-lightunplated carry the light tile, altform-unplated
(dark theme) the dark tile, the same split macOS dark mode gets. before-build
stages the whole assets/appx directory; app-builder-lib runs makepri and
`makeappx /l` as soon as it sees a qualified asset, so nothing else changes.
The hand-listed *-dark.png logos, generated but never staged, are gone.

Exposed along the way: the commit badge sat outside the portrait's silhouette
clip, and at 16–30px its anti-aliased edge stacked coverage on the plate's
corner pixels and changed the alpha channel between build flavors. The badge
now shares the clip, is plain black, and is left off direct renders below
32px where no SHA is legible anyway.
2026-10-02 12:51:02 -04:00
ethernet
be0eaab563 fix(scripts): portable bash shebang for set_dmg_volume_icon.sh 2026-10-02 10:33:44 -04:00
ethernet
0217a2a324 feat(icons): custom drive artwork as the volume icon of both installer DMGs
The desktop DMG's volume icon defaulted to the packager's icns, which is
actool's 256px fallback whenever mac.icon is the Icon Composer package, and
dmgbuild's badge option can only paste onto the stock removable-drive icon.
Ship our own drive-with-the-girl artwork instead: assets/dmg-volume.png
(hand-made, one artwork since volume icons have no appearance variants) is
emitted as apps/desktop/packaging/dmg-volume.icns, beside the DMG background
so the `files` whitelist keeps it out of the app bundle, and dmg.icon points
at it.

Tauri always uses the app icns as the volume icon and offers no override, so
the bootstrap workflow runs scripts/bundles/set_dmg_volume_icon.sh on the
built DMG before signing (the same .VolumeIcon.icns + custom-icon flag
dmgbuild sets), and the verify step checks the icon is on the mounted image.
Verified on macOS 26.6.2: both DMGs render the identical volume icon.
2026-10-02 10:33:44 -04:00
ethernet
383916b845 feat(icons): layered macOS 26 icon for the bootstrap installer and the DMGs
Tauri's bundler (cli 2.11.4) compiles an Icon Composer package listed in
bundle.icon into Assets.car with actool >= 26 and sets CFBundleIconName, so
the bootstrap installer gets an unbranded twin of the desktop package under
src-tauri/icons/icon.icon (generator target; the bundle stays the plain
brand because flavours only apply under apps/desktop/). Tauri only logs when
actool is too old, so the macOS job selects Xcode 26 and the verify step
asserts Assets.car and the plist key are in the built .app.

The desktop DMG's volume icon defaulted to the packager's icns, which is
actool's 256px fallback whenever mac.icon is the .icon package; point
dmg.icon at the full-resolution assets/icon.icns. Tauri's DMG already takes
--volicon from the bundle's .icns, which the regenerated outputs cover.
2026-10-02 10:33:44 -04:00
ethernet
c2b69de66d feat(icons): drop the brand ring everywhere and frost the mono icon
Next to macOS 26's own icons the flat plate + girl reads better than the
ringed tile, so the ring is off in every output: the tiles' inward stroke,
the layered icon's border layer and its targets. BORDER_ENABLED keeps the
ring code one flag away.

Without a ring the girl's lowest nodes are dragged past the tile edge
(EDGE_OVERSHOOT) so the outline crops her instead of leaving a gap.

Clear light/dark and Tinted light/dark now come from one grayscale mono
layer with Liquid Glass on: the parts of the girl that are black in light
mode become a near-black frosted ink (0.13 @ 90%), her white parts stay
white, each pixel painted exactly once so materials never stack. The art
layer hides under "tinted" and the mono layer shows only there.

Mac girl at 1.06x (between main's 1.12x and the layers' 1.0x), for the icns
and the layered icon alike. Chosen from a six-appearance matrix of real
IconServices renders next to Finder.
2026-10-02 10:33:44 -04:00
ethernet
e967eff52c fix(icons): register the layered icon's girl on the mac plate box
main scaled the girl 1.12x for every mac target (a white tile with a ring
reads small beside full-colour peers), but the macOS 26 layers were still
registered on the full-bleed squircle box, so macOS 26 would have shown a
smaller girl than macOS 15.

The layered icon's canvas is the plate itself, so derive its portrait box
from the mac box in plate coordinates. Ink overlap with the icns girl goes
from 0.65 to 0.996 IoU; only the two art layers change.
2026-10-02 10:33:44 -04:00
ethernet
02ed9f2649 fix(icons): give the macOS 26 icon its mono annotation for Clear and Tinted
Clear light/dark and Tinted light/dark all render from one "tinted"
specialization: the system reads the layer as luminance (white = visible)
and tints it. Without one they fall back to the light layers, whose black
girl and ring have no luminance, so those four modes showed an empty plate.

Point "tinted" at the white layers already used for dark mode; the manifest
test now requires it and pins it to the dark image. Verified in the Dock on
macOS 26.6.2 in Clear and Tinted, light and dark: white girl and ring as
glass / in the tint colour.
2026-10-02 10:33:44 -04:00
ethernet
cfc05e70b9 fix(icons): let actool pick the dark layer images for the macOS 26 icon
Each layer carried both a fixed "image-name" and per-appearance
"image-name-specializations". actool treats the fixed name as the image for
every appearance and drops the specializations, so Assets.car held only the
light PNGs and dark mode showed the black girl and black ring on the dark
fill: a black squircle. Icon Composer itself writes one or the other, never
both.

Drop the fixed name; the manifest test now rejects it and requires a dark
specialization on every layer. Verified on macOS 26.6.2 in dark mode: the
catalog gains art-dark/border-dark and the Dock shows the white girl.
2026-10-02 10:33:44 -04:00
ethernet
c179041466 feat(icons): macOS 26 layered icon whose border follows Apple's mask
macOS 26 clips every app icon to its own squircle and shows legacy .icns art
inside a glass plate. Our border is a stroke on the tile's rounded rect, so on
26 the ring sits inside a grey rim and thins toward the corners where Apple's
curve and ours diverge.

Add an Icon Composer package (apps/desktop/assets/icon.icon) of canvas-filling
layers the system masks itself: a border band whose outer edge is the canvas
(the mask supplies the outline) and whose inner edge is Apple's mask offset
inward by the brand border thickness, so the band is the same width everywhere
along the visible outline; and the girl (+ commit badge) as a second layer.
Build flavours recolour the fill in icon.json only.

Apple's mask is a circular arc flanked by two cubic easing segments (the
Figma corner-smoothing construction). Fitted to macOS 26.6.2's own render of
a system icon on the 824-on-1024 grid: radius 214px, smoothing 0.645, rms
0.32px / worst 0.81px; a superellipse cannot get under 0.54px rms.

The .icns targets are unchanged: Apple's own 26-era .icns still sit on the
824 grid, and macOS <= 15 keeps showing that artwork.

The new contract test probes the border layer 640 points along the mask:
opaque at thickness-1.5px, transparent at thickness+1.5px. Rendering the
layers with the old r=185.4 corner violates it at 1448 probe points.
2026-10-02 10:33:43 -04:00
teknium1
f43b70f25d fix(compat-audit): treat with suppress(Exception) as an import guard
contextlib.suppress swallows a failed import exactly like a non-raising except
arm; the audit marked such imports bare, so a module only ever imported under
suppress(Exception) by updaters could never be deleted.
2026-10-02 04:39:28 -07:00
teknium1
6174e342e1 fix(tests): a nested test runner must not inherit the outer runner's slice
The runner's own probe tests start runners from inside a test process,
which inherits HERMES_TEST_SLICE from the CI slice job. Slice 1/2 then
ran half of each probe's file set and slice 2/2 ran none of a one-file
probe ("probe never wrote handoff file"). Read the slice once and drop
it from the environment.
2026-10-01 23:49:20 -07:00
teknium1
093f8d4480 ci: run the Python suite and the macOS lane as two slices each
The full suite was the last job to finish in 33 of 43 CI runs since it
moved from a 96-core to one 32-core runner (p50 666 s). That runner is
~90% packed, so only more machines help, and one bigger box is poor
value: on 96 cores the same files took 25-50% longer each and the suite
ran ~1.8x faster for 3x the price. Two 32-core slices scale linearly
(simulated from per-file CI timings: 262 s / 252 s vs 514 s on one box).

macOS (free 3-core runner, ~7.5 min) would become the slowest required
job once Linux splits, so it runs as two slices on the same free runner.

--slice now partitions from the checkout alone (sorted round-robin):
each slice job restores its own duration cache, and a main push saving
a newer cache between the two restores made timed LPT compute different
partitions per job, silently dropping or double-running files.
Round-robin lands within 5 s of timed LPT at this suite size. Each slice
saves its own cache key on main; the runner keeps the restored entries
for files outside its slice, so the cache stays complete.
2026-10-01 23:49:20 -07:00
teknium1
7f7e1ab402 fix(desktop-update): any failure after the banner keeps a killed update failed
The 124 remap was vetoed only by "Update not complete", but the restart
and verify phase prints other failure lines after "Update complete!":
"Update incomplete — …", "✗ <unit> failed to come back/stay running
after restart", "verification incomplete". Any of them now keeps the
watchdog kill a failure. Only text after the last banner is inspected,
and ✗ is matched as \u2717 because Windows PowerShell reads the BOM-less
script as ANSI.

Reported by @unsupportedpastels in review.
2026-10-01 22:35:29 -07:00
teknium1
91e8f7595d refactor(desktop-update): keep only the reachable 124-after-completion remap
The Windows hand-off's idle watchdog is the only producer of exit 124 on
main (posix.sh has no watchdog and `hermes update` never exits 124), so
the posix remap was dead and goes. The retry-state helper and its
self-test arm collapse into one Resolve-HermesUpdateOutcome check, now
proved end to end through the real hand-off instead: a fake update that
prints its banner and parks silently past a 3 s ceiling surfaces as a
verified success with the gateway fleet restored.

The remap also refuses a run whose output already carries the
"Update not complete" verdict, which supersedes the banner.
2026-10-01 22:35:29 -07:00
Brooklyn Nicholson
1b0d8aa3d5 fix(desktop-update): completed update killed with exit 124 is terminal, never re-run (#96205)
Port of #96340 (Finn763) onto the current legacy-scoped retry gates,
narrowed to the watchdog sentinel only: exit 124 + the "Update complete!"
marker maps to success so the hand-off verifies and relaunches the new
build instead of re-running the completed update (the 70+ minute popup
retry storm). A plain exit 1 after the marker is the stale-gateway
verdict (hermes_cli/update_receipt.py) and must keep failing.

As first written this also added a posix.sh gate and a retry-state
helper with a -SelfTestRetryState arm; the next commit drops the posix
gate (nothing there exits 124) and replaces the helper and self-test
with one end-to-end test through the real hand-off.

Co-authored-by: Finn763 <165816600+Finn763@users.noreply.github.com>
2026-10-01 22:35:29 -07:00
teknium1
d802c47238 docs: one complete plugin catalog submission guide, synced with the README rules
Submitters only saw a stale 5-point summary on the catalog page (it even listed
a 'Released' requirement nothing enforces), while reviewers applied ten README
rules plus criteria written nowhere public (credentials, approvals, disclosure,
compatibility metadata, forks of bundled plugins).

- plugin-catalog/README.md: rules 11-15 write down those review criteria; the
  rule list sits between admission-rules markers.
- New developer-guide page 'Submitting to the plugin catalog': before you
  submit, the PR flow, the full rule block (verbatim mirror), pin updates,
  delisting vs removal, asking for a hook.
- The /plugins 'Submit' links and the user-guide section point at it.
- tests/website/test_catalog_rules_mirror.py fails when the two rule blocks
  differ; classify_changes makes a README-only edit run it.
- AGENTS.md, plugins/AGENTS.md and CONTRIBUTING.md reference both files.
2026-10-01 21:58:11 -07:00
teknium1
b909c01d07 fix(ci): scope the catalog data-only guard to entry PRs; diff the merge commit shallowly
- Run the data-only guard once, inside the changed-entries step of the
  pinned-source job, and only when an entry YAML changed. README, policy,
  docs and CI PRs that touch plugin-catalog/ without an entry no longer go
  red.
- Allow contributors/emails/** beside plugin-catalog/**: the attribution
  fix we push onto catalog PR branches is data the gate never executes
  (29 of the last 134 merged catalog PRs carried one).
- Diff GitHub's merge commit against its first parent with fetch-depth: 2
  instead of a full-history clone plus merge-base. The structural job,
  which only validates the whole directory, goes back to a shallow
  checkout with no diff at all.
- `subdir:` with no value means the repo root in both the parse step and
  the structural validator, matching how the runtime loader has always
  read it.
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
Brooklyn Nicholson
5690810779 fix(build): await the scratch-dir removal; drop an unused test import 2026-10-01 00:15:05 -05:00
Brooklyn Nicholson
dde4bc7bb7 fix(update): serialize source-completion tails on the shared update lock; skip Windows file-manager metadata in freshness hashing (#123376, #122803)
WIP for the freshness-guard-os-metadata unit: the UpdateLock claim in
complete_source_checkout plus its tests are complete but unrun; the
Thumbs.db/Desktop.ini skip predicate is in but its test rows are not yet
extended, and the rmTree ENOTEMPTY test has not been written yet.
2026-10-01 00:15:05 -05:00
PRATHAMESH75
64eca050ec fix(build): skip macOS Finder metadata in build freshness hashing (#122632)
Opening the checkout in Finder mid-build lets .DS_Store, AppleDouble ._*
sidecars or .localized land inside a hashed input dir (apps/desktop, apps/shared,
assets, scripts/build). treeHash then sees a new input and recordProduct throws
"Build inputs changed during compilation; retry the build", so `hermes update`
rebuilds the desktop app, fails closed, and never prints Update complete.

Finder metadata is never a build input, so skip it as a baseline in treeHash —
covering source, output and prepared trees uniformly rather than only the source
predicate. A real source change is still detected and still names nothing new to
hash, so clean checkouts (CI) hash byte-identically.
2026-10-01 00:15:05 -05:00
Brooklyn Nicholson
4091ef8967 fix(build): absorb Finder's ENOTEMPTY scratch-dir removal race (#122803)
macOS's Finder/Spotlight can drop a fresh .DS_Store (or xattr) into a
directory while we're removing it, making rmSync throw ENOTEMPTY on the
first pass. That file is cosmetic garbage — retry the removal so the
race can't abort an otherwise-clean product build.

Salvaged from #123078 (its freshness.mjs hunk is subsumed by the broader
metadata skip; the unrelated CI-workflow deletions in that PR are not
included).

Co-Authored-By: dlshumway <85191357+dlshumway@users.noreply.github.com>
2026-10-01 00:15:05 -05:00
Halldrix
eec5d2af64 fix(build): record the baked stamp clock, not the live one (#123308)
write-build-stamp.mjs rewrites the stamp's `builtAt` clock on every build, so the
whole-file hash moved under a receipt that had just recorded it. Two builds
overlapping on one tree failed with "Build inputs changed during compilation" for
a difference the build machinery itself made, and the caller turned that into
SystemExit(1).

Hash the stamp's provenance identity instead, key-sorted so a cosmetic
re-serialisation is not a change. The clock is excluded from that comparison but
recorded as `stampClock` — the value the bake actually embedded, captured from
the same read that produces __HERMES_INSTALL_STAMP__ — because packaging copies
the live stamp into the bundle after the receipt is written. Re-reading the stamp
at record time would certify a clock the output never contained, and
productCurrent would then report a bundle current whose packaged clock disagrees
with its baked main, which is exactly what detectBundleSwap must never do.

recordProduct still compares the prepared inputs, so a clock-only restamp cannot
throw. productCurrent additionally requires the recorded clock to match, so a
restamp between bake and packaging is a rebuild rather than a silent divergence.
A receipt predating the field compares by hash alone until the next build
rewrites it; the release packaging path never consults productCurrent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-01 00:15:05 -05:00
Halldrix
ff28c475e3 fix(build): hash the desktop stamp's identity, not its build clock (#123308)
write-build-stamp.mjs rewrites `builtAt` on every build, and build.mjs runs it
before compilation, so the desktop build rewrote a file its own receipt hashed
as a prepared input. Two concurrent `hermes desktop` builds therefore made
recordProduct() fail with "Build inputs changed during compilation" on a change
the build itself caused, and the caller turned that into SystemExit(1).

Hash the stamp's provenance identity instead of its raw bytes. builtAt is the
only field that moves between two stamps (the tree documents it as
"differs on every rebuild"), and it is a build clock, not provenance: the baked
output changes when commit/payload/tag change, never when time passes. A
non-JSON stamp still falls back to the whole-file hash, so content keeps
deciding freshness and only the clock is ignored.

Unlike b3618bda35, the stamp cannot simply leave the hash set:
bundleElectronMain bakes its bytes into electron-main.mjs, and
bundle-swap.test.ts pins "same commit rebuilt (builtAt moved)" as a real swap,
so freezing the clock would break a committed test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-01 00:15:05 -05:00
Siddharth Balyan
09681e7959 fix(release): --branding stable works before stable has a channel record (#129541)
`--branding stable` copies the identity from the published stable channel
record on R2, and refused when there was none. Production has no
releases/channels/stable.json yet: the channel pipeline merged after the
last stable release (v2026.9.24), and no stable release has published
through it since. So every stable-branded channel build failed at
allocation ("none is published"), including the first real dispatch.

When the stable record does not exist, use product_identity(), the
identity channel_publish already holds every stable release to. A stable
record without a head still refuses, so once the first stable release
publishes, its record is the only authority again and this path stops
running by itself.

The allocation step needs node for product_identity(), so the workflow's
identity-reader setup also runs for branding == 'stable'.
2026-10-01 01:30:24 +05:30
Siddharth Balyan
86f54bfdaa Flavored desktop builds no longer stamp themselves dirty (#129540)
* fix(bundles): write the build stamp before flavored icons land

A flavored build (commit, canary, preview channel) copies its icons over the
committed apps/desktop/assets before packaging. write-build-stamp ran after
that copy, so its git check saw modified tracked files and stamped the
bundle dirty, and the Windows smoke refused it (stamp.dirty: true != false),
which keeps a channel from promoting the build. Stamp first, in both the CI
driver (scripts/bundles/desktop.py) and the source build (build.mjs). The
stamp reads no artwork, so its content is unchanged.

* test(bundles): pin the build stamp ahead of the flavored icon copy

buildSourceDesktop copies flavored icons over committed files in
apps/desktop/assets. write-build-stamp asks git whether tracked files
changed, so it has to run while the committed bytes are still there. The
test records what the stamp step sees through the injectable run seam.
It fails on the old order (sees the flavored icon) and passes on the new one.
2026-10-01 01:30:18 +05:30
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
ethernet
13dbe260aa feat(release): --branding stable builds a channel commit as the regular app
A preview channel always minted its own side-by-side identity (Hermes NAME,
ai.hermes.channel.h<token>), and a plain --build-commit carries
Hermes Agent <sha7> with a red icon, so there was no way to ship an exact
commit under the official name, icon and package ID.

--branding stable (with --channel) makes channel creation copy the identity
from the published stable channel record instead of reserving a new token.
Branding is fixed at creation like the rest of the identity: re-dispatching
with the other branding is refused, as is stable branding before a stable is
published or in a disposable namespace. --branding without --channel is
refused rather than silently falling back to a sha-branded one-off build.

The workflow gains a `branding` input, forwarded to the allocation step and
the dispatch log so the printed release.py replay matches what was sent.
2026-09-30 09:19:35 -04:00
JoaoMarcos44
16b214e1a1 fix(config): make destructive replacements explicit
atomic_config_write() now refuses deletion-by-omission at any mapping depth:
atomic_roundtrip_yaml_save treats keys absent from the payload as deleted
recursively, while the old docstring described the call as a merge, so a
partial dict could silently erase unrelated config (#125107). A new
atomic_config_replace() names the deliberate full-state replacement contract
explicitly, and the owners that intentionally remove state (save_config,
provider reset/switch, credential mirror scrub, doctor key migration, profile
channel stripping, TUI full-state saves) route through it. Additive writers
stay on the guarded atomic_config_write.

Salvages #125152 (identical patch, cherry-picked; conflict in
_write_user_config resolved to pass atomic_config_replace through the new
telemetry wrapper) and carries the executable bit main expects on
scripts/check_config_yaml_writers.py.

Fixes #125107

Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
2026-09-29 20:24:19 -04:00
ethernet
e336fbcf79 fix: release cut output waits for its CI run and leads with the next action
The cut printed before GitHub listed the dispatched run, so it almost always
said "the run is not listed yet". It now polls up to 30s for the run, and when
none lists it links the stable-release workflow page. The copy is shorter:
Release CI, the skipped-tests warning (bold on a tty), the draft link, and the
publish command on its own line.
2026-09-29 15:12:04 -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
ethernet
36699ac97a Merge pull request #128156 from NousResearch/fix/desktop-flavored-icons-clean-checkout
Desktop release builds stop leaving their flavored icon render in the checkout
2026-09-29 13:08:09 -04:00
ethernet8023
4bc38bceec fix(desktop): hand the admitted checkout back after flavored icon staging
electron-builder resolves every piece of packaging artwork from the workspace
— the app icon, the icon.ico extraResource, the MSIX logo set before-build
stages into build/appx, and the exe identity stamp — so a flavored canary or
commit build copies its rendered icon set over apps/desktop/assets before
packaging.

Those files stopped being gitignored in 4b7229d612, which turned a write that
was invisible into 15 dirty paths. The source custody check then refuses the
checkout twice: inside the same build, at the check that used to sit after the
copy, and on every later build from the first check on a now permanently dirty
tree.

Hold the render at the workspace path for the packaging window only and
restore the admitted files afterwards, whether packaging finished or failed.
The final custody check now asserts the build handed the checkout back.
2026-09-29 12:29:47 -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
74163fa18c fix(desktop): pass release epoch to native version subprocess 2026-09-29 10:55:18 -04:00
kshitijk4poor
596678d665 refactor(update): tighten the partial-clone heal after review
- _deepen_shallow_repo marks packs in its finally without a git config probe
  that could itself raise and turn a finished unshallow into False; markers
  are inert in a clone without a promisor remote.
- Installers warn instead of aborting the stage when a marker cannot be
  written, matching the Python helper.
- Docs: the manual commands follow HERMES_HOME, and the missing-object
  backfill feeds IDs through `fetch --stdin` so a large list cannot exceed
  the Windows command-line limit.
- Test and docstring comments describe the contract, not the incident.
2026-09-29 15:24:15 +05:30
kshitijk4poor
7b20b72269 fix(install): installer rerun heals a partial clone stuck on the pack-objects crash
An install whose updater dies on the git 2.53+ pack-objects BUG (#124272)
never fetches the release that heals it: the fetch runs in the installed
code. The installer rerun (install.sh / install.ps1, served fresh) is the
path that runs new code, so when origin is a promisor remote it now marks
the checkout's unmarked packs before its existing-checkout fetch. Marking is
idempotent and never rewrites objects, so it runs up front rather than as a
retry that would first print a failed-fetch report.
2026-09-29 15:24:15 +05:30
teknium1
9b7db3105b fix(telemetry): fewer rows per busy day and a smaller local store
A volume simulation through the real store showed two costs worth cutting.

Rows: hermes.task_run.finished carried duration, retry, model-call and
tool-call buckets on top of outcome/end reason/surface, so almost every task
became its own row; hermes.tool_call.count did the same with latency and
retry buckets. The terminal rows now keep only the fields they are filtered
by, and the same end events feed two small split counters:
hermes.task_run.duration (surface, outcome, duration, retries) and
hermes.tool_call.latency (tool category, latency, retries). Per-task call
counts already ride on hermes.task_cost.count. v3 still accepts the v2 field
sets of both counters so rows recorded before an upgrade drain.

Local store: every package lived three times for 30 days (counter rows, the
payload column, a pretty-printed outbox file). Outbox files are now compact
JSON, and once the ingest has accepted or refused a package the database
drops its copy of the body; the file stays as the local history the docs
promise, and pending packages keep their body for resends.

Measured (simulated day, skewed draws, before -> after):
  heavy    2,788 -> 2,347 rows, 21.0 -> 17.7 KB on the wire, 102 -> 44 MB local over 30 days
  extreme  6,561 -> 5,047 rows, 46.0 -> 33.6 KB, 272 -> 102 MB
  typical    663 ->   650 rows,  7.1 ->  6.8 KB,  24 ->  13 MB
2026-09-28 12:43:03 -07:00
teknium1
0b6ac0b622 fix(telemetry): live relay smoke no longer expects adoption from an agent-created skill
The signals fix (M15) stopped counting Hermes' own skill creation as user
adoption, so the smoke's agent-created skill records no feature_adoption
row; the exact name sets now assert its absence.
2026-09-28 12:43:03 -07:00
teknium1
d6e4e33d7d fix(telemetry): live relay smoke expects the v5 efficiency and adoption rows
The smoke's exact metric-set checks (SQLite store and export packages) were
never updated for v5: every attended CLI turn now also emits task_cost,
tool_output_truncation, tool_overhead and tool_enabled_unused, and the skill
lifecycle step latches feature_adoption(skills_created). A correct run failed
with "Unexpected SQLite counters", so the documented live gate could no
longer catch a real regression. Both sets now include the five metrics and a
shared _validate_v5_rows asserts their dims in the store and the packages
(task_cost provider/model `custom` for the canary model, api_calls 2,
tool_calls 1; read_file truncation; file toolset used; tool_overhead on cli).

Live smoke (loopback fake OpenAI server, no model spend):
  before: rc=1 AssertionError: Unexpected SQLite counters
  after:  rc=0 "Hermes -> NeMo Relay shared-metrics smoke test passed"
Sabotage: expecting the canary model id in task_cost is caught.
2026-09-28 12:43:03 -07:00
teknium1
c5555efca8 test(smoke): expect the per-response model_reply_issue counter
Every primary model response now records hermes.model_reply_issue.count
(issue=none for usable replies), so the smoke turn's two scripted replies
add one row with value 2; the exact-metric-set assertions must include it.
2026-09-28 12:43:03 -07:00
teknium1
b1e9c2a5a1 test(smoke): expect v4 tool-quality, context-peak and startup counters 2026-09-28 12:43:03 -07:00
teknium1
ccd1a274e8 fix(telemetry): user-named providers and models never leave the machine; milestones use the owning profile
Independent review of the v3 metrics found:

- Provider fields were shape-checked only, so `custom:<config key>` (and
  any unshipped provider id) reached setup.completed, install.snapshot,
  model_switch, fallback, model_tokens and model_route. Providers now pass
  only when Hermes ships them (auth registry, overlays, model catalog,
  aliases, cached models.dev ids); everything else reads `custom`. A custom
  or loopback provider's model id reads `custom`, as does any model id that
  looks like a path or URL. Two existing tests asserted the old export of a
  custom endpoint's model id and an unshipped provider; they now assert the
  collapse, and the smoke run treats the model canary as prohibited.
- Install milestones read the install age on the Relay thread, which has no
  profile binding, so a multiplexed profile got the launch profile's age.
  The subscriber captures its home at construction.
- Gateway sessions retired by the store (auto-reset, /new, resume) now close
  their metrics session at the route transition instead of at shutdown.
- The dashboard MCP add records its install after leaving the config lock.
- Compression and gateway slash counting helpers move into
  shared_metrics_events so the >2k-line files barely grow.
2026-09-28 12:43:03 -07:00
teknium1
3d62ae2233 feat(telemetry): decision-data docs, consent copy, smoke coverage and install-age for new installs
- Docs: a Decision-data metrics section mapping each metric to the product question it answers;
  skill-load and snapshot paragraphs describe the new public-name and install-age fields.
- setup consent copy names the new data classes (session length, token totals, command and
  catalog names, bucketed setup counts).
- The real-CLI smoke now asserts the session summary, token sums, TTFT bucket and milestones in
  both the SQLite store and the exported, schema-validated package.
- A profile with no sessions yet is a brand-new install (lt_1h), not unknown: the first task's
  milestone and the first snapshot fire before state.db has a session row.
- Two invariant tests: session summaries + once-per-install milestones; token sums per model and
  auxiliary task with private task names collapsed.
2026-09-28 12:43:03 -07:00
teknium1
b8732b768e feat(telemetry): shared metrics v3 with failure classes, tool usage, platform and daily snapshot
The fleet rollups could say that tasks and tools failed, but not why, on which
model, on which messaging platform, or which features installs actually use.

- model_route rows gain call_role, outcome and error_class (the classifier's
  FailoverReason for the last failed attempt; a success row keeps the error it
  recovered from).
- task_run rows gain platform (built-in gateway platform, plugin, or none);
  task_run.finished gains failure_class from a closed set.
- new hermes.tool.usage.count: tool_name (only names from the static
  toolsets.TOOLSETS snapshot; mcp/plugin otherwise), outcome, error_class.
- new hermes.install.snapshot, latched to once per rolling 24h like
  client.active: memory provider plus bucketed counts of MCP servers,
  plugins, skills, cron jobs and profiles; no names.
- package schema v3; v2-shaped pending counters still validate and drain.
- consent copy and developer docs list the new categories; the relay smoke
  asserts v3 shapes and disables title generation (its third model request
  broke the smoke's request-count check on main).
2026-09-28 12:43:03 -07:00