Commit Graph

228 Commits

Author SHA1 Message Date
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
Teknium
5f7f9e143d fix(install-metrics): a Ctrl-C a stage handles no longer aborts the install; user aborts read interrupted
The ladder's INT/TERM traps wrote the receipt and exited as soon as the
foreground stage returned, even when the stage's child caught Ctrl-C and
succeeded (setup's provider picker prints "Provider setup skipped." and
carries on). gateway and complete were skipped, the bootstrap marker was
never written and rc went 0 -> 130: a telemetry write changed installer
control flow.

install.sh:
- The ladder traps (INT, TERM and now HUP, a closed terminal) only note the
  signal while a stage runs. rc==0 clears a Ctrl-C and the ladder carries on,
  as bash's cooperative exit did on main; rc!=0 with a signal noted writes
  `interrupted` and re-raises the signal. Outside a stage (platform check,
  banner, between stages) the signal stops the run at once with a receipt.
  The traps are installed with the EXIT trap, before check_platform.
- Inside a ladder stage, a signal marks STAGE_INTERRUPTED; stage_result
  writes `interrupted` over the stage's own class, so pm.cli /
  source_completion / `hermes setup` catching KeyboardInterrupt and exiting 1
  no longer reads deps_install_failed / setup_failed. A child the signal
  killed still ends the stage at once (status 128+N).
- A TERM/HUP sent to the installer alone takes effect when the running stage
  returns (documented; the stage stays in the foreground because a
  background stage's children ignore Ctrl-C under bash 3.2), and the receipt
  names the stage the run stopped before, not a finished one.
- A stage killed outright (SIGKILL/OOM) now leaves a `failed/<stage>/other`
  receipt; a failed write (full disk) removes its .tmp; the fallback id uses
  $(( RANDOM*32768+RANDOM )) (no octal parse); no clock -> no receipt.

install.ps1:
- A native child that ended on Ctrl-C (130 or STATUS_CONTROL_C_EXIT) reads
  `interrupted` over the stage's class; the receipt write removes its .tmp
  on failure; the dead `other` class on the pre-ladder Initialize-ResolvedPaths
  Fail is dropped (it runs before the ladder's try, so it never wrote one).

Tests (real install.sh main block with stub stage bodies, real SIGINT to the
process group): a handled Ctrl-C finishes the install with a success
receipt; a child that turns Ctrl-C into exit 1 reads interrupted. Both red
at 6246dd0f69d.
2026-10-06 11:10:43 -07:00
Teknium
faf3a1e5c0 fix(install-metrics): one receipt per install run, interrupted runs counted, latch after save
install.sh: a run id per ladder, first writer wins (a parent-only SIGTERM no longer
leaves two receipts); Ctrl-C / SIGTERM write failure_class=interrupted.
install.ps1: same once-only guard, finally{} records an interrupted run.
Reporter: latch a receipt id only after its row is saved, so a short-lived start
whose daemon reporter is cut off at exit re-reports on the next start.
2026-10-06 11:10:43 -07:00
Teknium
74e0225377 feat(observability): hermes.install.run from a local installer receipt
install.sh / install.ps1 leave one closed-token receipt per full ladder run
(installer, outcome, failed_stage, failure_class, start/finish epoch) under
$HERMES_HOME/telemetry/shared_metrics/pending_installs. Every fail()/Fail call
site passes a closed failure class; the installer never sends anything.

The next Hermes start records each receipt once (latched by id, deleted after
the row is saved) only while collection is on; a start with collection off
purges the receipts unreported. Out-of-contract receipts are dropped.
2026-10-06 11:10:43 -07:00
Teknium
1c97b57a93 fix(update): convert a treeless checkout before the dependency work, and on installer re-runs
A Desktop built before the blobless fix keeps walking history during
`hermes update`. On a treeless (tree:0) checkout every walk downloads trees
again, and #129514 filled a 434 GB disk during the minutes of dependency work.
The one-time conversion (#133016) ran at the very end of the update, after that
window. Both update hand-offs (update_completion._prepare and an older
updater's _update_takeover.prepare) now run it first, in the bootstrap
interpreter, and the late call is gone.

Re-running install.sh / install.ps1 over a treeless checkout now converts it
too. The filter is recorded only after the refetch succeeds, so a failure is
retried by the next update instead of leaving a blob:none filter over a
history that has no trees.

The pack cleanup also sweeps tmp_* transfer files that killed fetches left
behind. They are more than an hour old, a storm leaves gigabytes of them,
and git itself only prunes them after two weeks. The docs gain the recovery
steps for a disk that the storm already filled.

Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
Co-authored-by: Yuan Li <dskwelmcy@163.com>
2026-10-05 17:32:54 -07:00
Teknium
1d9bb51f6b fix(install): clone blobless, convert treeless checkouts once
Installers have cloned with --filter=tree:0 since c3e9ac87a7. A treeless
clone keeps commits only, and git requests a missing tree without telling
the server which trees it already has, so every checkout and every
path-filtered history walk re-downloads complete directory snapshots
(~0.58 MB per commit for this repo). GitHub's own guidance is that
treeless clones are for throwaway CI builds, not long-lived checkouts;
the blobless layout keeps every commit and tree local and fetches only
file contents on demand.

- install.sh / install.ps1 clone --filter=blob:none (same release tags,
  --commit pins and ancestor checks the treeless switch was made for).
- hermes update (post-update step) converts an existing tree:0 checkout once with
  `git fetch --refetch --filter=blob:none origin` (~120 MB, ~8 s
  from GitHub) and records the new filter only after it succeeds, so a
  failed refetch retries next update.
- The shallow-heal conversion unshallows as blob:none instead of tree:0.
- checkout_contains asks git with GIT_NO_LAZY_FETCH: a recorded SHA the
  checkout lacks is "not contained", never a download of that commit's
  whole history.
2026-10-05 01:30:17 -07:00
kshitijk4poor
847acaf1fb fix(install): never prompt for sudo under --non-interactive
The libatomic sudo warm-up checked only for a terminal, so
--non-interactive, -NonInteractive and --skip-setup still reached
`sudo -v </dev/tty` inside an SSH session or any PTY-allocating runner.
Gate it on NON_INTERACTIVE like setup and gateway. Reported by
@JoaoMarcos44 on #132664.
2026-10-04 19:19:41 +05:30
kshitijk4poor
01e67e5e7f fix(install): keep the cached sudo ticket alive until PM runs
The clone and downloads can outlast sudo's timestamp_timeout, after which
PM's sudo -n repair silently falls back to the manual remedy. Refresh the
ticket non-interactively right before PM installs. Compute the bootstrap
target once in stage_prerequisites.
2026-10-04 19:19:41 +05:30
kshitijk4poor
a3b80c749c fix(install): ask for sudo up front when Node will need libatomic
PM's libatomic repair runs `sudo -n` under its install lock, so on a host
whose sudo needs a password the automatic attempt can never succeed and
`curl | bash` still ends in "pm install failed". When the glibc host lacks
libatomic.so.1 and a terminal is attached, cache sudo credentials during
the prerequisites stage so the later non-interactive install goes through.
Root, passwordless sudo, musl, and headless runs are unchanged.
2026-10-04 19:19:41 +05:30
ethernet
71d120be6b fix(install): wire ~/.local/bin into ~/.bash_login for login-bash users
A login bash reads only the first of ~/.bash_profile, ~/.bash_login and
~/.profile. wire_shell_path always wrote ~/.profile and ~/.bash_profile
when present, so a user whose only login file is ~/.bash_login never saw
~/.local/bin on PATH. Treat ~/.bash_login like ~/.bash_profile: append the
guarded PATH line when the file exists, never create it (creating
~/.bash_profile would shadow it).

The test helper resolves bash by absolute path so the login-PATH tests run
on hosts without /bin/bash (NixOS).
2026-10-02 19:55:57 -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
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
1d287d5375 fix(browser): ship the Browser Use CLI engine in every install, Desktop included
The default browser_exec tool ran the `browser-use` CLI from a PM side
environment (browser-use==0.13.10 in <home>/environments/browser-use),
provisioned by the installers and `hermes update`. Sealed Desktop payloads
skip that step, so the Desktop app never had it and silently fell back to
the built-in tools; the side env was also per-profile and 225 MB.

The CLI's execution path is only `browser_harness.run.main()`; the
browser-use agent framework (anthropic/openai/google-api pins, 93 MB of
googleapiclient) is never imported. browser-harness itself is 2.6 MB of
pure Python whose pins (Pillow 12.3.0, websockets 15.0.1) already match
Hermes's own, so it becomes a core dependency and runs on sys.executable:

- pyproject/uv.lock: browser-harness==0.1.13 (+ cdp-use, fetch-use).
- _find_cli() returns [sys.executable, -m, browser_harness.run]; the child
  env points PYTHONPATH at the harness site dir (the Desktop store
  interpreter boots without a venv and the harness daemon re-runs
  sys.executable), replacing whatever the agent inherited.
- The side-env provisioning (install_cli, the update/installer step) goes.
2026-09-27 23:53:40 -07:00
teknium1
b63c138d78 fix: Hermes never falls back to the user's node/npm/npx/uv
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.
2026-09-27 22:04:26 -07:00
teknium1
27062c3474 fix: install cua-driver and the Browser Use CLI by default again
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".
2026-09-27 19:18:58 -07:00
kshitijk4poor
44e42b2502 fix(install): re-runs update tag-pinned narrow checkouts instead of aborting
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.
2026-09-28 01:53:35 +05:30
teknium1
842f162f7d fix(installer): refuse musl hosts without libstdc++ up front, naming it
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.
2026-09-27 03:24:39 -07:00
teknium1
34d52883cb fix(installer): ELF interpreter decides musl in uv_bootstrap_target, as in pm/store
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.
2026-09-27 03:24:39 -07:00
teknium1
79e6f1960d fix(installer): restore executable bits on install.sh and gen-bootstrap-pins.py
The salvaged commits dropped both scripts from 100755 to 100644.
2026-09-27 03:24:39 -07:00
JoaoMarcos44
87d1243623 fix(installer): detect musl uv when ldd is unavailable
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
2026-09-27 03:24:39 -07:00
JoaoMarcos44
24487f3db6 fix(pm): keep musl installs on compatible runtime artifacts
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.
2026-09-27 03:24:39 -07:00
ymat19
5cd9dbc261 fix(install): recognize bare PATH= assignments and keep the appended line idempotent
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.
2026-09-27 02:50:10 -07:00
Hermes Agent
980318e689 fix(install): detect arch-suffixed desktop builds on repair/upgrade reruns (#94703)
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.
2026-09-26 20:29:47 -05:00
ethernet
746d861504 fix(install): don't ask the gateway install questions twice
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.
2026-09-24 23:49:18 -04:00
ethernet
90963a6f74 install: --skip-browser / -SkipBrowser become PM's persisted opt-out
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.
2026-09-24 17:27:45 -04:00
ethernet
5640ba3e0f fix(install): hermes runs in the installing window; restore reload hint
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.
2026-09-24 14:23:48 -04:00
ethernet
50de548e49 fix(install.sh): arrow-style output with child noise collapsed on a terminal
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.
2026-09-24 14:23:48 -04:00
ethernet
ff14aaecc0 fix: reject incomplete source installer arguments before install work 2026-09-24 09:20:50 -04:00
ethernet
3cfa43f0c5 fix(install): expose published launcher to new login shells 2026-09-24 07:34:16 -04:00
ethernet
cb7b18431a Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	apps/desktop/src/app/settings/connections-registry.tsx
#	scripts/install.ps1
#	scripts/install.sh
#	tests/hermes_cli/test_update_autostash.py
2026-09-24 03:48:31 -04:00
brooklyn!
c7d2985ae3 fix(install): keep dropped commits behind a rescue ref on the installer reset
Re-running install.sh / install.ps1 over an existing checkout (desktop
bootstrap and its update retry do this) falls back to
`reset --hard origin/<branch>` when a fast-forward fails, with no anchor for
the commits it drops. Park HEAD under refs/hermes-update-backups/, the same
namespace `hermes update` writes and prunes, and print the ref.
2026-09-24 02:40:53 -05:00
ethernet
f91bd82286 fix: restore catalog Hindsight and harden installers and test guards 2026-09-24 02:03:59 -04:00
ethernet
c3e9ac87a7 fix(installer): clone treeless instead of full history
A --depth 1 clone hides the release tag runtime identity is derived from
and cannot resolve a non-tip --commit pin or the ancestor guard; a full
clone downloads every tree and blob ever committed. --filter=tree:0 keeps
the whole commit graph and its tags and fetches trees on demand: 125M vs
77M for --depth 1 against GitHub, identity exact. The deferred-checkout
fallback uses the same filter.
2026-09-24 00:51:07 -04:00
ethernet
9a3fcf9756 fix(install): pass --no-registry to every bootstrap uv python install
install.ps1 already kept uv's bootstrap Python out of the Windows
registry, but setup-hermes.ps1 did not, so every Windows dev checkout
registered that interpreter under HKCU. The flag is Windows-only; the
POSIX installers take it too so every bootstrap issues the same command.
2026-09-23 18:01:43 -04:00
ethernet
1038f8479c fix(install): make both installers survive their real invocation paths
Review findings against the pm-clean installers, each reproduced first:

- install.sh: `curl | bash` aborted before main under `set -u` (empty
  BASH_SOURCE). The entry guard falls back to $0.
- install.sh: setup/gateway read stdin, which under `curl | bash` is the
  script itself. They open /dev/tty when a terminal can be opened, and
  otherwise skip with guidance.
- Both: any uv on PATH was trusted. uv 0.6.17 has no `python install
  --no-bin`. A PATH uv now has to run and be at least the pinned version,
  otherwise the pin is staged.
- install.sh: the staged uv went under ~/.hermes/tools even with a custom
  --hermes-home. It now goes to pm's store_root() default,
  $HERMES_HOME/tools.
- Both: when a stash failed, the script logged "overwritten below" and ran
  `reset --hard` anyway. Local work is now parked before checkout, and a
  stash failure stops the install.
- Both: reruns ignored an explicit HERMES_REPO_URL. It now repoints origin.
- Both: --commit had no ancestor guard. The pin must be on the installed
  branch.
- install.sh: the blobless fallback was `--depth 1 --single-branch`, so a
  non-tip --commit could not check out. It now keeps full history with
  blobs fetched on demand.
- Both: ported main's recovery for a commit-less .git (moved aside, #40998)
  and for an unmerged index (reset -q before the stash, #4735). Stashing
  before checkout makes both reachable.
- install.ps1: on Windows PowerShell 5.1, `native 2>$null` / `2>&1` under
  Stop turns stderr into a terminating NativeCommandError, verified on a
  Win11 host. Every native call now goes through Invoke-Native, which
  relaxes the preference only for that call.
- install.ps1: clone publishes from a staging dir, with retries and a
  blobless fallback, and refuses a non-empty destination (mirrors
  install.sh). UV_NO_CONFIG and `--no-registry` are restored. pwsh 7
  HttpRequestException falls back to the mirror, except TLS trust failures.
  tar resolves from System32. A literal CR/LF in the desktop failure
  message is removed.

Deletes six tests that regex-extracted main's legacy install.sh functions
(node, browsers, PATH block, lockfile churn; pm runs `npm ci` whenever a
lockfile exists). The two behaviours still relevant are covered by new
behavioural tests.
2026-09-23 16:28:00 -04:00
ethernet
5e8fbd4919 install.sh: refuse Termux hosts and point at the APT package
check_platform accepted Termux as plain Linux, so `curl | bash` on a
phone walked the source-install ladder: a glibc uv, a lock whose CPython
is the bundled bionic build with no Android wheels, and on-device sdist
builds. The signed APT package is the only supported Termux shape, so
detect Termux the way the runtime does (TERMUX_VERSION or the com.termux
PREFIX) and stop before any stage with `pkg install hermes-agent` and
the setup docs URL. --json surfaces the reason in the stage frame.
2026-09-21 18:41:42 -04:00
ethernet
04bfa58d47 install.sh: refuse to clone over an unrelated destination
The clone publication step did `mv <staged>/tree "$INSTALL_DIR"`. When
INSTALL_DIR already existed as a non-git directory, mv moved the
checkout INSIDE it as INSTALL_DIR/tree and the stage reported success,
leaving later stages to read pm/lock.json from a directory that holds
the user's files instead of a checkout. An existing file made mv fail
with a generic "cannot publish" error.

Before staging the clone, refuse a destination that exists and is not a
Hermes checkout (non-empty dir, file, or symlink) and say what to do; an
empty directory is taken over so the checkout lands AT the path.
2026-09-21 18:40:30 -04:00
fangliquan
8c5a5deb79 fix(install): probe the command link directory for dependencies 2026-09-20 15:22:22 -07:00
ethernet
9214174e07 fix(ci): lint legs after the main merge
- check_no_tmp_literals: resolve scratch via tempfile/os.tmpdir; the termux
  container mount point is one marked variable per script
- ruff TID251: desktop E2E fixtures may reach PM internals like tests do;
  the pm.runtime_stage ban message no longer names a module that never existed
- auth_codex: build the capped httpx stream subclass on first use so importing
  hermes_cli.auth_codex no longer forces httpx (the lazy proxy in auth_constants
  was defeated by a module-scope base class; broke lanes without httpx)
- desktop-smoke: launchApp is a parameter; the bundle-env test substitutes a
  refusing launcher instead of letting Playwright spawn a dying binary
  (3 unhandled rejections failed the tests-js lane)
2026-09-19 23:25:18 -04:00
teknium1
e16fee4db1 refactor: resolve runtime temp paths via tempfile/TMPDIR instead of literal /tmp
Hermes now routes scratch space through HERMES_HOME/cache/scratch (exported as
TMPDIR), so every production path that still spelled out /tmp bypassed that and
kept teaching the agent the habit. Fallbacks in tool_result_storage,
code_execution_tool, process_registry, the ACP child HOME, mini_swe_runner's
local cwd, and the CI/profiling scripts now use tempfile.gettempdir(); shell
installers fall back to $TMPDIR (then HERMES_HOME) when mktemp is missing, and
repro/eval shells use `mktemp -d -t`. User-facing help text and sample payloads
(hermes send, approvals test, hooks test, voice-mode WSL hints, meet_bot debug
line) no longer suggest /tmp.

Container-side paths (mini_swe_runner docker cwd, sandbox base env, remote
sync tarballs) keep the literal because they name the sandbox filesystem,
not the host.
2026-09-19 10:44:26 -07:00
ethernet
5cb0a5073b fix(install): a diverged checkout cannot keep its old tree
`stage_repository` treated a failed `pull --ff-only` as "keep local state"
and carried on. Every later stage reads files only the new tree has (`pm/`),
so an install whose checkout could not fast-forward failed further down with
`ModuleNotFoundError: No module named 'pm'` instead of updating.

A release cut off the main line diverges for every user who installed from it:
v2026.5.29.2 is not an ancestor of main, so its checkout can never fast-forward
to a later main. Match the remote the way `hermes update` already does, after
parking the old tip behind refs/hermes-install-backup/<stamp>-<sha> and
stashing any local work so neither is destroyed silently.

Verified against a real diverged repository: before, the checkout stayed on
the old line (HEAD=6a2e091, remote=43eea63) with no backup; after, HEAD equals
the new main, the backup ref and an autostash exist, and both are named in the
installer's output.
2026-09-18 02:57:30 -04:00
ethernet
d5c2e3a830 fix(install): rebuild an existing desktop app on a plain installer rerun
installer-script+desktop -> installer-script failed as "desktop output is
missing, stale, or damaged": the leg installs with the desktop, then re-runs the
plain one-liner, which built only tui/web -- while the driver's verifier still
expects the desktop product it installed (EXPECT_DESKTOP comes from the install
method). The artifacts live inside the tree, so an update makes them stale rather
than absent, and a desktop build left over from the previous code is exactly what
the freshness receipt rejects.

The products stage now selects the desktop when --include-desktop/-IncludeDesktop
is given OR the checkout already carries a built app. Verified: install.sh syntax
clean and the new predicate returns absent/present against real temp trees;
install.ps1 parses clean.
2026-09-17 18:05:31 -04:00
ethernet
dbeebaadfc refactor(install): one completion tail shared by install and update
The installer ladder stopped at node-deps/path/desktop with its own
semantics while an update ran launchers, product builds and post-build
maintenance, so a fresh install and a finished update ended in different
states: after re-running the installer at HEAD the products had no receipts
and the read-only source acceptance failed.

hermes_cli/source_completion.py now owns that tail -- publish launchers,
build the products, run the maintenance -- and update_completion's
_complete_selected calls it, so there is one implementation. install.sh and
install.ps1 keep the bootstrap stages (prerequisites, repository, venv,
python-deps, config) and hand off to it in a single `products` stage;
--include-desktop selects the desktop product inside that stage instead of
adding a second build stage, and `desktop` stays dispatchable via --stage for
external callers.

Windows keeps its installer-owned PATH publication (expose_cli answers
"windows-installer-owned" on Windows) plus the packaged-artifact probe, ACL
grant and shortcuts. The desktop stage no longer pre-syncs wake/voice: pm
lazy-installs them at first use, as the update path does.
2026-09-17 15:26:05 -04:00
teknium1
3dcf0d49ad fix(install): let Rolldown name the missing binding; repair on every OS
The first cut derived the package from `binding-${platform}-${arch}` with an
exact-suffix match, which never matches Windows (`-msvc`) or Linux
(`-gnu`/`-musl`) names, so the repair only ever worked on macOS and
install.sh had to gate it there. Rolldown's own loader already resolves
platform, arch and libc and prints the exact `@rolldown/binding-*` it wanted
in its error chain; parse that instead and drop the gate. Also spawn npm
through a shell on Windows (Node refuses to spawn npm.cmd directly) and trim
the tests to the two invariants (no-op when it loads; installs exactly what
the loader asked for, then re-probes).
2026-09-17 00:25:57 -07:00
Gille
ae9f42accf fix(install): repair missing Rolldown bindings 2026-09-17 00:25:57 -07:00
Teknium
73521a8e37 fix(update): one bad workspaces glob no longer aborts the lockfile-churn cleanup
Path.glob raises NotImplementedError for a non-relative pattern, which a string `workspaces` (iterated char by char, so "/") or an absolute entry produces. The (OSError, ValueError, TypeError) catch missed it, so the error escaped to the caller's suppress(Exception) and no lock was reverted at all -- back to autostash every run. Non-list values are now ignored and each pattern is tried on its own so a bad one just owns nothing.

install.sh: read the workspace globs with `while read` instead of an unquoted $(...) so they are never pathname-expanded against the caller's CWD before `case` sees the pattern.
2026-09-16 17:44:36 -07:00