Commit graph

893 commits

Author SHA1 Message Date
fcavalcantirj
f04fd1e7ad fix(update): test runs never mutate the live checkout — pytest-guard the marker and repair paths
Guard the .lazy-refresh-incomplete marker writer (update_cmd), launch-time
recovery (main.py), and _early_recovery repair paths behind a two-condition
check: running under pytest AND the target is this live checkout. Sandboxed
tmp_path tests still exercise the real code paths.

Salvaged from PR #72002 by @fcavalcantirj. Fixes #72000.

Co-authored-by: fcavalcantirj <felipe.cavalcanti.rj@gmail.com>
2026-07-29 21:30:53 -07:00
Teknium
ad12df6ba4 Revert "remove Vercel AI Gateway and Vercel Sandbox (#33067)"
This reverts commit febc4cfec0.
2026-07-29 19:48:37 -07:00
brooklyn!
4b7f709843
Merge pull request #74436 from NousResearch/bb/update-mutex
fix(update): one updater at a time, and never roll an install backwards
2026-07-29 18:15:17 -05:00
Brooklyn Nicholson
fe8e4d93da fix(update): make the in-progress marker a real cross-process lock
Three surfaces start updates against one checkout: a terminal
"hermes update", the dashboard's Update button (which spawns that same
command detached), and the desktop's, which hands off to the Tauri
updater. Only the Tauri updater published the in-progress marker, and
only Electron read it -- to gate backend startup, not to stop a second
updater. So a dashboard-spawned update and an installer-driven git
checkout could mutate the same tree concurrently, rewriting source under
a live interpreter.

Claim the same marker from cmd_update rather than adding a second
mechanism: same path, same pid+started_at payload the Rust and Electron
readers already parse. A marker only counts as live when its pid is alive
and it is inside the shared age ceiling, so a crashed updater self-heals
instead of wedging every future update. Release only removes a marker we
still own, leaving a handoff partner's claim intact.

Refusing exits 2, matching the existing concurrent-instance contract the
Tauri updater already recognizes.
2026-07-29 17:45:48 -05:00
Ben Barclay
f5b68ad58b Merge origin/main into feat/hsp-sync-client
Resolves the PR's conflict with main (2252 commits). Two conflicts, both
"each side added an independent block in the same place" — kept both:

- gateway/run.py — the housekeeping loop. This branch adds the Skill Sync
  pulls inside the CURATOR_EVERY branch (12-space indent); main adds a
  stale-session auto-archive as a sibling `if` at loop level (8-space).
  Different scopes, so the naive union would have mis-nested the archive
  block into the curator branch; kept each at its own indent level.
- tools/skill_manager_tool.py — the _edit_skill result dict. This branch
  appends the org auto-propose note; main appends
  _add_description_prompt_preview(). Independent, order-insensitive.

No behaviour dropped from either side.

Verified: 3552 passed / 0 failed across 63 suites (scope regenerated to
include main's new maybe_auto_archive / _add_description_prompt_preview
consumers) via scripts/run_tests.sh. `hermes sync` and `hermes sync status`
still work against a live token, resolving the production plane default.

The Pyright Optional-parameter warnings in skill_manager_tool.py are
pre-existing on main (`content: str = None` etc.), not introduced here.
2026-07-29 13:01:36 -07:00
teknium1
bcb352eeab refactor: registry-owned execute() on CommandDef — informational commands unified (thin slice) 2026-07-29 12:11:24 -07:00
teknium1
c64a4d75e5 refactor: extract dashboard process-hygiene helpers to dashboard_procs.py 2026-07-29 10:59:54 -07:00
teknium1
0e7c4018f7 refactor: hoist cmd_sessions out of main() into sessions_cmd.py 2026-07-29 10:59:54 -07:00
teknium1
927463efcc refactor: extract update pipeline to hermes_cli/update_cmd.py (mechanical move) 2026-07-29 10:59:54 -07:00
teknium1
fedd689d37 perf(config): one raw config.yaml parse per process instead of 3-4
Counted with an open()/read_text audit hook on a real 'hermes --version'
run: config.yaml was parsed 3x before load_config() even ran — once by
env_loader._load_secrets_config, once by main.py's early redact/ipv4
bridge (bespoke yaml.load), once by hermes_logging._read_logging_config.
Each raw parse is 1-3 ms with libyaml plus an open/stat — pure
duplication since all three want the same raw dict.

All three now route through read_raw_config()'s existing (mtime_ns,
size)-keyed shared cache:

- env_loader._load_secrets_config: uses the shared reader when reading
  the process HERMES_HOME (the cache key's home); other homes (profile
  seeding) keep the isolated direct parse. Parse-error isolation is
  preserved — the shared reader also swallows errors and returns {}.
- main.py early bridge: drops the bespoke yaml.load for read_raw_config
  (managed-scope overlay unchanged).
- hermes_logging._read_logging_config: prefers the shared reader,
  falls back to the direct fast_safe_load parse when hermes_cli.config
  isn't importable.

Measured: config.yaml opens per 'hermes --version' 3 -> 1.
348 targeted tests green (env_loader + secret sources + applied-homes +
bitwarden + hermes_logging + config).
2026-07-29 10:32:25 -07:00
teknium1
3a69e34702 perf(update): cut redundant network + subprocess work from hermes update
Four fixes on the updater path:

1. uv self update freshness gate + timeout (managed_uv.py): the network
   self-update ran on EVERY hermes update — including the 'Already up to
   date!' fast path — with NO timeout (unbounded hang risk offline). Now
   skipped when it succeeded within 7 days (stamp file under
   HERMES_HOME/cache), capped at 60s, force= override available. The
   CVE-driven vulnerable-runtime repair probe is NEVER gated — it still
   runs on every invocation.

2. Drop the second network fetch from the pull step (main.py): the update
   flow fetched origin/<branch>, counted commits, then ran
   'git pull --ff-only origin <branch>' — a SECOND fetch of the same ref
   (~0.5-1.5s). Now merges the already-fetched tracking ref via
   'git merge --ff-only origin/<branch>'; the diverged-history reset
   fallback is unchanged.

3. Probe the upstream remote locally before fetching it (_cmd_update_check):
   non-fork installs have no 'upstream' remote, and --check burned a
   failed network attempt (~0.3-1s) on every run before falling back to
   origin. 'git remote get-url upstream' (~1ms local) now gates the fetch.

4. Desktop rebuild check reads the content-hash stamp in-process before
   spawning 'hermes desktop --build-only' (a full CLI re-import, ~1-3s)
   just to learn nothing changed. Stamp errors fall through to the
   subprocess path unchanged.

Savings on a no-op 'hermes update': ~2-6s (uv self-update 0.5-3s +
second fetch 0.5-1.5s + desktop spawn 1-3s when applicable).
187 targeted updater tests green incl. 5 new stamp-gate tests.
2026-07-29 10:16:44 -07:00
teknium1
3d48f893da refactor: single build_subprocess_env() factory for all child-process spawns (profile + secret-scrub single owner) 2026-07-29 10:14:11 -07:00
teknium1
19055492aa fix: route stray HERMES_HOME hardcodes through get_hermes_home() (profile + native-Windows safety) 2026-07-29 09:33:48 -07:00
Teknium
7de33cc57e
Merge pull request #64536 from victor-kyriazakos/feat/gateway-health-diagnostics
feat(monitoring): gateway health & diagnostics OTLP export
2026-07-29 08:58:33 -07:00
Teknium
c19fd5c505 fix(cli): wire hermes import-agent parser into main argparse tree
The import-agent subcommand (24c3c27ba8) shipped with its parser builder
and handler logic but build_import_agent_parser() was never called from
main.py, making the documented and unit-tested command uninvocable.
Register it alongside the other subcommand builders.
2026-07-29 08:48:05 -07:00
victor-kyriazakos
1773752c8c Merge remote-tracking branch 'origin/main' into feat/gateway-health-diagnostics-monitoring
# Conflicts:
#	uv.lock
2026-07-29 15:37:14 +00:00
Bao
c4d9fbacb3 fix(cli): honor pooled credentials in model wizard
Signed-off-by: Bao <nnqbao@gmail.com>
2026-07-29 10:56:19 +05:30
Brooklyn Nicholson
812b75fdfc fix(desktop): refuse hardened-runtime sign when entitlement plists are missing
Hardened-runtime restrictions are enforced even for ad-hoc signatures,
so signing with --options runtime without the allow-jit entitlements
would leave Electron/V8 crashing on launch — strictly worse than the
legacy plain ad-hoc sign. Raise instead, so the fixup falls back to the
legacy path and the bundle always stays launchable.
2026-07-28 17:34:43 -05:00
Brooklyn Nicholson
5d171ffbbe fix(desktop): stable macOS signing identity so TCC grants survive rebuilds
Local/self-updated macOS builds were finished with a plain
'codesign --force --deep --sign -', leaving a cdhash-only Designated
Requirement and stripping electron-builder's entitlements. Every rebuild
changes the cdhash, so TCC treats the new bundle as different code and
forgets Full Disk Access, Desktop/Downloads/Documents, Accessibility,
Automation, and microphone grants — users re-approve everything after
every update.

Rework the relaunch fixup to sign inside-out (standalone Mach-O
binaries, nested frameworks/helpers, then the main bundle), preserving
the repo's entitlement plists, and pin an identifier-based Designated
Requirement when signing ad-hoc so TCC has a stable identity to persist.
Opt-in desktop.macos_signing_identity names a persistent keychain cert
(self-signed Code Signing cert works — no Apple Developer account) for a
certificate-anchored DR, the strongest form. An intact Developer ID
signature is detected and never clobbered, callers can pass the
publisher-signing decision explicitly so a later dotenv load can't flip
it, and the legacy deep ad-hoc sign remains the last-resort fallback.

Co-authored-by: lewis4x4 <lewis4x4@users.noreply.github.com>
Co-authored-by: natebransc <natebransc@users.noreply.github.com>
Co-authored-by: caseyanthony <caseyanthony@users.noreply.github.com>
Co-authored-by: gvago <gvago@users.noreply.github.com>
Co-authored-by: twe-cloud <twe-cloud@users.noreply.github.com>
2026-07-28 17:28:32 -05:00
Ben Barclay
f9c4d835f9 refactor(sync): name the access gate for what it is (Nous admin)
The client called its gate "the DEV-PHASE gate (tool_gateway_admin)", which
reads as though Skill Sync is gated on an unrelated service's admin right.
It isn't. NAS populates that claim from Permissions.ADMIN_ACCESS — the global
portal admin permission that guards /admin/* — so the gate is "is this user a
Nous admin?". The claim is simply named for its first consumer, the tool
gateway.

Renamed on this side to say what it means, while keeping the wire string
(other services read it):

- DEV_GATE_CLAIM -> NOUS_ADMIN_CLAIM (value unchanged: "tool_gateway_admin",
  with a comment recording why the wire name differs).
- identity/status key dev_gate_ok -> nous_admin, across the client, the CLI
  consumers, and the tests.
- The module docstring now states where the claim comes from, that the wire
  name is misleading, and that this gate is pre-launch containment rather
  than the shipping entitlement — admin status conflates "may administer
  Nous" with "has Skill Sync enabled" and has no middle setting for a beta
  cohort. Choosing the real entitlement is left as a separate decision.

Naming only — no behaviour change, and no change to which accounts can sync.
The user-facing messages stay deliberately vague ("not enabled for your
account yet") rather than telling users they need portal admin.

Verified: 2346 passed / 0 failed across 56 suites via scripts/run_tests.sh;
`hermes sync status` against a live token reports "nous_admin": true. Zero
stale dev_gate_ok / DEV_GATE_CLAIM references remain.
2026-07-29 07:57:25 +10:00
Ben Barclay
4f990ec09e refactor(sync): put every Skill Sync verb under hermes sync; drop HSP naming
Encapsulates the feature behind one command for launch, and adopts the
official product name.

One command:
- `propose` moves from `hermes skills propose` to `hermes sync propose`, so
  the whole feature is one command to learn and one to document. Its handler
  moves from cmd_skills to cmd_sync accordingly.
- The `hermes sync` parser now documents both halves plainly: personal sync
  across your devices, and sharing with your organisation. Added an examples
  epilog; rewrote the verb help in user language ("Include a skill in your
  sync" rather than "Opt a skill into sync").
- Every user-facing string that pointed at `hermes skills propose` now points
  at `hermes sync propose` (8 sites, including the agent-visible guidance
  returned by skill_manage and the org provenance header).

This also clears the way for #39343, which adds its own top-level `sync` for
git-repo profile backup — that feature nests under `skills`, this one owns
`sync`.

Naming:
- HSP / "Hermes Sync Protocol" is gone from prose, docstrings, and comments.
  The feature is "Skill Sync".
- Public identifiers renamed: HSPClient -> SyncClient, HSPError -> SyncError,
  HSPConflict -> SyncConflict, hsp_address -> wire_address, HSP_VERSION ->
  WIRE_VERSION.
- The WIRE names are deliberately NOT renamed: the `hsp_version` capability
  field and the `x-hsp-object-type` response header are set by the deployed
  gateway-gateway sync plane (verified in src/sync/syncRouter.ts), so
  renaming them client-side would break sync against a live server. A comment
  at the version constant records why they differ from the product name.
- The version-mismatch error is now actionable ("this server speaks sync
  version X, but this Hermes speaks Y — update Hermes to sync with it")
  instead of leaking the protocol acronym.

Also fixes a wiring gap found on the way: the gateway housekeeping tick
pulled personal skills but never org skills — the same defect already fixed
for the CLI. Org pull now runs there too, gated on real org membership.

Tests: the jargon guard now also fails on a bare "HSP". The two tests that
asserted the old cross-command structure are replaced by three asserting the
new one (propose IS under sync, propose is NOT under skills, sync usage
lists it). 2294 passed / 0 failed across all 51 suites that import the
changed modules, via scripts/run_tests.sh.

Verified by running the real CLI: `hermes sync --help` lists all eight verbs,
`hermes skills --help` no longer mentions propose, `hermes sync propose
--help` parses, and `hermes sync status` still reports live org state.
2026-07-29 07:47:06 +10:00
Teknium
38cd253abd feat(update): self-heal the hermes-acp launcher + document Buzz Desktop as an ACP host
Existing installs predate the install.sh hermes-acp launcher, and hermes
update never re-runs setup_path, so ACP hosts (Zed, JetBrains, Buzz)
still resolve Hermes as unavailable until a reinstall. _ensure_acp_launcher()
writes the launcher next to an existing hermes command in ~/.local/bin or
/usr/local/bin during hermes update — delegating to the sibling launcher so
it is correct for every install layout. Never follows symlinks (#21454),
skips unwritable dirs, no-op on Windows (venv Scripts is already on PATH).

Docs: add a Buzz Desktop section to the ACP page (en + zh-Hans).
2026-07-28 11:53:12 -07:00
Bartok9
bff3aa3c1e fix(cli): resolve deferred platform plugin for its top-level CLI command
Closes #54678

`hermes photon ...` could fail with argparse `invalid choice: 'photon'`
even when the bundled Photon platform plugin is present. Photon registers
its top-level CLI command from the platform adapter module via
`ctx.register_cli_command(name="photon", ...)`, but bundled platform
plugins are cheap-registered as *deferred* entries to avoid importing every
gateway SDK during normal startup.

On the unknown-top-level-command slow path, `discover_plugins()` records the
deferred loader but never imports the matching platform module, so the CLI
registration side effect doesn't run and `photon` stays absent from
`_cli_commands` — argparse then rejects it.

Fix: after `discover_plugins()` on that slow path, resolve only the deferred
platform whose name matches the first positional token (via
`platform_registry.get(name)`) before reading `_cli_commands`. This imports
exactly the targeted platform, leaving normal startup cheap (a bare `hermes`
or flags-only invocation has no positional token and touches nothing). The
resolution is best-effort: registry/import failures are logged at debug and
never crash startup.

Added 3 tests in tests/hermes_cli/test_startup_plugin_gating.py: resolves the
matching platform, ignores empty/None command, and swallows registry errors.
Fails without the fix (symbol absent).
2026-07-28 10:49:55 -07:00
Victor Kyriazakos
6a174e9967 Merge origin/main into feat/gateway-health-diagnostics
# Conflicts:
#	cron/executions.py
#	cron/jobs.py
2026-07-28 13:09:17 +00:00
Ben Barclay
981feb6730 feat(skills): org skills are editable in place; local edits survive org updates
The read-only org mirror broke the learning loop precisely where it matters
most. The system prompt tells every agent to patch a skill the moment it
finds a gap, and shared skills are the ones the most people use — but every
write to _org/ was refused, and the curator was excluded from them outright.
So org skills froze while personal skills kept improving, and the offered
alternative ("fork it into a personal skill, then propose the fork") is not
something an agent does mid-task. The refusal WAS the feature; improvements
were simply lost, and manual forks would have fragmented the shared set.

Edit in place:
- skill_manage patch/edit/write_file now work on org skills. Only delete is
  still refused (the mirror is a view of org HEAD — a local delete returns on
  the next pull; removing a shared skill is an admin action).
- Org skills are curation-eligible again, so the curator can improve the
  highest-leverage skills in the system instead of skipping them.
- The load-time provenance header now says edits are allowed and kept,
  instead of instructing the agent not to edit.

Local edits are never overwritten:
- pull_org_skills previously rmtree'd each skill dir and re-materialized it,
  silently destroying local work on the next session start. It now records a
  content fingerprint per skill (.org-baseline.json) when it writes one, and
  SKIPS any skill whose local content diverges from that baseline.
- When upstream ALSO changed such a skill, it is reported in the pull
  result's "conflicted" list and left untouched for the user to resolve
  deliberately (propose the local version, or delete it and re-pull to take
  theirs). A missing baseline is treated as unmodified so pre-existing
  mirrors do not raise phantom conflicts.
- Fingerprints are content-based (path + bytes, sorted), so a touch/mtime
  change is not mistaken for an edit.

Sharing back:
- Default: the edit stays local and the tool result tells the user to run
  "hermes skills propose <skill>".
- Opt-in sync.org_auto_propose / HERMES_SYNC_ORG_AUTO_PROPOSE submits each
  edit immediately. Defaults OFF — pushing every agent edit to a whole
  organisation is not a safe default. A failed submission never fails the
  edit; the change is saved and can be proposed later.
- "hermes sync status" lists org skills with unshared local edits;
  "hermes sync pull" reports conflicts it declined to overwrite.

Tests: 25 in the namespace suite (was 15). The two that asserted the old
read-only behaviour now assert the opposite. New coverage for edit-applied,
share-back guidance, delete-still-refused, curation-allowed, edit detection,
missing-baseline tolerance, mtime-insensitivity, and the auto-propose
default. 428 passed / 0 failed via scripts/run_tests.sh.

Verified through the REAL pull path against a mock plane: pull v1 -> edit in
place -> upstream ships v2 -> pull leaves the local edit intact, reports the
conflict, and surfaces it in status.
2026-07-28 09:08:04 +10:00
Ben Barclay
e19ac3b745 docs(sync): point users from personal sync to org sharing
`hermes sync` gave no hint that org sharing exists, so there was no path
from 'I want to share this with my team' to `hermes skills propose` — sync
looked like the only sharing surface while being personal-only (it always
CAS-es refs/user/<owner>/HEAD).

- Bare `hermes sync` usage and the `--help` epilog now state that these
  commands are personal-only and name `hermes skills propose <skill>` as
  the org path, noting the approval step and that org skills arrive
  automatically and are read-only locally.
- Scrubbed the remaining internal jargon from the sync module docstring
  (M1-D, DEV-PHASE, HSP/1) missed by the earlier pass, which only covered
  help= strings.

Tests: 2 new guards asserting both surfaces reference the org command.
373 passed / 0 failed via scripts/run_tests.sh. Both outputs verified by
running the actual commands.
2026-07-28 08:19:56 +10:00
teknium1
ea2499bcfe fix(update): close the stale-bytecode class with a launch-time checkout-fingerprint sweep
The stale-.pyc bug class (#6207, #60242, live WhatsApp report: gateway
ImportError 'cannot import name parse_model_flags_detailed' after /update)
has one shared shape: the checkout's .py files change while __pycache__
retains bytecode from the previous revision, and a later process trusts
the stale .pyc.

Update-time clears can never fully close this class: 'hermes update'
always executes the PRE-pull updater code, so hardening added to it takes
effect one update late — and a manual 'git pull' never runs the updater
at all.

Class fix:
- Launch-time guard in main(): compare the checkout fingerprint (cheap
  file reads via _read_git_revision_fingerprint, no git subprocess)
  against a .bytecode-fingerprint stamp; sweep __pycache__ once when they
  diverge. Covers manual pulls, old updaters, ZIP restores — every entry
  point (CLI, gateway service, desktop backend) passes through main().
- Record the stamp at all three update-time clear sites (git path pre-
  install, git path post-install, ZIP path) so a normal update never
  triggers a redundant launch sweep.
- Sibling site: 'hermes plugins update' + dashboard plugin update now
  clear __pycache__ under the plugin dir after git pull (plugin trees
  live outside the repo guard).

E2E validated: reproduced the stale-pyc shadowing (same-size same-mtime
source swap → old symbol wins in a fresh process), confirmed the launch
sweep restores the new symbol, no-ops when the checkout is unchanged,
and re-sweeps on the next pull. Sabotage-tested: reverting the sweep
fails 4 of the new tests.
2026-07-27 11:33:40 -07:00
izumi0uu
d76d0d61d8 fix(update): refresh runtime modules before lazy backends
Refresh update-sensitive modules before lazy backend refresh so an in-place git update does not keep using pre-pull module objects when newly pulled code imports fresh helpers.

Constraint: Issue #60242 reports post-update lazy backend refresh importing stale hermes_constants after a large Windows update.

Rejected: Only clearing __pycache__ earlier | the update process can still hold old modules in sys.modules.

Confidence: high

Scope-risk: narrow

Directive: Keep update-time lazy refresh guarded against in-process code skew after git pull.

Tested: ./.venv/bin/python -m pytest tests/hermes_cli/test_update_autostash.py::test_cmd_update_reloads_runtime_modules_before_lazy_refresh tests/hermes_cli/test_update_autostash.py::test_reload_updated_runtime_modules_restores_new_hermes_constants_symbol -q

Tested: ./.venv/bin/python -m pytest tests/hermes_cli/test_update_autostash.py -q

Tested: ./.venv/bin/python -m ruff check hermes_cli/main.py tests/hermes_cli/test_update_autostash.py

Tested: ./.venv/bin/python -m py_compile hermes_cli/main.py tests/hermes_cli/test_update_autostash.py

Tested: git diff --check

Not-tested: Windows v0.14.0 to v0.18.0 end-to-end update.
2026-07-27 11:33:40 -07:00
Brooklyn Nicholson
690ecfa1c7 fix(update): anchor the web toolchain check on npm's actual search path
Reshapes the salvaged fix so it recovers the `tsc: not found` build without
mis-diagnosing healthy trees.

The npm config forcing is dropped. It rested on the premise that an inherited
`npm_config_omit=dev` can beat the `--include=dev` CLI flag; npm resolves
command-line flags above environment config and filters `omit` by `include`,
so the flag already wins. Verified on npm 10 and 11.5.1: with
`npm_config_omit=dev` set and `--include=dev` passed, devDependencies install.
`npm_config_production` is worse than redundant — npm 9 removed it, so setting
it prints `npm warn config production Use --omit=dev instead.` on every
install.

The readiness probe now reads every root `npm run build` searches, not just
the workspace root. npm links a package's bin shims under the package itself
when it owns its lockfile (#42973), so a root-only check called a working tree
broken: it forced a redundant install, skipped the build entirely, and made
`hermes web` exit 1 on a layout that builds fine today.

The pre-build probe is gone with it. A build that works is never second-guessed
and no filesystem introspection gates it; recovery is driven by the failure
instead. When the build cannot resolve tsc or vite, we reinstall (visibly) and
retry before the generic delayed retry, which otherwise just reruns the same
command and leaves the stale dist in place forever. The lockfile-hash skip
still invalidates on an incomplete toolchain so the next update repairs itself.

Also drops the branches that changed behavior based on whether a test mock was
installed, and replaces the mock-call-count tests with real temp trees covering
both hoisting layouts, the Windows shim extensions, and each shell's wording of
an unresolvable binary.

Co-authored-by: Gerardo Camorlinga Jr. <gercamjr.dev@gmail.com>
2026-07-27 12:52:42 -05:00
Gerardo Camorlinga Jr.
363d1aefa4 fix(update): recover web UI build when tsc/vite missing after npm install
hermes update could report a successful workspace npm ci while
devDependencies were omitted (production/omit-dev env leakage), leaving
tsc/vite unlinked. The subsequent web UI build then failed with
`tsc: not found` and fell back to a stale dashboard dist.

Force npm_config_include=dev on install, verify toolchain shims after
workspace install (and refuse to hash-cache a half tree), repair/reinstall
when node_modules exists without tsc/vite, prepend workspace .bin dirs to
PATH, and retry the build after a tsc/vite-not-found failure.
2026-07-27 12:47:16 -05:00
Ben Barclay
797c52b571 fix(sync): wire org skill pull into the runtime; scrub internal jargon
Two defects found by manual testing on the branch.

1. ORG SYNC NEVER RAN. The org pull/mirror/gating machinery was fully
   implemented and unit-tested but had ZERO runtime callers —
   maybe_pull_org_skills() was referenced only inside a comment, and
   `hermes sync` had no org path at all. Every code path fell through to
   personal sync (refs/user/<sub>/), so org skills never loaded and the
   feature looked like 'everything syncs to my personal org' even with a
   valid org token. The unit tests could not catch this: they invoked the
   functions directly, which is exactly the gap they left open.

   - cli.py session startup now calls maybe_pull_org_skills() alongside the
     personal maybe_pull_skills(), fail-quiet.
   - Auto-pull is gated on real org membership: resolve_org_identity()
     requires an org role on the token, only issued for multi-member orgs,
     so a solo account never reaches the network.
   - `hermes sync pull` refreshes the org mirror too (one pull, both
     surfaces) and reports what it refreshed.
   - `hermes sync status` exposes org_available/org_id/org_role/org_skills
     plus a plain-language summary, so a user can tell whether the org
     workflow applies instead of it being invisible.

2. INTERNAL JARGON LEAKED TO USERS. Help text and errors exposed internal
   milestone/spec coordinates: 'Propose a skill ... (M2)', 'Personal skill
   sync (HSP/1)', 'DEV-PHASE gate closed: your token lacks
   tool_gateway_admin', 'contract §4.3', and an inert message describing our
   internal personal-vs-multi-member design split. All rewritten in user
   language. Feature-local comments/docstrings lost their internal
   coordinates (§N, M1/M2, design.md, PR numbers) while keeping the
   explanatory prose. Pre-existing issue references elsewhere in the tree
   were deliberately left untouched.

Tests: 4 new guards, including two that assert the CALL SITES exist so the
org pull cannot silently become dead code again (verified failing when the
wiring is removed) and one that fails if user-facing help leaks jargon.
344 passed across the sync/skills/prompt suites.

Verified against live staging with a real org token: sync status reports
org_available=true, org_role=OWNER; sync pull performs the org refresh; the
.active_org marker is written with the org id from the token.
2026-07-27 16:40:16 +10:00
Brooklyn Nicholson
28a87d6319 fix(cli): scope -c/--resume to the current workspace
`hermes -c`/`--resume` (continue last session) resolved the globally
most-recently-used session, then cd'd into *its* recorded cwd. So running
`hermes -c` from repo A could land you in repo B's session — the session
you last touched anywhere, not the last one *here*.

Now `_resolve_last_session` scopes to the current workspace first: the git
repo root when CWD is inside a repo (so all sessions across its
subdirs/worktrees group together), else the CWD itself — matching the
`workspace_key` identity `hermes sessions list --workspace` already groups
on. It falls back to the unscoped global MRU when no session matches the
current workspace, preserving the old behaviour for fresh directories.

Adds `workspace_key` param to `SessionDB.search_sessions` and a
`_workspace_key_clause` SQL helper that mirrors `workspace_key()`: a row
matches when its `git_repo_root` equals the key, or (legacy rows without
git metadata) when its `cwd` is at or under it.
2026-07-27 01:26:34 -05:00
Frowtek
a228b81501 fix(sessions): preserve recently active sessions during pruning 2026-07-26 19:30:21 -07:00
LeonSGP43
5645169c8f fix(update): refresh active memory provider deps after venv rebuild
Surgical reapply of #53505 by @LeonSGP43 onto current main (stale-base
cherry-pick conflicted in main.py):

- _refresh_active_memory_provider_dependencies() in hermes_cli/main.py,
  wired into BOTH the git-pull update path (after lazy refresh) and the
  ZIP update path — the provider's plugin.yaml bridge packages are not
  in extras or LAZY_DEPS, so the core reinstall could strip/downgrade
  them and the update flow never healed them (#53272 mem0ai).
- _install_dependencies(force=True) in memory_setup.py: hand every
  declared spec to the resolver on update so missing AND version-drifted
  packages are restored (no-op when satisfied).
- Skip guards: no provider / builtin store / memory.enabled=false.

Widened beyond the original PR for the #70636 half:
- _provider_pip_dependencies(): mode-aware expansion — Hindsight in
  local/local_embedded mode needs hindsight-all (daemon + embedder), not
  just the declared hindsight-client; setup installs it but plugin.yaml
  can't express it, so update-time healing previously missed
  hindsight-embed and the daemon stayed broken.
- Spec-aware import probing: version ranges in pip_dependencies
  (mem0ai>=2.0.10,<3) no longer break the pip-name -> import-name
  mapping.

Fixes #53272. Fixes the hindsight-embed half of #70636.
2026-07-26 19:29:18 -07:00
teknium1
85c2976e22 fix(update): migrate legacy pythonw Windows gateway launchers to the hidden-console design
Two halves close the 'legacy pythonw gateways survive updates forever' gap:

1. hermes update now regenerates the installed Scheduled Task / Startup
   launcher scripts (gateway.cmd + gateway.vbs) during the gateway resume
   phase. They are persistence artifacts written once at install time;
   updates never touched them, so pre-aa2ae36c3f installs kept launching
   the gateway through pythonw.exe forever — every descendant spawn
   flashed a conhost (#54220/#56747) and, since #70344, the console-less
   gateway died at startup with RuntimeError: sys.stderr is None (#71671).
   The task /TR points at a stable script path, so rewriting the files
   retargets it with no schtasks call and no UAC. No-op for modern
   installs; best-effort so a failed refresh never fails the update.

2. _resolve_detached_python() normalizes a legacy pythonw.exe interpreter
   to its sibling console python.exe when it exists, so the update
   pause/resume argv-replay path (and any other caller handed a legacy
   command line) respawns on the current design instead of faithfully
   resurrecting the old one. Keeps pythonw when no sibling exists — a
   failed respawn is worse than a console-less gateway.
2026-07-26 17:50:54 -07:00
teknium1
1e652cca7a fix(cli): register 'approvals' in _BUILTIN_SUBCOMMANDS (startup plugin-gating parity) 2026-07-26 17:49:03 -07:00
teknium1
db90e36202 feat(approvals): hermes approvals suggest — mine approval history into allowlist proposals 2026-07-26 17:49:03 -07:00
konsisumer
486c5ffc8d fix(cli): live-probe dashboard status and share update cleanup across paths
- hermes dashboard --status now verifies each matched PID is alive AND
  bound to a listening socket before reporting it, so stale PIDs and the
  desktop app's IPC-only 'serve --port 0' backends no longer masquerade
  as running dashboards (#58578).
- The git and Windows ZIP update paths share one
  _finish_dashboard_update_cleanup(), so the ZIP fallback gets the same
  stop/restart reporting.
- _kill_stale_dashboard_processes returns a structured
  {matched, killed, failed, unrecovered} result; the explicit was-stopped
  notice fires only for processes that could NOT be auto-restarted,
  meshing with the auto-respawn from #72192.
2026-07-26 14:11:05 -07:00
teknium1
920facdc4c fix(update): never blind-reinstall cua-driver during hermes update
'Refreshing cua-driver (Computer Use)...' could hang for minutes on
Windows: when the driver's native check-update verb returned an
indeterminate result (old driver without the verb, offline, GitHub
rate-limited, or the probe timing out), install_cua_driver(upgrade=True)
fell through to the full upstream installer — a silent, output-captured
run with a 660s ceiling, plus install.ps1's 600s concurrency-lock wait
on Windows on top. Every 'hermes update' paid that cost.

Two changes:

- install_cua_driver() grows require_confirmed_update: with it set, an
  indeterminate check keeps the installed version and returns fast,
  printing the force path (hermes computer-use install --upgrade).
  'hermes update' passes it; the explicit --upgrade CLI keeps the old
  fall-through so a force refresh still works when the check can't
  answer.
- cua_driver_update_check() default timeout is now 25s on Windows
  (8s unchanged on POSIX): first-spawn of the exe under Defender /
  SmartScreen routinely exceeds 8s, and a false timeout is exactly the
  indeterminate result that used to trigger the multi-minute reinstall.
2026-07-26 13:47:35 -07:00
teknium1
3dc2decd43 fix(update): explicit utf-8 decoding on systemctl restart calls
Windows-footguns lint: subprocess text=True without encoding= decodes
via locale.getpreferredencoding(). Match the file's house style.
2026-07-26 13:42:47 -07:00
Variable85
8e1fb9ea34 fix(update): respawn manually-started dashboard/serve backends after update
Capture each manually-started dashboard/serve process's argv before the
stale-process kill (/proc/<pid>/cmdline on Linux, ps -o command= on macOS),
then respawn it detached after the update — headless (--no-open) with output
to logs/dashboard-restart.log under the active profile's HERMES_HOME.

Supervised PIDs keep their systemd-unit restart; --stop stays a plain stop.

Salvaged from PR #41508 with scope fixes: serve matching preserved, profile-
aware log path, restart only on the update path (restart_managed=True).
2026-07-26 13:42:47 -07:00
webtecnica
d1f376006a fix(update): restart systemd-supervised backends after stale kill 2026-07-26 13:42:47 -07:00
Brooklyn Nicholson
16042b0c4b feat(sessions): add hermes sessions retitle-skills for stored titles
Previews correct themselves on read, but a title already written to the DB
stays wrong. This regenerates those titles from what the user actually typed,
dry-run by default.

Two guards, both hit on a real store: a candidate that isn't title-shaped is
rejected rather than replacing a serviceable title with command output, and a
unique-title collision dedupes through the lineage the way the live auto-titler
does instead of leaving the leaked title in place.
2026-07-26 02:58:24 -05:00
Hermes Agent
92549c9a6e refactor(cli): route every aux picker through one provider-inventory seam
Adds build_aux_picker_rows() + format_aux_picker_entries() to
hermes_cli/inventory.py and converts both auxiliary-task pickers to them,
so custom providers, exclusions, and picker visibility can no longer be
dropped one call site at a time.

The two salvaged commits each fixed one kwarg at these same two sites
(user/custom providers, then for_picker). Both were per-site patches, so
the next aux picker would have reintroduced the gap. The seam makes the
correct behaviour the default a new caller cannot forget:

- user `providers:` and saved `custom_providers:` entries
- `model_catalog.excluded_providers`, matching /model
- exhausted-credential-pool providers stay visible
- only the active custom endpoint is probed, so an offline saved local
  server can't hang the picker
- the virtual `moa` row is excluded (auxiliary_client unwraps it to the
  aggregator anyway, so offering it is a silently-rewritten choice)

The vision picker also gains the current-selection marker it never had.

Also threads for_picker through build_models_payload, which had no way to
express it despite list_authenticated_providers supporting it.
2026-07-25 22:47:12 -07:00
Drexuxux
9aefa4c61c fix(model-picker): show exhausted-pool providers in the aux-task and vision pickers too
credential pool has entries but is entirely rate-limited (exhausted): rate
limits are per-model for many providers (e.g. Google Gemini), so an exhausted
key for model-A may still serve model-B, and the user should stay able to
select it. That fix wired `for_picker=True` into `list_picker_providers`.

The two sibling interactive pickers reached by `hermes model` still called
`list_authenticated_providers()` WITHOUT `for_picker=True`, so they kept
hiding exhausted-pool providers:

- `_aux_select_for_task` (hermes_cli/main.py) — the auxiliary-task
  provider/model picker.
- `_configure_vision_provider_model` (hermes_cli/tools_config.py) — the vision
  provider/model picker.

Both persist a per-task config the user runs *later* (once the momentary
cooldown clears), so hiding an exhausted-pool provider here is exactly the

Tests: two regressions assert each picker requests `for_picker=True` (they
omitted it before the fix); the existing picker/exhausted-pool suite still
passes (6 total).
2026-07-25 22:47:12 -07:00
deepjia
f7001f9683 fix(cli): show custom providers in auxiliary model picker 2026-07-25 22:47:12 -07:00
Gille
508764d384 fix(sessions): add opt-in partial database recovery 2026-07-25 22:14:57 -07:00
Brooklyn Nicholson
c03a977a96 fix(update): drop the GetNativeSystemInfo probe that lies under WoA emulation
The residual Windows-on-ARM integrity-gate fix added GetNativeSystemInfo as a
second "OS-native" probe behind IsWow64Process2. Microsoft documents the
opposite behavior: "the API GetNativeSystemInfo also returns emulated
processor details when run from an app under emulation."

On the exact hosts the probe was added to rescue -- an x64 hermes-setup.exe
emulated on an ARM64 Surface -- it therefore reports AMD64, making it a
duplicate of the PROCESSOR_ARCHITECTURE rung already below it rather than a
fallback for it. Its test only passed because the kernel32 fake was hand-fed
ARM64, asserting behavior real Windows does not exhibit.

Replace it with GetMachineTypeAttributes, which answers the question the gate
actually asks -- "can this host load a PE of machine X?" -- instead of
inferring it from an architecture name. It is also the only documented API
that reports AMD64-on-ARM64 emulation support. The gate prefers it and falls
back to the existing name-based mapping on pre-Windows-11 hosts.

The load-bearing half of the previous fix (typing GetCurrentProcess as HANDLE
so IsWow64Process2 stops failing ERROR_INVALID_HANDLE) is unchanged.

Co-authored-by: xxxigm <xxxigm@users.noreply.github.com>
Co-authored-by: Teknium <teknium1@users.noreply.github.com>
2026-07-25 21:13:15 -05:00
teknium1
ec2a0f8c1e fix(sessions): point failed in-place repair at offline recovery
A failed `hermes sessions repair` printed "keep state.db and the backup"
and stopped, so a user whose sessions had vanished had no way to discover
that a non-destructive recovery path exists — the reported dead end.

The failure branch now names the next command, read-only step first, seeded
with the backup path it just preserved. Covered by a CLI-surface regression
test that drives the real subprocess against an unrepairable database.
2026-07-25 16:42:31 -07:00
Gille
a9b8128bcb fix(sessions): add offline state database recovery 2026-07-25 16:42:31 -07:00