Commit Graph

2503 Commits

Author SHA1 Message Date
ethernet
2ba873b93a fix(install.ps1): resolve the bootstrap Python once per process
The full ladder runs every stage in one PowerShell process, and venv,
python-deps, source completion and launcher publication each called
Get-BootstrapPython, repeating the uv probe and `uv python find`.
Memoize the resolved interpreter in script scope. The existing
find-before-install order is unchanged.
2026-09-24 14:08:22 -04:00
ethernet
9b4ed5f5ec refactor(pm): one plugin-input request for sync_venv and venv_is_current
sync_venv took four mutually exclusive plugin kwargs (plugin_dirs,
extra_plugin_dirs, selection, staged_plugin) and venv_is_current two, with
runtime ValueErrors guarding the combinations. Replace them with a single
`plugins=` argument typed as one of pm.plugin_inputs.Members, Candidates,
Selection or StagedUpdate, so a conflicting request cannot be expressed.
The module also owns the worker wire encoding that client.py and worker.py
each duplicated.

pm.install.sync_venv is split into cohesive helpers (feature policy,
install lock, publication snapshot, target selection, commit) with the
same ordering, receipts and recovery; its CC drops from 54 to 16.

All in-tree callers and tests move to the new argument.
2026-09-24 14:08:21 -04:00
ethernet
f83a9e9a45 fix(build): announce npm ci and show npm's spinner
The workspace npm ci printed nothing until 'added N packages': builders
set CI=1, which turns npm's progress off, and the stage name only went
to the desktop UI's status file.

Print a line before npm ci, and pass --progress=true so a terminal gets
npm's spinner back (npm still shows it only on a TTY, so piped output
such as the desktop app's log stays clean). The flag stays out of the
receipt-keyed args, so existing installs are still reused.
2026-09-24 13:43:40 -04:00
ethernet
dc11e3b3bc feat(release): add --skip-bundles and --skip-tests to stable releases
`release.py release` gains two flags. They can be used together.

--skip-bundles ships only the claim, the GitHub release, the final tag
and the Docker image. No desktop, Termux or PM bundle job runs. The
final tag records candidateManifestSha256: null. Publication moves only
the Docker stable/latest aliases. The R2 stable head, feeds, APT, the
downloads page, the signed-package baseline and the Store stay on the
previous bundle release.

--skip-tests builds, signs and publishes every artifact and runs no
test job: source CI, Nix, PM bundle check, Termux, Windows live,
install/update E2E, bootstrap identity, native smokes, upgrade
acceptance, tests/docker and the in-build vitest step. The candidate
manifest records each smoke as skipped, never as passed.

The flags live in the claim message (skipBundles, skipTests), next to
autopublish. They are not workflow inputs, so a rerun cannot change
them. admit emits them, and every job condition and gate reads them.
stable.validate_claim and stable.validate_final are now the one shape
check for stable.py and the sequencer.

The gates stay strict. SKIPPED_BY in stable.py maps each job to the
flags that remove it. `gate` requires those jobs to report skipped and
every other gated job to report success. A job that ran although a flag
removes it blocks the release.

A release that skipped bundles never moves the R2 stable head. Two
readers depended on that head:

- The next version was derived from it, so the next cut would reuse the
  version. It now takes the newer of the R2 head and the newest
  published non-prerelease GitHub release with a vX.Y.Z tag. Bare v*
  tags do not count, because those refs are not protected yet.
- The sequencer used it to decide which published releases still need
  their publication pass, so a bundle-less release would re-advance
  every 15 minutes. The head is now the newer of the R2 head and the
  published release whose final tag binds the Docker stable alias
  digest.

`release` also refuses a cut when its next version already has a final
tag. That closes the window between the final tag and the public
release, where the published identity still names the old version.

Tests: 42 release test files, 546 passed. Three tests fail on this
Windows host, and they fail the same way on a clean HEAD worktree:

- test_stable_release_graph::test_docker_recovery_refuses_to_replace_a_divergent_version_tag
- test_release_artifacts::test_windows_metadata_is_read_from_package_and_stale_stamp_is_rejected
- test_tag_builds_summary::test_admitted_failure_publishes_tag_info_without_promoting_channel[True]

Not verified: no real Stable Release dispatch ran with either flag, and
actionlint is not installed on this host. The workflow changes are
checked by the graph tests and by running the phase-result step script.
2026-09-24 13:31:33 -04:00
ethernet
a5f5a31fe7 fix(install): fall back to the mirror when the progress download fails
Invoke-DownloadWithProgress runs Invoke-WebRequest in a separate runspace,
where an HTTP or DNS failure is non-terminating: EndInvoke returned normally,
the caller skipped the mirror, and Get-FileHash died on a file that was never
written. Rethrow the runspace's first error outside the unwrapping catch, so
the caller sees the same WebException/HttpResponseException it classifies.
2026-09-24 12:16:33 -04:00
ethernet
7e59364c8f Merge remote-tracking branch 'fork/ethie/pm-clean' into ethie/pm-clean 2026-09-24 11:58:45 -04:00
ethernet
e8d1fc76ec fix(install): run the installed hermes under the Restricted policy
`irm | iex` runs install.ps1 as text, which execution policy never
checks, but Invoke-InstalledHermes then dot-sourced runtime.ps1 from
disk. That is a file load, and the default Restricted policy (Windows
Sandbox, fresh machines) refused it right after "hermes command
installed". Load the helper from its text instead.

That failure hid a second one on the same path: the `$command` local
was shadowed inside Invoke-Native by its case-insensitive `$Command`
parameter, so `& $command[0]` invoked the scriptblock itself until the
call depth overflowed. Rename the local.
2026-09-24 11:56:34 -04:00
ethernet
2be53ecd7b fix(encoding): read kernel pseudo-files as plain utf-8
utf-8-sig exists to tolerate BOMs that Windows tooling adds to files
users edit. /proc and /sys files are generated by the Linux kernel, never
BOM'd and absent on Windows, so -sig there only muddies the read/write
policy. Switch every literal /proc/ and /sys/ read to utf-8 and teach
the footgun read rule that string literals starting with /proc/ or
/sys/ are exempt (user-edited files keep utf-8-sig).
2026-09-24 11:50:30 -04:00
ethernet
6f14f6001d ci(lazy-deps): describe tools.lazy_deps as the stub it is
tools/lazy_deps.py was not deleted; it survives as an old-updater stub
that raises or stops for relaunch. The job display name and the
checker/test prose claimed it was deleted, which sends readers looking
for a missing file. The display name is not a required status check
(main requires only "All required checks pass", which keys on job ids),
so rename it to "No production imports of the tools.lazy_deps stub".
2026-09-24 11:50:30 -04:00
ethernet
845b61f2b8 fix(release): rerun failed stable runs on the failure event, drop the cron
Stable Release Publication ran every 15 minutes (96 runs a day, each
checking out full history, setting up node and buildx, logging into
Docker Hub, and taking the release-signing environment) only because the
sequencer held a failed run for a 15-minute backoff that the failure
event could never satisfy, so the cron was what actually retried.

Drop the backoff: the reconcile pass started by a failed Stable Release
reruns its failed jobs right away. MAX_ATTEMPTS burning, oldest-first
retry ordering, the attempt-entry check, and the needs_retarget repair
stay. The schedule trigger goes; workflow_run and workflow_dispatch
remain the recovery paths.

The shared stable-release concurrency group cannot deadlock: the rerun
waits as pending behind this job, and the sequencer only confirms the
new attempt is queued before it exits and frees the group.
2026-09-24 11:50:30 -04:00
ethernet
689d77292a fix(install): say what the ARM64 build-tools step is doing
On a fresh ARM64 Windows host, the venv sync first installs Visual
Studio Build Tools, Rust and a vcpkg OpenSSL to compile dependencies
that have no ARM64 Windows wheel (cryptography among them). The Visual
Studio installer ran with --quiet, so the installer printed nothing
for 20+ minutes after the tool lines and looked hung.

Print a line before each slow step, with what it is for and how long it
can take, and run the Visual Studio installer with --passive so its own
progress window shows.
2026-09-24 11:46:28 -04:00
ethernet
bcabc5b881 fix(install): keep the PowerShell session open when an iex install fails
The documented one-liner, iex (irm .../install.ps1), runs the installer
inside the user's own session. Fail ended with exit 1, so any failed
stage closed the user's PowerShell window.

Fail now throws. The two entry points own reporting and the exit code:
-Stage prints the reason, emits the -Json frame and exits 1, as before;
the full install exits 1 only when it runs from a script file, and under
iex it prints the reason, sets LASTEXITCODE=1 and returns. A scriptblock
literal's File tells the two apart: $MyInvocation.MyCommand.Path names the
caller's script under iex.
2026-09-24 11:31:08 -04:00
ethernet
f30e86c665 fix(install.ps1): more windows log improvements 2026-09-24 11:21:47 -04:00
ethernet
be694719e9 fix(handoff): curl latest sha for repo 2026-09-24 11:09:52 -04:00
ethernet
419a3cbe00 fix(install): request the native bootstrap Python on Windows-on-ARM
A bare version request lets uv pick an emulated x86_64 CPython on
Windows arm64 hosts ("support for the native architecture (aarch64) is
not yet mature"). The bootstrap interpreter then ran as win-amd64.

Request cpython-<minor>-windows-<arch>-none from the machine
architecture the scripts already detect, in install.ps1,
setup-hermes.ps1 and setup-hermes.sh (win32 only; POSIX keeps the bare
version so uv still picks the right libc variant).
2026-09-24 10:58:07 -04:00
ethernet
f39aa8a7a6 feat(install): progress bars on windows 2026-09-24 10:35:19 -04:00
ethernet
f6585964b6 feat(install): windows install progress 2026-09-24 10:32:30 -04:00
ethernet
e8a5e0978c chore(desktop): require prepared build Python and refresh release comment 2026-09-24 09:20:50 -04:00
ethernet
edcb3346a7 fix(desktop): constrain baked environment and validate registered feeds 2026-09-24 09:20:50 -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
dcfe8f2d75 fix(install): refuse occupied Windows checkout before Git provisioning 2026-09-24 06:09:17 -04:00
ethernet
2fb89006cd Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	agent/learning_mutations.py
2026-09-24 06:08:20 -04:00
kshitijk4poor
03584e41b9 test(docker): pin the non-mapping config.yaml boot path
The strict version read in docker_config_migrate is what keeps a list-root
config.yaml on the warn-and-continue path; only a probe covered it. Fold the
list-root case into the existing invalid-YAML test (reverting to the tolerant
read now goes red) and correct the comment about where the warning comes from.
2026-09-24 15:34:19 +05:30
kshitijk4poor
92324790f2 refactor(config): read the version stamp once and drop has_version_stamp
migrate_config() and the docker boot script each parsed config.yaml twice:
check_config_version() coerced a missing `_config_version` to 0 and threw the
"was it present" bit away, so has_version_stamp() re-read the file to recover
it, guarded only by a call-order promise in its docstring. That promise did not
hold for the docker script, which used the tolerant check: a list-rooted
config.yaml read as "unversioned, not below the floor", ran the backup +
migrate_config() dance and exited 1 (base: floor warning, exit 0).

Factor the read into _read_config_version_stamp() -> (Optional[int], latest);
None means the mapping has no stamp. check_config_version() is a thin wrapper
(None -> 0) so its 10 callers see identical output. migrate_config() and the
docker script decide `unversioned` from that single read; the docker script
now does the strict read itself and leaves an unparseable or non-mapping file
alone with a warning and exit 0, matching its invalid-YAML posture.
has_version_stamp() is deleted. Docker tests that mocked the pre-check now
mock the new helper.
2026-09-24 15:34:19 +05:30
John Paul Soliva
a88bef98b2 fix(config): stop the migration ladder rewriting unversioned configs
A config.yaml without _config_version reads as v0 and is exempt from the
support floor, so the first `hermes update`, profile clone,
`hermes doctor --fix` or docker boot ran every one-time migration step on
it. Installers seed config.yaml from cli-config.yaml.example, which had no
version, and targeted writers (`hermes config set`, /personality, the
TUI/Desktop config writers) never stamp one, so this is the normal state
of --skip-setup, non-TTY and Desktop (--non-interactive) installs. The
value- and absence-based steps then reset the personality, raised the
delegation caps, turned verify_on_stop off, shortened the curator windows,
dropped model_catalog.ttl_hours and enabled plugins the user had installed
but never enabled.

- A config with no _config_version now gets only the steps keyed on a
  legacy key or identifier (LEGACY_KEY_STEPS), then the stamp.
- cli-config.yaml.example carries _config_version, so every seeded
  config (install.sh, install.ps1, docker/stage2-hook.sh, doctor --fix)
  starts at the current schema.
- docker_config_migrate.py no longer refuses a version-less volume with
  the "predates version 12" warning; like migrate_config() it migrates
  and stamps it.

(cherry picked from commit 97ba11e07009f633662b0c7fa8701aa5b441bd22)
2026-09-24 15:34:19 +05:30
ethernet
fb7fe862ef fix: restore pinned Git in each Windows bootstrap stage 2026-09-24 04:42:28 -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
brooklyn!
14a8a771de fix(desktop-update): stop launching a Chrome instance for the macOS update shim
Each update started the user's Chrome binary with its own --user-data-dir,
a second instance of the same bundle that the Dock records as a new
recent-app tile. On macOS the outcome now goes through the existing
notification + next-boot result dialog.

Fixes #96374
2026-09-24 02:36:14 -05:00
ethernet
5f78e110e1 test: align Windows launcher and bootstrap fixtures with native behavior 2026-09-24 03:12:29 -04:00
ethernet
d6106975e9 fix(docker): promote desktop variant from its own release digest 2026-09-24 02:03:59 -04:00
ethernet
d288905b11 test: assert rehearsal stops only the target home gateway 2026-09-24 02:03:59 -04:00
ethernet
f91bd82286 fix: restore catalog Hindsight and harden installers and test guards 2026-09-24 02:03:59 -04:00
ethernet
038d7f797f fix(pm): avoid duplicate Daytona install and decode Store output explicitly 2026-09-24 01:36:14 -04:00
ethernet
dcd2bca06a fix(desktop): render icons with core runtime dependencies 2026-09-24 01:30:15 -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
26c6307046 ci(install-e2e): route selects specific legs, not just an OS
route already means "which legs run"; a leg name is the most specific
route. Presets keep their meaning (all, the linux trio, windows-desktop,
macos-desktop, the bundled three); any other value selects legs by name,
so a leg name, a fragment of one, or the job name GitHub shows
("<leg> / e2e") runs just those legs. A route that selects nothing fails
instead of producing a green empty run.

The generator is now the one interpreter of route for source legs: the
linux/windows/macos jobs run when it selected legs for them, replacing
three hand-kept preset lists. Dispatch route becomes a string input (a
choice cannot take a leg name) and reaches the gen step through env, never
interpolated into the script.
2026-09-23 23:42:01 -04:00
ethernet
b1c9d1279a test(install-e2e): every combination also installs HEAD and updates it to a synthetic NEXT
Every leg installed a release tag and updated to HEAD, so nothing ran
HEAD's installer on an empty machine (where all four 2026-09-23
install.ps1 breaks lived) and nothing exercised the updater we ship
today -- tag legs run the OLD build's updater handing off to HEAD.

The generator appends a HEAD -> NEXT start after the sampled tags, so the
column runs wherever the update legs run (dispatch and stable-release).
NEXT is a reserved update ref: the drivers mint a child of the install
commit that adds one marker file, written to the object store only, which
the local bare clone carries into serve.git.

On Windows the HEAD leg takes every git.exe dir off PATH and installs no
remote get-url shim: Get-PinnedGit returns any git on PATH, so either one
skipped pinned-git staging. launch-from-spec's HEAD observer now uses the
driver's real git so it cannot poll '' forever on that leg.
2026-09-23 23:06:28 -04:00
ethernet
74c365a85a fix(release): check the draft body before the claim; release takes --no-changelog
The stable cut built its draft body after pushing the attempt ref, so a
body over GitHub's 125000-character limit failed with HTTP 422 and
burned the attempt. The body is now built first, and an oversized one is
refused before anything is claimed, naming --no-changelog.

--no-changelog was defined only on the top-level parser, so
`release.py release --no-changelog` was an argparse error and the flag
never reached the cut. The release subcommand now takes it, with a
SUPPRESS default so the top-level spelling is not reset.
2026-09-23 22:24:22 -04:00
ethernet
d75d3fe15c Merge origin/main into ethie/pm-clean
Conflict resolutions and semantic fixups:

- tools/environments/base.py: main's hard-exit kill fence (kill a spawn the
  fence missed, deregister from _live_foreground in a finally) wrapped around
  pm-clean's output collector.
- pyproject.toml: pm-clean's marker list plus main's new `live` marker.
- hermes_cli/main.py: pm-clean runs startup recovery from hermes_bootstrap, so
  the old early-recovery block stays gone; main's interrupted-pull restore
  (auto-merged above it) runs right after bootstrap, as on main.
- hermes_cli/update_cmd.py: main's interrupted-pull marker now guards
  pm-clean's first tree mutation (release-tag detach, ff-only, or reconcile)
  and is cleared once git is done. The marker's target is the ref git actually
  moves to (a release tag, not always origin/<branch>), since the restore
  compares against it.
- hermes_cli/_early_recovery.py: restore `import subprocess`, which pm-clean
  had dropped and main's auto-merged restore needs (NameError on the first
  launch after a killed update; test_update_interrupted_pull red -> green).
- apps/desktop/src/i18n/{de,es,fr}.ts: main's new locales carry the full
  settings.about block; trim it to `updates` as pm-clean's type and the other
  overlays do (tsc: 27 errors -> 0).
- main's new e2e tests: `import yaml` -> hermes_yaml; wake-word import table
  names pyopen_wakeword (pm-clean's wake-openwakeword extra); the anthropic
  key-leak switch leg needs the SDK, and the api_server two-tenant test needs
  aiohttp, both PM runtime extras the test env does not carry.
2026-09-23 21:55:59 -04:00
teknium1
0988a99743 test(runner): forward HERMES_E2E_REQUIRE_TUI, CI and GITHUB_ACTIONS through the hermetic env
run_tests.sh starts every run from env -i with an allowlist, so the e2e
job's HERMES_E2E_REQUIRE_TUI=1 never reached the terminal suite (a missing
ui-tui build skipped instead of failing) and the upgrade suite's CI branch
in sandbox_required_reason() was dead. With ui-tui/dist removed and
HERMES_E2E_REQUIRE_TUI=1: before, 2 skipped; after, 2 failed.
2026-09-23 17:55:23 -07:00
ethernet
e0265925f1 fix(release): --no-changelog no longer raises NameError
generate_changelog() defined all_authors and teknium_aliases inside the
'if not no_changelog' block but read them after it. The canary tests
stub generate_changelog, so nothing ran the real path.
2026-09-23 19:56:46 -04:00
ethernet
c0bd183eef fix(release): build stable draft notes from commits, not generate-notes
GitHub's generate-notes lists merged pull requests since the last
published release. A fork merges none, so the stable draft body was only
a "Full Changelog" link, and it lost the HERMES_BUILDS_TABLE marker that
the stable workflow renders the download tables into.

The cut now builds the body with generate_changelog() over the commits
from the published stable commit (or the seed version's tag before the
first publication) to the cut commit.
2026-09-23 19:56:46 -04:00
ethernet
c58744e59e fix(release): link the draft at cut time, not a page that 404s until green
The stable cut already creates the draft on the claim ref before it
dispatches the gate, but the output pointed at releases/tag/<claim> and
then at releases/tag/v<version> "when it is green". GitHub serves a draft
only at an untagged-* URL, so neither link showed it. Operators read that
as "the draft appears after the build".

Take the URL gh release create prints (the release's html_url) and print
it up front, with a note that notes edited during the build survive:
edit_draft_release keeps the body and strips only the warning fence. The
v<version> URL stays, labelled as where the release lives once published.

The canary resume path had the same broken releases/tag/<tag> link; it
now uses the create output or the url field of gh release view.
2026-09-23 19:20:27 -04:00
ethernet
0384a24edc Merge branch 'ethie/release-attempt-refs' into ethie/pm-clean
Conflicts:
- scripts/releases/stamping.py, tests/scripts/test_version_stamping.py:
  took ethie/pm-clean. The release branch's side was only its base's copy
  of "stamping a payload snapshot skips the bootstrap-installer check"
  (8411fdb333, same patch-id as 8d34601f47 here); the install-stamp
  refactor c13ea774e6 supersedes the rest.
- tests/ci/test_stable_release_graph.py: kept pm-clean's release-epoch
  contract (no HERMES_RELEASE_EPOCH on termux-deb, version on docker and
  nix only) and the release branch's per-group receipt wiring.

Semantic conflict: the dispatch log step (6e64e961d8) read
inputs.termux_only, which the jobs input replaced. It reads JOBS now, and a
dispatch that selects only some groups has no release.py replay, as a
termux-only one had none before.
2026-09-23 19:00:44 -04:00
ethernet
babbec1c4c feat(pm): isolate developer test environment from runtime extras 2026-09-23 18:13:38 -04:00
ethernet
4e3684c993 fix(install): create the pm store root before publishing pinned git
Prerequisites is the first stage, so on a fresh Windows host
<HermesHome>\tools does not exist when Get-PinnedGit runs, and Move-Item
throws DirectoryNotFoundException (reported against the source path).
The uv stager already creates its slot with New-Item -Force.
2026-09-23 18:10:09 -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
e6446eb72e fix(build): pin the ARM64 cargo linker from VCToolsInstallDir, not PATH
PM prepares the ARM64 build environment again inside callers that already
carry a matching developer environment. It is reused as-is, including PATH
order, and a Git Bash caller puts coreutils' link.exe first, so the guard
aborted setup ("MSVC link.exe is shadowed by ...\Git\usr\bin\link.exe").
VsDevCmd's VCToolsInstallDir names the MSVC linker exactly, whichever way
the environment was obtained. PATH order stays the caller's.
2026-09-23 17:51:29 -04:00