Maintainer ruling: only Hermes and only its packaged package managers
(uv/pip/node/npm) are ever used; no PATH fallback when the managed tool is
missing, no "prefer the user's if new enough".
- hermes_constants.find_node_executable: node/npm/npx resolve to PM's
installed copy or None. Every caller already pm.ensure()s on None, so a
missing runtime is now provisioned instead of silently borrowing the
user's Node (native-addon ABI / npm cache mismatches).
- agent/lsp/install._install_npm: pm.ensure('npm') when PM npm is absent,
instead of failing over to whatever npm is on PATH.
- gateway._append_node_dir_for_service: stop baking the invoker's PATH node
dir into generated systemd/launchd units.
- main_install_repair._resolve_node_runtime_npm: drop the PATH re-scan for
another npm.
- source_build.source_product_current: run the freshness reader only with
PM's node.
- doctor: Node/npm rows and npm audit use PM's copies (Termux APT distro
keeps its system Node).
- install.sh ensure_uv / install.ps1 Get-Uv: always stage the pinned uv
artifact; delete the "uv on PATH if new enough" developer shortcut.
Computer use and browser use are meant to work out of the box. The PM rewrite
(3d12e86ef1) and the MSIX installer rework (47f4ab3a17) dropped the
install-time cua-driver fetch (7060ac7bed) and the Browser Use CLI install
(baa6b2e34d). On a fresh install the computer_use check_fn therefore stayed
False, so the tool never reached the model and its lazy ensure could not fire,
and browser_exec quietly fell back to the built-in tools.
- cua-driver is a default PM package, so the installers, a bare
`hermes pm install` and `hermes update` carry it on every target it builds
for. Adds the missing Android gap (the lock has no bionic artifact).
- The shared default-tool step, which the installers (via source completion)
and `hermes update` both run, provisions the Browser Use CLI for the default
and explicit Browser Use backends. `--skip-browser` declines it along with
agent-browser, and `off`/Camofox never use it.
- Installers regain --skip-computer-use / -SkipComputerUse (recorded as
`--without cua-driver`).
- The update message stops calling every default "browser tools".
Relaxes the fail-closed install gate from #124037 per teknium1 review
2026-09-27 (known_issues informational; guard belongs at mode-selection seam):
- dashboard_install_plugin: no longer refuses known_issues entries — the text
flows into the result warnings plus a machine-readable "known_issues" key so
the UI can show it; the memory-provider migration paths (migrate_all_homes /
recover_at_startup via _install_into) install hindsight again in any mode.
- cmd_install: prints the yellow "Known issue:" lines but drops the TTY
requirement and the y/N confirmation; non-interactive installs proceed.
- entry_capability_summary: includes "Known issues: ..." so install prompts and
the catalog UI surface the text.
- scripts/validate_plugin_catalog.py: register known_issues in KNOWN_KEYS.
- tests: keep parse round-trip + install-summary display; drop the live-catalog
prose pin (test_live_catalog_hindsight_declares_known_issues) and the
zoneinfo import hack.
- contributors: map 5318happy@users.noreply.github.com -> happy5318.
Process.Kill() on PS 5.1 kills only the stub; its post-install children kept
%TEMP%\hermes-git-bootstrap-<PID> held past the finally cleanup. taskkill /T /F
reaps the tree. The single-wrapper-dir flatten is gone: the sha-pinned
PortableGit roots cmd\git.exe directly (verified against the real archive).
CI run 36189416163 failed after "git: unpacking": executing the cached
fetch-<sha> PortableGit PE in place left it handle-held (Defender
on-execute scan / the stub's RunProgram child chain) past pm's ~2 s
_remove_entry retry, so download cleanup raised WinError 32. Review
(teknium1, 5 threads) adds the rest:
- Git.unpack now copies the artifact into a .sfx-* dir beside the
staging tree and executes the copy; pm's .staging-* teardown
(ignore_errors) owns that path, so any hold lands on a disposable
path and the cache dir only ever holds read handles
- refuse off-Windows with the cross-host trade-off stated instead of a
raw PermissionError; documented in package-management.md and the PR
- the extractor is a GUI-subsystem stub that is silent under -y: error
messages now carry the exit code and the usual causes (disk full,
path length, antivirus) instead of promising captured output; the
docstring no longer claims "no GUI" and records that the stub shows
an Extracting window and runs the vendor post-install
- install.ps1: WaitForExit(600000) + Kill() mirrors pm's timeout=600,
Fail reports the exit code and the silence
- tests: the OS-refused-exec assertion and the not-in-text change
detectors are replaced by subprocess-argv invariants (scratch copy
location, exit-code message, off-Windows guard before any execution)
Related to #122512
(cherry picked from commit adaf76a286)
Windows 10 boxes whose System32 tar.exe cannot run the bzip2 filter die
at stage=prerequisites with "unable to run program bzip2 -d" while
extracting the pinned Git-2.53.0.3 tar.bz2 (#122512). Repin git for
both win32 targets to git-for-windows' PortableGit self-extracting 7z,
which carries its own extractor and the bundled usr/bin/bash.exe:
- pm/lock.json: new artifact urls + sha256 (the pin authority)
- scripts/install.ps1: generated fragment regenerated; Get-PinnedGit
downloads the SFX and waits on it explicitly (the stub is a
GUI-subsystem exe, so PowerShell's & does not wait); the System32
tar invocation and its msys symlink excludes go away
- pm/packages.py: Git.fetch_url/Git.unpack run the self-extractor
after the sha256-verified download
- pm/store.py: drop the now-callerless git_msys branch of extract_tar
- tests: RED->GREEN test runs the real Get-PinnedGit against a
bzip2-less System32 tar.exe stub with no bzip2 on PATH; the three
obsolete tar-contract tests and the install.ps1/PM msys-links parity
test are replaced by a no-external-decompressor contract test
Closes#122512
(cherry picked from commit 4915304213)
The installer's re-run path fetched `origin <branch>` by name, then checked
the branch out and fast-forwarded to origin/<branch>. On a checkout an older
installer made with `--depth 1 --single-branch --branch <tag>`, the remote's
only refspec maps the tag: the fetch wrote FETCH_HEAD but no origin/<branch>,
and `git checkout main` failed with "pathspec 'main' did not match".
Fetch by explicit refspec (as `hermes update` now does), and when there is no
local branch yet create it at the fetched tip — checkout's own branch guess
ignores remote refs the configured refspec does not map. The remote's fetch
config is left as the user has it. Same change in install.sh and install.ps1.
Review fix-ups on top of the re-pin:
- pm/lock.json: keep "version": "9.0.1". pm keys the store entry on
`ffmpeg-<version>-<target>` and reinstalls on the artifact sha alone
(pm/install.py::_identity / _entry_current), so the sha change already
re-fetches the four BtbN targets. Bumping the label would also rename the
macOS (martin-riedl, still 9.0.1) and Termux entries and re-stage unchanged
bytes on every existing install for no reason.
- tests/pm/test_ffmpeg_pin_liveness.py: removed. It HEADs live GitHub URLs
from the unit lane, and BtbN prunes dated autobuild tags after ~14 days
(autobuild-2026-09-10-15-31 is already gone), so the test turns red for
every PR on ~Oct 11 by construction. Upstream rot is what
archive-inputs.yml (sha256 mirror on merge) and #122433 (re-pin on fetch
failure) are for.
- website/docs/user-guide/windows-native.md + install.ps1 header: say that
-SkipSetup is accepted as a deprecated alias for -NonInteractive instead of
claiming it is rejected.
(cherry picked from commit def90331c2; ffmpeg lock/liveness hunks dropped, superseded by #125468)
The staged-installer rework (92686159d1) dropped the -SkipSetup switch
from install.ps1, so wrappers written against the old spelling
(hermes-desktop -SkipSetup -NonInteractive ...) die at parameter
binding with NamedParameterNotFound. Accept it as a deprecated alias
that folds into -NonInteractive.
pm/lock.json pinned ffmpeg artifacts to the dated BtbN autobuild tag
2026-09-10-15-31; BtbN prunes old dated tags, so fresh installs 404 on
GitHub and the sha256 mirror has nothing to serve (403). Re-pin all four
BtbN targets to a live tag (autobuild-2026-09-27-13-04, n9.0.2-12) with
digests taken from the GitHub release API.
(cherry picked from commit 91371d0b9a; ffmpeg lock/liveness hunks dropped, superseded by #125468)
install-e2e.yml only ran on the clock, so nothing in front of a merge
installed a release and updated it on a real OS. A pull_request trigger,
path-filtered to the install/update surface (derived from 60 days of
update/install/pm commits), runs the new `pr` route of
generate-e2e-matrix.mjs with only the newest release tag sampled:
linux installer-script -> hermes-update (newest release -> PR)
linux installer-script -> hermes-update (PR -> NEXT)
windows installer-script -> hermes-update (PR -> NEXT)
macos installer-script -> hermes-update (newest release -> PR)
The bundle-manifest validation job is skipped on PRs (bundled legs need
dispatch-only manifests). The full matrix stays on schedule and release.
The Nix agent derivation builds scripts/build/*.py from a fileset that
does not include pm/, so importing pm.store.ALL_TARGETS there failed
nix flake check with ModuleNotFoundError. Extend the local target
regex with linux-(x64|arm64)-musl instead.
PM's musl Node is the unofficial-builds musl archive, which links the
system libstdc++. On a stock Alpine (bash, git, curl) node and npm then
fail staged verification with raw relocation errors after the clone and
downloads. Check in the prerequisites stage and name the package.
install.sh consulted ldd before the ELF interpreter while pm/store.py's
_is_musl_libc reads the native userland's ELF interpreter first, so a
glibc ldd (secondary toolchain, gcompat) could make the bootstrap stage
a different libc than PM later resolves. Read /bin/sh (then /bin/ls)
PT_INTERP first in both; ldd and the loader glob are fallbacks only.
Fall back to the musl loader if ldd cannot identify libc, while honoring
an explicit GNU libc report on hosts with a secondary musl toolchain.
Cover both paths against the pinned uv URL and digest.
Refs: #123682
Skip glibc-linked FFmpeg on musl in both default install and update roots.
Select musl uv during the standalone shell bootstrap, and let the native
userland resolve libc before bootstrap Python build metadata.
Add regression checks for the closure, target precedence, and installer pins.
wire_shell_path's existing-setup regex required a character before PATH=, so it missed bare assignments such as Fedora's ~/.bashrc ( PATH="$HOME/.local/bin:$HOME/bin:$PATH") and Debian's ~/.profile. The installer then appended its own line to .bashrc, .profile and .bash_profile, and because Fedora's .bash_profile sources .bashrc, login shells got ~/.local/bin on PATH several times.
Match bare assignments too, and make the appended line a no-op when PATH already contains ~/.local/bin.
npm_execpath can point through a symlink, and the manifest sits beside
the resolved CLI, never beside the link: resolve the realpath before
looking for package.json. Layouts without a readable manifest now fall
back to the pre-fix child probe instead of aborting with ENOENT.
Move the regression test to tests-js/node-deps.test.mjs (the module's
own suite) and cover the symlinked-execpath and fallback lanes there.
The npm version probe spawned node-under-node before the reuse
short-circuit, so on Windows a Job-Object EBUSY spawn failure aborted
the whole dependency preparation even when the install was already
complete — leaving the pending-completion marker behind and turning
every launch into the same doomed completion pass (#123933).
npm's own package manifest states its version without any process
creation, so the probe lane can no longer fail: the install-receipt
key keeps its exact value, engine checks still run, and completed
installs reuse with zero spawns.
conhost blocks every write to a console while a selection is active. The
hand-off replays buffered child output through Write-HandoffLog after
`hermes update` exits, so a selection in a visible hand-off console held the
result, marker cleanup and relaunch until the user pressed Esc (#103222).
Turn QuickEdit off on the console input buffer for the run (restored on
exit), and skip the console echo while a selection is in progress. The log
file still gets every line.
A failed receipt check after a zero-exit update means the updated Desktop build
could not be read, not that the install is damaged. The old copy told users to
repair the installation and review antivirus quarantine, which is destructive
advice for a healthy install (#107685). Say what happened and give the one
non-destructive recovery step.
electron-builder names the unpacked output <os>-unpacked on x64 and
<os>-<arch>-unpacked elsewhere (linux-arm64-unpacked, win-arm64-unpacked).
install.sh desktop_product_present and install.ps1
Test-DesktopProductPresent only listed the x64 names, so a rerun on an
ARM64 desktop install skipped the desktop rebuild and left a bundle built
from the previous code. List every unpacked dir main_desktop.py already
resolves, in both installers.
electron-builder names the unpacked dir linux-unpacked on x86_64 but
linux-<arch>-unpacked on every other arch (linux-arm64-unpacked is what
ARM ships). The gate hardcoded the x86_64 name, so a healthy ARM install
false-gated as "skew" on EVERY update, telling the user to reinstall an
app that was already correct. The ostree/symlink half was fixed earlier
(d3b090a3); this lands the remaining arch-dir half.
Resolve the unpacked dir the running binary actually lives in by scanning
the release dir's linux*-unpacked candidates (canonicalised both sides
before the compare, so the symlink fix's semantics are preserved, with a
first-found fallback so foreign targets keep gating as skew.
Tests drive the real --self-test-gate entry point; on BSD-readlink hosts
a PATH shim provides GNU so the gate logic runs everywhere
(the existing linux_only symlink matrix covers -dependent paths).
Co-authored-by: C-Est-Dept <cestdept@example.com>
Co-authored-by: Sahilvishnaliya <sahil@example.com>
EOF
)
Linux uses apple-touch-icon.png as the window icon and Nix requires it to
match the full-bleed launcher icon, so keep it full-bleed and point the
dev-only app.dock.setIcon at assets/icon-mac.png instead.
Dev runs replace the Dock icon with public/apple-touch-icon.png, which the
generator rendered full-bleed, so it drew ~24% larger than its Dock
neighbors. Render it from the mac-grid master like the icns targets.
On the 824 grid the plate matches peers, but the girl inside a white tile
with a ring read small; scale her 1.12x about the plate center for every
mac target.
Skill slashes that miss the 4018 gate print the loading banner and park
the prompt on unread _pending_input. Refuse before process_command, and
return command.dispatch's skill payload or a hard error when the skill
scan raises so fail-open cannot drop the turn.
gh v2.99 gained a native --attach flag for issues, PRs and comments, so
the custom trusted-publisher chain (gh-image extension + GH_IMAGE_SESSION_TOKEN
workflow_run job + attachment-upload script) is superseded.
Remove:
- .github/workflows/publish-e2e-evidence.yml (workflow_run publisher)
- scripts/ci/publish_e2e_evidence.py + its tests
- e2e-evidence-* artifact upload + evidence staging in e2e-desktop.yml /
e2e_screenshot_status.py, incl. the 'inline evidence is publishing...'
marker placeholder in the CI review comment status
Keep: the review-comment screenshot/diff counts and artifact links produced
by e2e_screenshot_status.py.
The setup stage installs the gateway service through
ensure_gateway_service. On Windows that asks the start-now, Scheduled
Task and UAC questions. The gateway stage then ran `hermes gateway
install`, which asked them all again.
`gateway install --if-missing` does nothing when a service is already
installed. Both installers' gateway stages use it, so they ask only
when setup did not install the service.
Steps inherited the hand-off console's stdin, so any step that asks a
question blocked forever. Its prompt went to the captured stdout, which
is shown only after the step exits. `gateway start --all` did exactly
this: it saw an interactive console and asked "Install it now so the
gateway starts on login?". The update stopped after `hermes update exit
code: 0` and never relaunched the app.
Steps now read NUL. Prompts see a non-interactive stdin and take their
defaults. The working-directory self-test also checks that a step's
stdin is not a console, and a new test runs it under a real console.
powershell -Command joins trailing argv into the script text, so $args
was never populated and the path became a stray token (ParserError).
An env var needs no quoting.
record() looked up stamp['identity']['windowsExecutableName'], a key no
stamp writer emits, so every Windows record on a runner resolved no
executable and failed. The built package's Application/@Executable
names the exe; match it in the unpacked dir only, which also skips
before-pack's .bak rollback copy.
User installs failed with 'resvg-py is missing' because the web/desktop
source builds rendered icons on whatever python was on PATH. The default
brand outputs are now committed; source_build, apps/desktop build.mjs and
the npm/docusaurus pre-hooks consume them directly. Flavored release
bundles (canary/commit) still render into their own product dir.
icons-freshness-check now regenerates and fails on any byte diff.
A non-elevated Windows ARM64 install threw "run setup-hermes.ps1 in an
Administrator PowerShell" whenever Visual Studio ARM64 C++/Clang were
missing. Interactive runs now launch the signed VS installer through a
UAC prompt; CI, ssh and scheduled runs keep the explicit instruction.
The flag used to exit 1 as retired. Now that PM installs the browser tools
by default, it maps to `pm.cli install --without agent-browser`, which later
installs and `hermes update` honour.
Updating an old checkout ran `git merge --ff-only`, which prints a
diffstat plus create/delete-mode summary. From v2026.7.1 to today that
is ~27k lines, emitted in under a second. Hermes-Setup.exe forwards
every stage line to its window as its own Tauri event; the burst
overflows the UI thread's Windows posted-message queue (10k), emits fail
with FailedToSendMessage, and afterwards clicking Launch can leave the
installer on "Launching" without ever spawning Hermes.exe.
This is why both Windows desktop-installer@latest E2E legs from
v2026.7.1 timed out waiting for the app window after Launch, while the
same routes from v2026.9.24 (small diff) passed.
Reproduced on Windows arm64 with the production Hermes-Setup.exe and a
protocol-faithful stage script: a 32k-line burst hangs Launch in most
runs (with ~14k FailedToSendMessage warnings), 5k lines and no burst
always launch. --no-stat reduces the merge to two lines.
pre no longer builds a bare serve.git mirror, moves refs or sets
allowAnySHA1InWant. The repo-local insteadOf now rewrites the official
URLs directly to --source, so updates follow the fork's main (force-push
it to the branch under test). post still undoes the redirect via the
clone/snapshot restore.
--ref is removed: neither hermes update nor the tester's pre-branch
desktop can follow another branch through config the kit could set
without the mirror. pre now ls-remotes --source's main before writing
anything, so a bad source aborts with nothing done. The stale
global-redirect cleanup is dropped: it only matched serve.git paths.
The Git-for-Windows archive ships dev/fd, dev/std{in,out,err} and
etc/mtab as symlinks into /proc. Without symlink rights (a standard
user, not elevated, no Developer Mode) inbox bsdtar cannot create them
and exits non-zero, so the bootstrap failed with "failed to extract
pinned git archive" before pm existed.
Exclude exactly the links pm's own extractor skips (extract_tar
git_msys); any other extraction failure, e.g. a truncated archive,
still fails. A test pins the installer's list to pm's skip set.
Install E2E never saw this: GitHub-hosted Windows runners run as an
elevated administrator, which holds SeCreateSymbolicLinkPrivilege.
The conftest split moved its platform gating and live-system guard into
tests/_fixtures/. tests/conftest.py re-arms the desktop-updater lane
because it owns that skip logic; the modules it now delegates to must
too, or an edit there skips the lane that exercises it.
The PM-clean installers dropped the old "restart your terminal" /
"source ~/.bashrc" closing line, so a fresh install ended on "Run: hermes"
in a shell that could not find it.
install.ps1 now also prepends the bin dir to $env:Path. That variable is
process-wide, so under the documented `irm | iex` path (and `& .\install.ps1`)
the caller's own window resolves `hermes` immediately. When run as a script
file whose inherited PATH lacked the bin dir (e.g. `powershell -File`, a
child that cannot touch its parent), the ladder ends with one arrow line:
restart the terminal, or reload $env:Path from the User and Machine values.
Setup/gateway already launch hermes through the resolved runtime command.
install.sh prints the equivalent "open a new terminal, or run: source <rc>"
line after the ladder when the inherited PATH lacks ~/.local/bin; the
installer is always a child and cannot change its parent's PATH.
Same presentation as install.sh, in the pre-pm installer's ASCII glyphs
(-> [OK] [!] [X]; PS 5.1 reads a BOM-less script as ANSI). Invoke-Logged
wraps the git, uv, pm and source-completion calls: on a console one status
line plus logs\install.log and a failure tail; CI, -Verbose or redirected
output (the -Json stage driver) stream through Out-Host as before. It keeps
Invoke-Native's contract: $LASTEXITCODE stays the caller's to judge, and
nothing reaches the pipeline, so value-returning functions can call it.
windows-build-deps.ps1 runs under pm (installer, hermes update) with the
console as stdout, so the vcpkg clone/bootstrap, the OpenSSL port build
(patch application and all) and rustup get the same status line, logged to
build-tools\build.log; discovery notes print only when streaming. -Verbose
exports HERMES_INSTALL_VERBOSE so that child streams too.
The pm-era installer printed "[hermes]" lines between raw git, uv and pm
output. Restore the pre-pm installer's look (→ ✓ ⚠ ✗, banner on the full
ladder) and route every child command through run_logged: on a terminal it
shows one status line rewritten with the command's newest output line
(git clone phases via --progress), appends everything to
$HERMES_HOME/logs/install.log, and on failure prints the last 20 lines plus
the log path. CI, --verbose, HERMES_INSTALL_VERBOSE and a non-terminal
stdout (the Hermes-Setup --json driver, E2E transcripts) keep the full
stream, so their parsers see what they saw before. Errors stay on stderr.
release.py was 2,804 lines, 2,076 of them the frozen legacy author dict.
The map and its resolver now live in scripts/releases/: authors.py holds
the directory loader, the merged AUTHOR_MAP and resolve_author, and
authors_legacy.py holds only the frozen LEGACY_AUTHOR_MAP literal (same
1,895 entries, same order). release.py drops to 707 lines.
The contributor-check job and audit_pr_attribution.py grep the legacy
file for quoted emails, so both now read authors_legacy.py. The
importers (contributor_audit.py, add_contributor.py), contributors/README
and the tests read the defining modules. No behaviour change.
tests/conftest.py had grown to 2,343 lines, past the 2,000-line gate. Move
three self-contained topics out, unchanged:
- env_filter.py: the credential / behavioral env-var name tables the
hermetic fixture blanks;
- live_system_guard.py: the autouse live-system guard fixture, its marks
and the protected checkout roots;
- platform_gating.py: the platforms() marker evaluation used by the
collection hook.
conftest imports them instead of listing them in pytest_plugins: it is not
the rootdir conftest, and pytest fails a run that loads a non-root conftest
carrying pytest_plugins after startup. Imported fixtures register on the
conftest module under their old names, so autouse order is unchanged.
Tests that reached into the moved symbols now import the new modules.