Commit Graph
2968 Commits
Author SHA1 Message Date
Ben Barclay d96591fbc8 fix(config): route config_backend to the upgrade lane and fault-inject the seam's parser in the MoA read test
update_channel now writes through hermes_cli.config_backend, so the CI
classifier lists it as an update dependency. The MoA transient-read test
patched hermes_cli.config.fast_safe_load, which the seam no longer calls;
it now patches utils.fast_safe_load, the parser the FileBackend uses.
2026-10-25 18:54:09 -07:00
kshitijk4poor b7cd1f084b fix(config): reader gate tracks every binding form and scans pm/
The reader gate only bound plain `name = <config path>` assignments and never scanned pm/, so
direct local-file reads still passed it: `env_path, config_path = ...` then
`config_path.exists()` in tools/send_message_tool.py, and pm/plugins_state.read_home_selection
reading config.yaml with read_text().

The gate now binds tuple-unpacked elements, `self.<attr>` targets and for-loop targets; flags
unbound `Path.<method>(p)`, shutil copies and pm's byte readers (read_bytes_or_none,
file_digest); and scans pm/.

Routed through the backend: send_message_tool's missing-config check and
read_home_selection (same FileNotFoundError / OSError / YAMLError -> ValueError contract).
Marked as file tooling: pm's byte-level compare-and-swap publication/eviction rewrite and the
LKG backup copy, which operate on the local file itself.

Known limit: bindings flow into nested scopes, not across sibling methods, so a `self.x` bound in
`__init__` is not tracked in another method (pm/publication.PluginSelection.publish).
2026-10-25 18:54:09 -07:00
Ben Barclay 801847e954 fix(config): reader gate covers every PyYAML parse entry point and requires the escape's reason; unshadow config_exists 2026-10-25 18:54:09 -07:00
Ben Barclay 871900df63 feat(config): route every user config.yaml read and write through a ConfigBackend seam
Add hermes_cli/config_backend.py (config-config design §4.1, D10/D11): a
ConfigBackend protocol, the FileBackend (today's behaviour), selection by
HERMES_CONFIG_BACKEND only (default file; 'remote' and unknown values fail
closed with ConfigBackendUnavailable), and path-level helpers
(read_config_doc, read_config_doc_readonly, config_version, config_exists,
write_config_key, write_config_document, supports_file_tooling).

Migrate every direct reader/stat/exists/bypass writer of a user config.yaml
to the helpers (config.py loaders + caches, config_effective, env_loader,
gateway, TUI, CLI watchers, cron, tools, profiles, logging, time). File
tooling (config edit, backups, xAI migration rewrite, raw dashboard editor)
checks supports_file_tooling() first; migrations skip write-back without it.

Add scripts/check_config_yaml_readers.py (CI lint next to the writers gate)
that rejects a direct read of a config path outside the backend.
2026-10-25 18:54:09 -07:00
Teknium a80b413cd6 fix(git): graph refresh sweeps only object-store debris and lets git clean up
Review round on #144647 (head d016f04694d):

- CI routing: the refresh (slot claim, bounded runner, stale check, detached spawn) moves to
  hermes_cli/git_commit_graph.py, routed to e2e_upgrade with the update pipeline. The update path
  imports it instead of all of worktree_ops; worktree_ops keeps its repack seams and imports the
  shared slot claim and runner from it (no cycle).
- W1: the -w pass runs in whatever repo it was launched from, so its refresh sweeps only the
  object-store locks (clear_stale_git_locks(names=OBJECT_STORE_LOCK_NAMES)); index.lock / HEAD.lock
  there belong to the user's git. The install checkout's update sweep is unchanged.
- W2: timeout/exit reaping SIGTERMs the git tree first and SIGKILLs only after a 5 s grace
  (kill_process_tree(grace=), default unchanged for every other caller), so git deletes
  commit-graph-chain.lock and tmp_graph_*. The age-gated tmp sweep now also takes
  objects/info/commit-graphs/tmp_graph_*.
- N1: a breakaway-forbidding Windows job gets the no-breakaway retry, and a spawn that fails gives
  the 6 h slot back.
- N2: the detached child logs a failed write (git's fatal line) to the home's agent.log/errors.log.
- core.commitGraph=false in the target repo skips the write.
- N3: a successful no-op write stamps hermes-commit-graph.fresh, so an idle repo is not rewritten
  every slot.
- N4: test_update_finish's completion child and every in-process hermes_cli test stub the
  detached spawn; no writer outlives a test run.
- Greptile: the refresh test now runs on a treeless clone over a Bloom graph and asserts the
  write adds no pack (red when --no-changed-paths is dropped).
2026-10-25 17:51:21 -07:00
unsupportedpastels 342a55ebad fix(cli): /discard usage hint, parity note and connection test fakes
Addresses maintainer review on #143595.

Refs #143547
2026-10-25 11:30:24 -04:00
Maciej Lisiewski 05b83f632f Merge pull request #143719 from NousResearch/bulk-update-js
chore(deps): bump npm dependencies to latest minor/patch releases
2026-10-25 12:54:29 +01:00
emozilla 443732a58b fix(update): hermes update rebuilds the Webapp bundle a home serves
hermes update rebuilt the TUI, the web UI and the desktop app but never apps/desktop/dist-webapp.
A gateway with webapp.enabled kept serving the bundle built before the update, which then talked
to the updated backend with an outdated contract.

The update now rebuilds that bundle when any home sharing this install has webapp.enabled, with
prepare_webapp_renderer, the function hermes webapp uses (a current bundle is reused). Like the
other products it is attempted on its own: a failure is reported with the rest at the end and
never skips another product.
2026-10-25 03:22:27 -07:00
Bear Huddlestonandemozilla b5eb5ee7a8 feat(webapp): carry the browser-hosted Desktop's server pieces from #93508
Carried from BearHuddleston's #93508 onto main. The Webapp relay and the
profile-incarnation fence stay out: carried code follows main's
named-profile rule and refuses a missing or deleted profile home.

- Webapp routes: one-use launch tickets, window handoff, and gateway
  WebSocket tickets minted for the signed-in browser
- Browser surface policy and the SPA document policy
- A host terminal for the browser that survives reconnects
- Staged browser uploads and file tickets; file.attach takes a staged upload
- hermes sessions adopt
- Webapp bundle build helpers and the renderer freshness check
- Regenerated gateway contracts

Co-authored-by: emozilla <emozilla@nousresearch.com>
2026-10-25 03:22:14 -07:00
Teknium 97253cc182 fix(run_tests): count import/collection failures and missing named paths
A test file that raised at import (e.g. a decorator using pytest without
`import pytest`) exited 2 with "collected 0 items / 1 error" and no
failed-test count, so the Summary line read "2 tests passed, 0 failed"
while the run exited 1, and the file was filed under a generic "no tests
ran" bucket. A positional test path that did not exist was dropped by
discovery and the run exited 0 without testing it.

- mark a failing file whose output shows a collection error and give it
  its own failure bucket, "FAILED AT IMPORT/COLLECTION", with pytest's
  one-line cause (the last traceback E line)
- the Summary line names failing files: "; FAILED: 1 of 2 files
  (1 at import/collection)"; error/crash/skip notes move into one helper
- the failure buckets become a cause -> (header, detail) table
- a positional path that does not exist exits 2 with
  "error: no such test path: ..."; --paths defaults keep skipping missing
  roots
- per-file pytest exit 5 (nothing collected, e.g. platform-gated) stays a
  pass, as deliberately added with the run-level NO TESTS RAN guard
2026-10-24 17:10:39 -07:00
Byrd-IT cd424473d1 fix(run_tests_parallel): name pytest ERRORs on the Summary line
A file whose tests pass but whose fixtures error at setup ("41 passed,
7 errors") exits 1, yet the Summary read "41 tests passed, 0 failed" and
the file was filed under "all tests passed but pytest exited non-zero".

- accumulate summary["errors"] into tests_errored and print ", N errors"
  on the Summary line when non-zero (same pattern as crashed_note)
- dedicated failure bucket "with pytest ERRORs" listing passed/error
  counts per file; such files no longer land in the passed-but-nonzero
  or no-tests-ran buckets
- tests: parser returns errors for "41 passed, 7 errors"; end-to-end
  probe with a failing fixture asserts the Summary shows "3 errors";
  clean run has no errors note
2026-10-24 17:10:39 -07:00
ethernet be82958af5 fix(nix): the dev shell's HERMES_PYTHON carries its own dependencies
The shell pointed HERMES_PYTHON at the bare pm-pinned interpreter and reached
the uv2nix site-packages only through PYTHONPATH. Every consumer that replaces
PYTHONPATH (Ink's gateway bootstrap, image attachments, run_tests.sh's env -i)
got an interpreter with no dependencies, so 'hermes --tui' in the shell failed
with 'No module named ruamel'.

devVenv re-homes uv2nix's editable venv onto the pinned interpreter: a real
pyvenv.cfg whose home is the lock's python, devPython's site-packages, and
console scripts re-shebanged onto it. HERMES_PYTHON, VIRTUAL_ENV, python,
pytest and pm are now one interpreter and the shell sets no PYTHONPATH.

The shipped package and the shell share one tool list: runtimeDeps takes
ripgrep and ffmpeg from pm/lock.json too. Reverts the run_tests.sh PYTHONPATH
passthrough, which leaked a caller's PYTHONPATH into every HERMES_PYTHON run.
The dev-venv-self-contained check runs the venv under env -i.
2026-10-23 17:20:39 -04:00
Maciej Lisiewski c69d6d8763 chore(deps): bump indirect npm dependencies within existing ranges (whatsapp)
- lru-cache: 11.5.2 -> 11.5.3
- uint8array-extras: 1.5.0 -> 1.6.0
- ws: 8.21.3 -> 8.22.0
2026-10-23 22:00:12 +02:00
Teknium f05df166b3 ci: route config_version_stamp to the upgrade suite
post_update now imports hermes_cli/config_version_stamp.py (it converts legacy
AGENT_BROWSER_* settings before stamping), so a change to it must start the
e2e_upgrade lane like every other module the update path imports.
2026-10-23 01:43:17 -07:00
Teknium 7152bf009a fix(install): browser opt-out uses the name every PM release accepts
install.sh --skip-browser / --no-playwright and install.ps1 -SkipBrowser
now hand PM `--without agent-browser` again instead of `--without chromium`.

The downloaded installer is the newest script, but by default it installs
the stable tag, whose PM (v0.22.5) rejects `--without chromium` with
"--without accepts only optional default packages (agent-browser,
cua-driver)" and exits 1, failing the whole install. The new PM already
reads `agent-browser` as `chromium` (pm.defaults.RETIRED_DEFAULTS), so the
legacy spelling works against both the stable tag and main. Direct
`hermes pm install --without chromium` stays valid on the new PM.
2026-10-23 01:43:17 -07:00
Teknium c225308275 fix(config): legacy AGENT_BROWSER_* settings survive homes that skip the v52 step
The runtime reads only browser.* now and the AGENT_BROWSER_* -> browser.* move
lived only in the versioned v52 config step, so three kinds of home lost their
engine, headed mode and persistent profile without a word: a missing
config.yaml (reads as current), an unstamped one (boot skipped it as below the
support floor) and one first stamped by an ordinary save before any migration.

The move is now done once, at the home's first stamp:
- note_first_stamp (every save_config first stamp, and left-core's created
  file) moves the settings along with the left-core rows it already records;
- stamp_missing_config creates config.yaml with that first stamp when a
  missing config's .env still carries them, before the missing-config shortcut
  in boot (post_update.step_migrate_config), migrate_config and the Docker
  entrypoint;
- boot migrates an unstamped config whose .env still carries them through
  migrate_config (legacy-key steps, v52 among them, then the stamp); other
  unstamped configs are left alone as before.

The conversion reuses the v52 step's code (move_agent_browser_env) and only
ever runs while a home is unstamped or missing, so a retained .env never
overrides a later config edit.
2026-10-23 01:43:17 -07:00
Teknium 0f8bbed1ed fix(config): drop browser.extension_control (default and v52 migration)
Nothing reads the key since browser-extension control was removed; the existing v52 step (a
legacy-key step, so unstamped configs get it too) now strips it from user config.
2026-10-23 01:42:29 -07:00
Teknium 1ac18d2b12 refactor(pm): drop the agent-browser package; Chromium is a default package in its own right
- pm: AgentBrowser class + lock entry gone; Chromium default=True and its env
  exports only PLAYWRIGHT_BROWSERS_PATH (Playwright launches without an
  executable path still need it). A recorded or passed --without agent-browser
  maps to chromium (pm.defaults.RETIRED_DEFAULTS), and pm gc forgets the stale
  agent-browser fact so its store entry is reclaimed.
- install.sh --skip-browser/--no-playwright and install.ps1 -SkipBrowser pass
  --without chromium; undo hint is hermes pm install chromium.
- Docker bakes /etc/hermes/chromium-executable-path; stage2-hook validates it
  and exports nothing; the sandbox-desktop image drops the agent-browser npm
  install and keeps its Playwright Chromium.
2026-10-23 01:41:25 -07:00
Teknium a4072ce7e6 fix(desktop): the short uv cache junction creates its parent
Review follow-up: short_alias now creates the junction's parent directory,
so preparation never depends on the caller having made the work dir first
(the existing prepare_tools test failed on both Windows lanes with WinError 3).
That test now checks the invariant the Windows path keeps: UV_CACHE_DIR
reaches the cache directory, through the short work-local junction on Windows.
2026-10-22 17:47:42 -07:00
Teknium 1aaf75e6f2 fix(desktop): build Windows sdists under a short uv cache path
With a cold desktop-inputs cache, Windows desktop preparation builds sdists
that have no cp314 Windows wheel (obstore on win32-x64, dependency-injector on
win32-arm64). uv builds each one in
<cache>\python\runtime\<toolchain id>\sdists-v10\pypi\<pkg>\<ver>\<id>\src,
about 200 characters below the deep .cache\desktop-inputs root, so MSVC
link.exe outputs passed MAX_PATH and the build failed with LNK1104.

On Windows, prepare_tools now hands uv a short job-local junction
(<work>\uv) that points at the same transported cache directory. uv keeps the
spelling it is given, so builds run under the short path while the bytes are
still restored and saved by the desktop-inputs cache. The native toolchain id
is read through the junction, and prepare_native keeps the absolute spelling
instead of resolving the junction back to the long path.

CARGO_TARGET_DIR is also owned by the preparation environment and points at
<work>\cargo, so Rust sdists link under the work root on every target even
if a tool resolves the junction.
2026-10-22 17:47:42 -07:00
ethernet c073ce29c1 fix(tests): keep the Nix shell's PYTHONPATH through run_tests.sh's env -i
The devShell's HERMES_PYTHON is the lock's interpreter, which finds pytest and
the uv2nix venv only through PYTHONPATH. env -i dropped it, so every file failed
with 'No module named pytest'. Only the unactivated HERMES_PYTHON branch keeps it;
the activated test environment stays hermetic.
2026-10-22 17:25:38 -04:00
ethernet 75a8d6fcf4 fix(deps): update uv to 0.13.5 2026-10-22 17:25:38 -04:00
ethernet 8c3734f1d7 fix(desktop): fresh Windows hosts can unpack Electron 40.10.3+ without the VC++ redist
From 40.10.3 on, every Electron line unpacks its binary with
@electron-internal/extract-zip, an MSVC-built binding that imports
VCRUNTIME140.dll. A Windows host that never installed the VC++
Redistributable has no copy, so install.js dies with ERR_DLOPEN_FAILED.
This is the failure that made bb8280b753 roll Electron back to 40.10.2.

Our Python ships vcruntime140.dll. source_build_env() passes its path to
the Node builders as HERMES_VC_RUNTIME, and electron-runtime.mjs copies it
beside the extractor's binding right before it runs install.js. Node
loads bindings with LOAD_WITH_ALTERED_SEARCH_PATH (libuv uv_dlopen), so
Windows finds that copy first. This is the same app-local copy
fdea3e92e0 ships beside ARM64 rg.exe.

Clean Windows Sandbox (ARM64, no redist), Node 26.7.0, electron 44.9.3:
- before: install.js exits 1, "Cannot find native binding", ERR_DLOPEN_FAILED
- after: install.js exits 0, dist/electron.exe --version prints v44.9.3
2026-10-22 16:17:57 -04:00
ahrazzle a9376424e5 fix(desktop): prepare the locked Electron runtime explicitly before use
Electron 42 dropped the postinstall that unpacked its binary into
node_modules/electron/dist. The binary now appears on the first
require('electron'), so parallel vitest workers race to unpack it
("Text file busy" in #128267's CI), and `hermes desktop --source` finds
no path.txt to launch.

electron-runtime.mjs runs Electron's own install.js once: after npm ci
for the desktop workspace, and in CI before the parallel checks. It first
checks the installed package against the desktop pin and the lockfile,
and does nothing when the runtime is already in place. On the current
40.10.2 pin, whose postinstall still unpacks the binary, it is a no-op.

Salvaged from #137648 (the runtime-preparation subset only).
2026-10-22 16:17:57 -04:00
Maciej Lisiewski 41dfcf2c1b Merge pull request #142831 from NousResearch/deps/pillow-13.0.0
fix(deps): bump Pillow 12.3.0 → 13.0.0 (critical, GHSA-8g5g-8vmr-9782)
2026-10-22 17:56:19 +02:00
ethernet 0153a9e460 refactor(ci): simplify cache report state and rendering
Use one escaped HTML fragment for the report and review comment instead
of maintaining separate Markdown and HTML table builders. GitHub's
renderer preserves the same table text and all 39 real job/step links.

Remove the obsolete phase-generic accounting helper, redundant attempt
list and save-evidence flag, and unused collected-job counter. Append
main's saved column in one place.

Use urllib's unredirected Authorization header instead of a custom
redirect handler. The real HTTP redirect test still proves that only
the API request receives the token. Exercise PM formatting directly,
while retaining the actual stdlib-only CLI invocation test.

Verification: 696 CI tests pass, one native skip, and all 12 checks pass.
Saved real-log parser records and rendered values remain unchanged.
2026-10-22 11:55:58 -04:00
ethernet 4f53b89a47 ci: summarize cache hits with main-aware job details
put cache hits/attempts and restored/missed archive bytes above an expander.
show hit and miss job/step links in both Markdown and HTML. off main,
compare hits with main's latest measured archive and hide write figures;
on main, show saved bytes without self-comparison.

retain unknown outcomes and sizes instead of treating them as misses or
zero. miss-byte totals use main archive references off main, and newly
saved archive sizes on main. preserve the real run mode and step numbers
in the snapshot so artifact-only rendering keeps the same behavior.
2026-10-22 11:55:58 -04:00
ethernet 19ed6e147c ci: compare cache archive transfers in the review comment
Collect ordinary actions/cache job logs alongside the existing timing
snapshot and retain per-invocation restore/write receipts and archive bytes.
Compare each cache with main and show saved-minus-restored archive changes
in the review comment, Actions summary and complete HTML report.

Count successful writes only after their save receipt; transfer progress
supplies a size once, never a sum. Preserve unknown sizes, failed or missing
logs, lookup-only results and exact-hit save skips. Match content-keyed
writes to their restored paths, without inspecting cache contents or using
the GitHub inventory as a proxy for a runner provider's cache store.
2026-10-22 11:55:58 -04:00
ethernet f3ea0290e2 ci: show PM cache restore results and write policy
Print a compact tools/uv/npm summary from setup-pm, including the selected
restore outcome, exact-hit output, cache path, ref scope and save policy.
Keep actual archive sizes and upload results in the native cache logs.
Do not scan cache trees or add logging wrappers to non-PM cache actions.
2026-10-22 11:55:58 -04:00
Maciej Lisiewski 2b31d12ade fix(pm): apply lock overrides to requirements builds; read pip check uncoloured
Requirements builds (Termux deb assembly and wheelhouse gate, PM runtime
wheelhouse install) install the lock's export with uv pip install, which
re-resolves and rejects the overridden graph. Pass the lock: its manifest
overrides become --overrides and the closing check accepts them.

FORCE_COLOR/CLICOLOR_FORCE made uv pip check emit ANSI the report parser
cannot read, failing every install with an expected override mismatch.
2026-10-22 17:36:39 +02:00
Teknium 99cc712bf3 fix(update): a damaged .git no longer strands paused gateways
When a checkout's .git exists but git rejects it (empty HEAD, a gitdir:
file pointing nowhere), every `hermes update` failed at the fetch with a
generic "Failed to fetch updates" and, on Windows, left the paused
gateways stopped with "the next `hermes` command restarts them". No
later command could: the whole-tree gate held the set while HEAD was
unreadable, so bots stayed down until someone repaired the checkout by
hand.

- tree gate: a set whose every pause already found HEAD unreadable and
  that never bound a tree move resumes. The gateways were serving that
  exact tree and no update step wrote it. A recorded move, or a HEAD
  that was readable at the pause, still holds the set.
- fetch failure: "not a git repository" now says the checkout's git data
  is damaged and prints the installer one-liner that repairs it.
- install.sh: a dangling `.git` file is moved aside and re-cloned like a
  commit-less one, matching install.ps1 (Test-Path already matched it);
  both installers' warning names a damaged .git, not only an
  interrupted clone.
2026-10-21 16:57:37 -07:00
yoniebans 3e6589fc34 ci: run the upgrade suite when hermes_state_fts.py changes
The hermes update search-index notice now reads the stored FTS layout through hermes_state_fts.fts_layout_drift, so the module is on the update path.
2026-10-21 04:14:08 -07:00
ethernet c4c42bc72e Merge pull request #141741 from NousResearch/ci/pr-restore-only-pm-caches
ci: pull requests restore main's PM caches instead of saving their own
2026-10-20 18:00:40 -04:00
ethernet d6d77c3649 Merge pull request #141725 from NousResearch/feat/store-auto-publish
feat(release): Store certifies before the gate; publication switches it live
2026-10-20 17:43:36 -04:00
ethernet 01af57a06c Merge pull request #141716 from NousResearch/perf/release-metadata-only-receipts
perf(release): stage candidates from metadata; verify package bytes at publish
2026-10-20 17:31:33 -04:00
ethernet ee8f05a48a Merge pull request #141651 from NousResearch/fix/desktop-prepare-caches
fix(desktop): Windows prepare reuses its restored wheel, vcpkg and npm caches
2026-10-20 16:03:44 -04:00
ethernet 13cb8cca17 ci: pull requests restore the uv and npm caches without saving their own
save-python-cache and save-node-cache now default to false on
pull_request events, like save-tools-cache. Their restore-only steps
read the exact keys the saving steps write on main, so a PR gets main's
uv and npm caches and leaves nothing PR-scoped behind.

Restore-only used to mean an explicit-save flow: run- and job-scoped
keys meant for save-pm-cache or a caller's actions/cache/save to write
later. Nothing calls save-pm-cache, and the last caller restoring from
the npm explicit-save key moved to its own cache, so the flow and its
outputs are deleted.
2026-10-20 15:50:30 -04:00
ethernet 4148f6d1f3 feat(release): Store certifies before the gate; publication switches it live
stable-store commits the Store submission held (targetPublishMode Manual)
instead of leaving an uncommitted draft, so certification runs before the
release gate and a run whose stable-publish fails still shows customers
nothing. The publication pass then PUTs the held submission with
Immediate and commits it again, so it goes live without a Partner Center
click.

Microsoft documents no call that releases a held submission and does not
say whether update accepts a committed one (409 is documented only as
"the current state of the app"). A 409 from the switch leaves the
submission held and prints the Publish now instruction; the release
stays green. The first real release shows which answer the API gives.

Reconcile receipt-bound Store work independently of channel advancement, including
already-current published releases, so an interruption cannot strand a held
submission. Record each upload's claim in certification notes and replace a held
Manual predecessor only after its exact remote abandon marker matches the claim
commit. Unknown owners and publishing submissions remain protected.
2026-10-20 15:50:30 -04:00
ethernet fa6a127ed6 perf(release): stage candidates from metadata; verify package bytes at publish
candidate-manifest spent ~4 of its 5.2 min downloading 8.7 GB (stable run
38491090442: both MSIX bundles, including the 4.3 GB Store bundle) only to
re-hash them, and each stage-receipt job re-downloaded its group's ZIPs and
bundle (60-256 s). The same bytes are hashed by the build job that uploaded
them, bound by digest in its handoff receipt, and downloaded and digest-
checked again by release_artifacts.publish (materialize) before anything
goes live.

Candidate staging and receipts now download only the metadata JSONs; package
rows still carry the receipt digests. The one check that read package
contents, the universal bundle identity against its per-arch rows, moves to
publish, after materialize has verified the bytes. A Store-only claim has no
metadata to fetch, so candidate-manifest reads its receipts alone.
2026-10-20 15:50:30 -04:00
ethernet 550e2a891f feat(release): publication commits the Store draft with auto-publish on
The Store build used to be committed at green with targetPublishMode
Manual, and the publication pass could only print "click Publish now":
the submission API has no call that releases a held submission (update
and commit return 409 once it is committed). Every stable release needed a
person in Partner Center.

stable-store now uploads the verified bundle as an uncommitted draft
(msstore publish --noCommit) and stops. The publication pass, which runs
only when the release really goes out, reads the receipt-bound candidate
manifest, checks the pending draft carries that release's Store bundle,
sets targetPublishMode Immediate and commits it. Certification starts
then, and the build goes live when it passes.

- A draft from a newer green run is left for its own release; a committed
  or live submission is a no-op, so reruns are safe.
- submit refuses to replace a committed submission still on its way live
  (an earlier release); msstore publish would delete it.
- complete's Reconcile step now receives the MS_STORE_* credentials; it had
  none, so the success path silently skipped the Store.
- A failed or unreadable Store step stays a warning: the release publishes.
2026-10-20 15:50:30 -04:00
ethernet 84b1f05313 fix(desktop): read VCToolsVersion BOM-tolerantly; match vswhere as a path
Windows CI on #141651:
- footguns lint: Microsoft.VCToolsVersion.default.txt is read with
  'utf-8', which keeps a BOM in the version string. Use 'utf-8-sig'
  (and write a BOM in the test fixture, as Visual Studio does).
- Windows-only test: locate_command joins the known dir with mixed
  separators (".../Installer\vswhere.exe"), so the stub's string
  comparison never matched, vswhere looked absent, and both calls fell
  back to a random partition. Compare as paths.
2026-10-20 15:50:30 -04:00
ethernet ec64d1440e refactor(desktop): find vswhere through hermes_platform
Review (#141651): _msvc_identity hand-wrote the vswhere.exe install path,
which is the known-path table the hermes_platform rule keeps in one place.
Add visual_studio_installer_dirs() to resolver/known_dirs.py and locate
vswhere with locate_command (PATH first, then the installer directory).

Return None explicitly when vswhere reports no installations, before selecting
the newest instance.

Warn on stderr with the reason whenever MSVC identity detection returns None:
missing vswhere, no matching installations, or a failed probe/version read.
2026-10-20 15:50:30 -04:00
Maciej Lisiewski ba78364593 fix: update tag selection to work without local refs 2026-10-20 20:36:30 +02:00
kshitijk4poor 9ca23c6840 fix(cli): run every /cron form on the gateway that owns the cron store
Symptom: in a classic `hermes chat` session on the shared gateway, /cron
add|edit|pause|resume|run|remove still ran main's handler in the CLI
process, which called the cronjob tool there and wrote the profile's
cron/jobs.json from the client. Under strace, every write to jobs.json
came from the `chat --cli` process and none from the gateway.

Root cause: the one-gateway runtime (#106742) made the gateway the
owner of the profile's runtime state and of the ticker that fires cron
jobs. It left CLICommandsMixin._cron_* in the client, and the
gateway's slash.exec refused /cron for every local session except TUI
reads (2efe028116). That left the cron store with two writers.

Change:
- Move main's /cron handler (overview, list and every write) into
  hermes_cli/cli_cron_lines.py as line builders (cron_command_lines),
  with no other behaviour change. The CLI prints them when it has no
  gateway session. The unused CLICommandsMixin._cron_list goes; its
  test and eval read cron_list_lines directly.
- On the gateway, /cron is an owner command
  (gateway/session_cron_commands.py). Bare /cron and `list` are reads
  for every local session; that includes Desktop- and ACP-created
  sessions, which main refused with unsupported_command. Every other
  form is an admission-fenced control. An unclosed quote is refused as
  invalid_params before pre_command fires. `/cron list -a` keeps the
  list-all alias that the TUI read path accepted.
- A /cron write runs only when it arrives on a connection that is not
  attached to the session: the classic CLI's own command socket
  (GatewayConnection.call_until), which waits with no deadline. A
  connection attached to the session keeps main's unsupported_command.
  That is how Ink, Desktop and ACP send /cron: on their main
  connection, which the gateway answers one request at a time and
  which waits with a deadline, while a /cron run lasts as long as its
  job. The gate is on the requesting connection, not on the surface
  that created the session, so a classic CLI that resumed a TUI-made
  session still writes (as on main), and Ink on a CLI-made session is
  still refused.
- The classic CLI sends /cron to slash.exec
  (gateway_setting_commands.GATEWAY_SETTINGS) with no deadline, on a
  socket of its own (gateway_plugin_commands.call_unbounded, now shared
  with plugin commands; its Ctrl+C handle is renamed
  _gateway_command_wait). Ctrl+C prints the stopped line and
  propagates.
- Regenerate the parity table. test_pre_command_in_gateway pinned the
  old refusal of every classic CLI /cron; it now pins that the accepted
  /cron forms fire pre_command once and a refused one (an unclosed
  quote) fires nothing.

Why: the design contract allows one writer per owned store, and
clients only read and submit. The gateway already serves this
profile's cron store to its ticker and to Desktop (cron.manage). Now
the classic CLI's writes go through the same process.

Refs #134889 (T1)
Refs #139158
2026-10-20 13:08:24 +05:30
ethernet 5d8bf6eaf0 chore(desktop): time each native payload staging phase
prepare showed 79-197 s of silence between the repo snapshot and the
pm-runtime venv (snapshot extract, pinned tool preparation, the copy +
re-digest of every tool into the payload store, store pruning). Name each
phase with its duration, and each copied tool, so the next run says which.
2026-10-19 12:30:43 -04:00
ethernet 58801c565b perf(ci): stop transporting node_modules in the desktop dependency snapshot
node-deps runs npm ci on every prepare (the receipt never matched across
runs in any sampled job), and npm ci deletes node_modules first. The
restored trees were GBs of tar/untar for nothing; npm's content cache
(npm/_cacache) is what lets npm ci skip downloads. The workspace path
validation only existed to guard those paths, so it goes too.
2026-10-19 12:30:42 -04:00
ethernet e97ce2a457 fix(desktop): Windows native wheels and vcpkg OpenSSL reuse the restored cache
Two caches were restored every run and then never read.

uv's native-wheel partition (native_cache_path) needs a toolchain identity
to be stable. win32-x64 never runs VsDevCmd, so VCToolsVersion is unset and
the identity fell back to a random uuid: every run compiled cryptography,
obstore, davey and friends from source into a fresh partition (all 6
sampled x64 runs had a new partition). Identify MSVC the way rustc's cc
crate finds it, through vswhere and the default VCToolsVersion file.

vcpkg's default binary cache lives under LOCALAPPDATA, which the desktop
preparation moves into the job-local home, so OpenSSL rebuilt every arm64
run ("Restored 0 package(s)", ~3 min). Point VCPKG_DEFAULT_BINARY_CACHE at
native/vcpkg-archives, inside the transported native domain.
2026-10-19 12:30:42 -04:00
Teknium 71b45e13aa docs(gateway): the parity note for /save names Desktop's client-side write
The generator's /save note still said the export file is written on the gateway host; Desktop with
a current gateway saves session.export text through its own dialog. Fix the note in
scripts/gen_gateway_command_parity.py and regenerate the table.
2026-10-18 20:50:00 -07:00
kshitijk4poor 3752497cc4 refactor(update): one deferral core, shared reason prefix, per-line Desktop hint; review folds
Folds the final-head review and /simplify-code pass on #141070:

- update_serve_obligations: defer_manual_serve and defer_unit_serve share
  _defer_identified (pid/create-time guard, dead-pid discharge, reminder
  write) instead of two copies; a unit row's failed-write warning says
  "unit restart reminder", not "manual".
- update_cmd_fleet: a positive _is_serve_or_dashboard_unit name gate,
  composed into _is_hermes_gateway_unit; _is_serve_unit uses it rather
  than "gateway gate minus SIGUSR1 support". _SERVE_SKIP_REASON names
  hermes-dashboard* units too.
- update_cmd_fleet_verify: SERVE_ONLY_REASON_PREFIX and the gateway
  sentence are module constants, so the producer and the Desktop
  predicate share one spelling; the test now feeds the producer's own
  reasons to both scripts.
- Desktop hand-offs (posix.sh, windows.ps1): drop the gateway hint only
  when EVERY gateway_restart line is serve/dashboard-only, so a serve-only
  line next to a raw restart-phase error keeps it; case-sensitive in
  PowerShell (-cmatch) like grep.
- main_dashboard._format_restart_commands replaces three copies of the
  backticked " or " join.
- Docstrings: already_restarted_units covers declined and failed units.
2026-10-19 02:48:18 +05:30
kshitijk4poor 3c23e52ae3 fix(desktop-update): add the gateway hint only when the follow-up blames gateways
posix.sh and windows.ps1 appended "Run hermes gateway restart" whenever
the gateway_restart step was owed. Since this PR, that step can be owed
for a serve/dashboard unit alone (a system hermes-dashboard the updater had
no root for), and the hint then sends the user to a command that never
touches that unit. Both hand-offs now drop the hint only when the owed
reason is serve/dashboard-only: it starts with "serve/dashboard" and does
not mention `hermes gateway restart`. Every other gateway_restart reason
keeps the hint, including the raw exception text update_finish owes when
the restart phase itself crashes and reasons from older CLIs. A
dashboard-only owed restart points to the update log, which names the
unit's own command. The step id and owed-step parsing are unchanged.

Fixes #141071
Refs #141021
2026-10-19 02:48:18 +05:30