Commit Graph

131 Commits

Author SHA1 Message Date
Teknium 39ed610f8c feat(cron): create paused jobs without a scheduling race
Persist paused state, timestamp, reason and no first trigger in the original
locked creation write. Forward the same boolean contract across CLI, tool,
gateway API and dashboard API, validating at the store boundary. Preserve
explicit operator force-run behavior and normal enabled creation.

The live CLI probe also caught the command shim dropping failure return codes;
forward them so invalid creation reports exit 1 rather than success.

Credit earlier atomic-creation work in #78935 and #94952 and the focused
implementation in #104578. The broader manifest staging layer is not imported.

Co-authored-by: Konstantin Khlopkov <konstantin.khlopkov93@gmail.com>
Co-authored-by: Chloé DuPont <321112755+misschloedupont@users.noreply.github.com>
2026-09-07 08:27:50 -07:00
Teknium f5afe8bd40 feat: authorize MCP servers with device codes from the CLI
Add explicit RFC 8628 device login and oauth.flow selection while keeping
browser PKCE and the SDK runtime refresh path. Reuse issuer/resource
validation, configured client authentication and profile-scoped storage.
Only persist an approved, validated grant; never echo endpoint error bodies.

Slim redo of #104752 by @wjorgensen, replacing duplicate HTTP/storage
wrappers with the existing SDK and two real-wire invariant tests.

Refs #104742
Co-authored-by: Wes Hermes <weshermes@Wess-Mac-mini.localdomain>
2026-09-07 08:22:35 -07:00
Brian Le 32a59f3bf7 feat: refresh one pooled OAuth grant from the CLI 2026-09-07 08:06:48 -07:00
Brian Le 1a4bb74a40 feat: choose pooled credential priority from the CLI 2026-09-07 08:06:48 -07:00
Brian Le 53221df05f feat: reset one pooled credential without clearing sibling cooldowns 2026-09-07 08:06:48 -07:00
Teknium 0a5164cebe compat(plugins): tell users which installed plugins break on 2026-09-14, and stop loading them after
hermes_cli/plugin_compat.py is now the single source of truth for the compat window:
  COMPAT_REMOVAL_DATE = 2026-09-14; scan_plugin() statically finds `from F import n`, `import F` + `F.n`,
  alias forms and string targets against compat_manifest.json; compat_report() aggregates over the user's
  ENABLED external (non-bundled) plugins; disable_reason() decides the loader's skip.

Surfaces (all read from that one report):
  * CLI: yellow block under the banner naming plugins + date + `hermes plugins compat` (red + DISABLED after)
  * `hermes plugins compat [--json] [path]`: file:line, old -> new per hit; exit 1 while anything remains;
    `path` lets a plugin author scan their own checkout
  * `hermes doctor`: "Plugin import paths (removed Sep 14, 2026)" section next to the xAI retirement check
  * `hermes update`: post-update notice alongside the FTS/curator notices
  * Desktop: compat_report() writes HERMES_HOME/.plugin-compat-report.json (deleted when clean); Electron
    shows ONE warning dialog per distinct report after the backend is up and persists the dismissal in
    userData/plugin-compat-dismissed.json. A new affected plugin, or the date passing, is a new report.

From the date, PluginManager skips a hitting external plugin before importing it, with the reason in
LoadedPlugin.error ("uses N import path(s) removed on 2026-09-14; run `hermes plugins compat` ...") — the
same path a plugin with a broken register() takes, so nothing else is affected. Escape hatch:
plugins.allow_deprecated_imports: true (config_defaults), which only helps until the compat commit is
actually reverted.

Docs: COMPAT_MANIFEST.md (removal date, what-happens table, author instructions), plugin dev guide section.
Tests: tests/test_plugin_compat_notice.py (scanner forms, report scope, date gate + escape hatch, summary
text, report file lifecycle, loader skip via a real PluginManager), electron/plugin-compat-notice.test.ts
(show once, re-show on a different set or on the date passing, malformed file ignored).

Live A/B on this box with a demo plugin on old paths: before the date it loads and the banner/doctor/report
name it; with today=2026-09-14 it is skipped with the reason and the banner turns red; with the escape
hatch it loads again.
2026-09-04 01:28:31 -07:00
Teknium 0e78694c72 simplify(compat): hermes_cli.main — drop 198 re-exports/aliases (155 eager + 45 lazy PEP 562 + _warn_stale_dashboard_processes alias + _time/_self/_LAZY_* machinery), repoint 17 source callers + 91 test files 2026-09-03 15:07:39 -07:00
Teknium 5c3db6750f simplify(compat): tools/computer_use/cua_backend — drop 24 re-exports + shutil re-import, repoint 5 callers + 14 test files 2026-09-03 13:22:51 -07:00
Teknium e83816a4d1 review-fix(comments): restore lost #NNNN rationale comments across non-test source (mechanical sweep, condensed, code unchanged)
For each issue anchor present in BASE 63279301bc non-test .py and absent on HEAD, the BASE comment/docstring block was re-attached at the HEAD location of the code it explained (matched by the distinctive code line / enclosing def). Sentences already covered by an existing HEAD comment were deduped; the issue number always survives. Insert-only: no code lines changed.
2026-09-03 09:44:26 -07:00
Teknium 2b2684c8f9 refactor(hermes_cli): compact argparse builders (sessions/cron/gateway/skills) — one-line store_true flags via _flag; --help byte-identical 2026-09-03 00:04:42 -07:00
Teknium aacaa52471 refactor(hermes_cli/subcommands): sessions filter flags as a data table; skills shared _SOURCE_CHOICES 2026-09-02 20:57:04 -07:00
Teknium e0473f0975 refactor(hermes_cli/subcommands): compact module/function docstrings and drop restating section comments (WHY comments kept) 2026-09-02 20:52:06 -07:00
Teknium 0228845ffc refactor(hermes_cli/subcommands): hoist add_yes_flag/add_json_flag into _shared, dedupe gateway --system and peer remote-subparser builders, compact dashboard docs 2026-09-02 20:45:41 -07:00
Teknium 50d455a75a refactor(hermes_cli/subcommands): computer_use.py — lift action handlers to module level behind dispatch tables 2026-09-02 20:33:45 -07:00
Teknium 0b00642096 refactor(hermes_cli/subcommands): peer.py — dispatch-table cmd_peer into per-action handlers, unify error/JSON emission 2026-09-02 20:25:21 -07:00
Teknium 1967a13bfa refactor(hermes_cli/subcommands): reflow argparse kwargs and drop redundant help=(...) parens (AST-verified) 2026-09-02 20:14:46 -07:00
Teknium a43396dd53 refactor(hermes_cli/subcommands): pack argparse builder calls onto <=100-col lines (AST-verified) 2026-09-02 19:57:24 -07:00
Teknium d1bdab0a2a refactor(hermes_cli/subcommands): strip Phase-2 extraction boilerplate docstrings and banner comments 2026-09-02 19:52:16 -07:00
Teknium b81ab901f3 refactor(cli): extract the last 16 inline parser groups from main() into hermes_cli/subcommands/
moa, fallback, worktree, browser, secrets, egress, migrate, whatsapp-cloud,
checkpoints, bundles, curator, pets, journey, computer-use, sessions and
completion each become a build_<group>_parser() builder. Closure handlers
that only closed over their own parser moved verbatim; sessions/completion
take the handler by injection. --help/usage/defaults byte-identical for all
399 parsers in the tree (in-process dump before/after).
2026-09-02 13:29:44 -07:00
Victor Kyriazakos c9491e6a7d feat(cron): per-job failure_deliver — route or suppress failure notices (NS-788)
Coatue FR (Frank Long): jobs delivering into shared channels publish
engine failure notices ('⚠️ Cron X failed…') to those channels with no
opt-out. Adds an optional per-job failure_deliver field sharing
deliver's grammar: on failure, targets resolve from failure_deliver
when set (local = structural silence; state still recorded in
last_status/last_error/run history). Success delivery is unchanged;
absent field = today's behavior byte-for-byte.

Honored by every failure-category engine notice: the run_job failure
summary (+streak nudge), the escaped-failure retry path, drift-skip and
blocked-config alerts (composed into the same delivery), and the
gateway-shutdown interrupted-run notice (_notify_interrupted_cron_jobs).

Surfaces: cronjob tool create/update (same bot-chat validation as
deliver; '' clears on update), hermes cron create/edit
--failure-deliver, docs tip in automate-with-cron.

Existing fake_deliver test doubles gained **kwargs for the new
for_failure keyword — signature-compat only, no behavior change.
2026-09-02 20:16:14 +05:30
Gille 5180601a6a perf(cli): dispatch serve without the full parser tree 2026-09-02 06:06:47 -07:00
emozilla 43e67d872f feat: local models — managed llama.cpp runtime with one-click desktop setup
Run models locally as a first-class provider. The CLI grows a managed
llama.cpp runtime (engine install, model download, server supervision);
the desktop app grows the full setup and management story on top of it.
GUI surfaces ship behind the desktop --local launch flag (hermes desktop
--local, or the flag on the packaged app); backend routes and the CLI
are always live.

Runtime (hermes_cli/local_runtime/):
- curated GGUF catalog with per-machine variant selection: hardware
  probe (VRAM/RAM/UMA), fit planning with spill accounting, quant choice
  by context window
- derived recommendation: quality-ranked picks gated by a predicted
  decode-speed floor, bandwidth-aware on unified memory; the decision
  table is pinned as a test (pick AND reason per memory class), and the
  Recommended badge explains its pick in a tooltip fed by the resolver's
  actual branch
- engine install + model download with resumable split parts, cumulative
  plan-level progress, and staged-model integrity (a split GGUF counts
  only when every part is present)
- server supervision: spawn/adopt/stop, router mode with per-model load
  progress relayed over SSE, abandoned-request cleanup

Desktop:
- Settings -> Providers -> Local models: one-click quickstart (install
  engine, download the recommended model, boot) plus per-model download/
  activate/eject, fit-ranked catalog with context pills
- model pickers (composer dropdown + Cmd+K) show staged local models,
  in-flight downloads as live progress rows, and load-into-memory bars
- local-setup campaign tip for eligible hardware; System resources
  statusbar widget (GPU/VRAM/RAM); in-chat load progress during sends
- friendly dead-server errors, and failed agent builds retry on the next
  send instead of wedging the session

Co-developed with NVIDIA field feedback on RTX 5090 and DGX Spark.
2026-09-01 16:01:53 -04:00
kshitijk4poor 525dd12da7 fix: fail closed when no safe export destination exists; polish salvage edges
- _profile_export_directory(): when the managed store, the home-sibling
  store, AND the temp dir all resolve inside Git checkouts, raise a clear
  ValueError instead of warning and proceeding — a stderr warning would not
  stop a scripted export from staging a secret-bearing archive in a source
  tree, which is the exact #92457 incident class. All three callers already
  surface ValueError cleanly (CLI/TUI print Error: + exit, API returns 400).
- .dockerignore: drop the /default.tar.gz line made redundant by the global
  *.tar.gz pattern this PR adds.
- hermes profile export -o help text: stop advertising the old
  <name>.tar.gz cwd default.
- Tests: cwd-in-unrelated-checkout topology (the second production shape
  from the blocking review) and the fail-closed path. Mutation-checked:
  both fail on the pre-fix helper.
2026-09-01 01:00:23 -07:00
joe102084 b028fe632e feat: add cron doctor health check 2026-08-31 10:00:29 -07:00
David Dudok de Wit e7433910e9 feat(bot-mode): add scoped cross-gateway Group Chat transport 2026-08-31 01:04:11 -07:00
yoniebans 00bbfc6900 docs(update): --yes help states the fork-upstream prompt is skipped, not accepted
The old text read as if --yes answers yes to every prompt. It accepts the config-migration and stash-restore prompts but skips the fork-upstream prompt without adding a remote (#97052 review); say so.
2026-08-28 13:33:56 -04:00
kshitijk4poor 9f05b06589 Revert "Merge pull request #94245 from kshitijk4poor/feat/gw-event-replay"
This reverts commit df7d7f6e8d, reversing
changes made to 1a66134404.
2026-08-27 11:26:57 +05:30
kshitij df7d7f6e8d Merge pull request #94245 from kshitijk4poor/feat/gw-event-replay
feat(gateway): slim WS-only server — remove FastAPI/uvicorn from desktop boot path
2026-08-27 11:22:41 +05:30
CriptoGus eeb391d0b1 feat(desktop): add --setup-tcc-identity to keep macOS TCC grants across rebuilds
Adds a one-shot `hermes desktop --setup-tcc-identity` command that creates a
self-signed code-signing certificate in the login keychain (openssl +
security import), grants codesign access to it, writes
desktop.macos_signing_identity to config.yaml, and re-signs the packaged app
with a certificate-anchored Designated Requirement.

macOS persists permission grants (Full Disk Access, Accessibility, Files and
Folders, microphone) against the app's code-signing identity, not its path.
The default identifier-pinned ad-hoc signature is stable across rebuilds, but
a certificate-anchored identity is the strongest guarantee — the same
mechanism yabai/skhd rely on. Previously users had to create the certificate
manually in Keychain Access; this command automates the whole flow and is
idempotent (re-run after updates).

Docs: desktop.md TCC section now leads with the command, keeps manual steps.
Tests: 4 new — fresh cert creation path, idempotent reuse, non-macOS no-op,
cmd_gui early-exit before build.
2026-08-25 16:24:19 -07:00
Teknium 9de5460c12 feat(cron): acked failure signatures stop re-pinging — durable incidents + ack CLI (salvage #94692) (#95017)
* feat(cron): durable failure incidents with signature dedup and ack

Introduce a durable cron incident store (cron_incidents in the shared
cron/executions.db) that groups "same job + same error signature" across
runs, so a known recurring failure stops re-pinging the operator every run
once it has been acknowledged.

- cron/incidents.py: lazily-created incident table (detected -> alerted ->
  reviewed -> closed lifecycle; closed is per-signature terminal), sha256
  signature dedup over job_id + normalized error, redacted/truncated error
  storage, failure-type classification, and ack/list/get/count helpers.
- cron/scheduler.py: record an incident on the failure delivery path and
  suppress the per-run failure ping when the exact signature is acked (both
  the normal failure path and the processing-raised retry path). Best-effort:
  an incident-store error never breaks the cron run or delivery. Streak nudge,
  alert-once markers, and delivery-error behavior are untouched.
- hermes_cli: add `hermes cron incidents [--state ...]` and
  `hermes cron incidents ack <id>`.
- tests/cron/test_cron_incidents.py: dedup, lifecycle, redaction,
  classification, lazy-schema, scheduler gating, and CLI coverage.

Non-goals deferred to later slices: Discord buttons/review view, HMAC action
tokens, owner-agent review launch, approval-gated fixes, incident playbooks.

* refactor(cron): tighten incident lifecycle, wire alerted state and suppressed_acked outcome

Follow-ups on top of the salvaged #94692:
- Drop the dead 'reviewed' state and the SQLite CHECK (state validity
  lives in INCIDENT_STATES so future slices can add states without a
  table rebuild); lifecycle is detected -> alerted -> closed.
- Actually mark incidents 'alerted' after a failure ping reaches
  delivery, on both the normal and exception delivery paths.
- Record ack-suppressed runs with a distinct 'suppressed_acked'
  delivery outcome (registered in cron_health monitoring) instead of
  the ambiguous generic 'suppressed'.
- Drift-skip alerts explicitly bypass the ack gate (they carry the
  remediation command and alert once via drift_alerted already).
- Docs: failure-incidents section in the cron guide.
- Tests for the alerted transition + never-resurrect-closed.

---------

Co-authored-by: Laura López Real <113060513+laulopezreal@users.noreply.github.com>
2026-08-25 14:02:40 -07:00
kshitijk4poor 434ea57eb0 feat(gateway): slim WS-only server — remove FastAPI/uvicorn from desktop boot path
The desktop app spawns `hermes serve --ws-only` instead of `hermes serve`.
The slim server (tui_gateway/entry_ws.py) uses the bare websockets library
to call handle_ws directly — same dispatch surface, same 158 RPCs, same
events, zero HTTP framework. tui_gateway.server has zero FastAPI imports;
the slim server never touches web_server.py (19.8K lines) at all.

Full backward compat: older runtimes strip --ws-only and fall back to
regular serve (still headless, just through uvicorn).
2026-08-25 13:27:28 +05:30
leosiedler a0ca7c1920 feat(cron): add explicit one-shot re-arm 2026-08-24 00:34:26 -07:00
Teknium 231e613d3d fix(peer): resolve hidden canonical Bot Chats in hermes peer dm
Bot Mode always hides canonical 'Bot Chat' sessions, but _find_bot_chat's
GET /api/sessions listing used the default include_hidden=False path, so
the existing hidden row was invisible, _ensure_bot_chat tried to create a
duplicate, and the peer DB's UNIQUE(title) guard rejected it — DM failed.

- api_server: GET /api/sessions now accepts an exact-title lookup
  (?title=...) and honors include_hidden=1 ONLY alongside a title filter,
  so canonical hidden rows resolve without exposing a blanket hidden
  listing on the client surface. The title needle is pushed into SQL
  (search_query) so old hidden rows outside the recency window are found.
- peer dm client: _find_bot_chat sends title + include_hidden=1; older
  peers ignore the unknown params and degrade to today's behavior.
- Clear diagnosable error on the older-peer duplicate-create rejection,
  naming the hidden canonical chat and the PATCH hidden:false workaround.
- Unit tests (hidden resolution, no duplicate create, older-peer error,
  older-peer visible fallback) + real-gateway E2E over a real state.db.

Root-cause analysis and regression recipe by @kubaboski in #91583.

Fixes #91583
2026-08-23 03:56:28 -07:00
Teknium 70151dd549 feat(update): updates.parked_branch_strategy gates the in-place merge; switch stays the default
Adapts the in-place branch update from PR #89507 (@willfrombr) onto the
switch-by-default behavior: the deterministic switch path remains the
default so non-interactive updates (desktop, gateway, cron) never dead-end
on a merge conflict, and deliberate custom-branch users opt in with
updates.parked_branch_strategy: update_in_place. --switch-branch overrides
the in-place strategy for one run (deep feature branches that must not
accumulate update merge commits). Docs + config comments + tests cover
all three routes.

Co-authored-by: Willian Santos <285090322+willfrombr@users.noreply.github.com>
2026-08-21 15:23:49 -07:00
Willian Santos 4fad27a101 feat(update): --switch-branch opts an unmerged branch out of the in-place merge
Review feedback on #89507: in-place merging suits a branch that tracks the
target with a small patch set, but a long-lived feature branch (a PR branch
hundreds of commits deep) does not want an update-driven merge commit
written into its history. Reported against a checkout carrying 819 unmerged
commits.

--switch-branch routes the unmerged case to the switch path instead: the
checkout moves to the update target and updates there, and the branch is
left byte-identical — no merge, no commit, nothing written to it. The tree
is known clean on that path (the guard checks dirty before cherry), so a
dirty tree still gets the loud skip, unchanged.

Opt-in: without the flag the default remains the in-place update, which is
what keeps a small-patch-set branch's running code current.

Tests: the flag switches and leaves the branch tip byte-identical; the
default without it still updates in place. The first fails if the flag's
branch is severed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 15:23:49 -07:00
Teknium a2da0ab797 feat(cron): bot-chat delivery target — cron output lands in a bot's canonical Bot Chat and the bot responds
deliver='bot-chat[:<profile>]' is a machine-local pseudo-platform: the
scheduler delivers job output as a real inbound turn in the target
profile's canonical Bot Chat via the chat CLI lane (--in ~ -c "Bot Chat"
--create-if-missing -Q --query-file), the same lane Bot Mode
agent-to-agent messages use. The bot reads the output, acts on it, and
responds in its chat — instead of the output only landing in Run history.

- cron/scheduler.py: token parsing, target resolution (own profile /
  named local profile / unknown -> skipped with warning), subprocess
  delivery lane with cron.bot_chat_delivery_timeout_seconds (default
  600s), preflight exemption, and bot-chat entries in
  cron_delivery_targets() for UI pickers. Excluded from 'all' by design.
- tools/cronjob_tools.py: create/update-time validation — named profiles
  must exist on this machine (fail at create, not at 3am); deliver schema
  documents the new token.
- tui_gateway/methods_tools.py: cron.manage add forwards deliver.
- hermes_cli/profiles.py: list_profile_names() cheap name-only scan.
- hermes-bots plugin: Create Cronjob dialog gains a 'Send results to'
  picker (Run history only / <bot>'s chat); bot-chat jobs send the BARE
  token on the profile-scoped create so Desktop-side aliases can never
  name a profile the backend doesn't have.
- Docs: user cron guide, automate-with-cron, cron-internals.

Machine-local by construction: names resolve only against the executing
machine's ~/.hermes/profiles/, so overlapping profile names across
multiple connected gateways are unambiguous.
2026-08-21 12:48:53 -07:00
Teknium 0aecadc17c feat(update): hermes update --plan — read-only fleet inventory + plan phase in every update
Phase 2 core slice of #91277: the updater now knows WHAT it is operating
on before it mutates anything.

- hermes_cli/update_inventory.py (new): side-effect-free runtime
  inventory — install kind via detect_install_method (git / docker / nix
  / apt, updatable-in-place or not, with the correct external update
  command for image/package-managed installs), all profiles, every live
  gateway with its supervisor (systemd / launchd / manual via the
  fleet-wide _get_service_pids), running code_sha/code_version from the
  #91283 gateway_state.json stamps, and the restart mechanism each
  runtime will get.
- hermes update --plan: prints the plan and exits; runs BEFORE the
  docker/nix refusal gates so image-managed installs get a useful
  'not updatable in place + right command' report instead of a bare
  refusal. Read-only, safe on a live fleet.
- Every real update run now records the pre-update plan in its receipt
  ('plan' key) and prints a one-line fleet summary, so post-mortems can
  compare what the update SAW against what it did.
- Docs: updating.md (--plan section + receipts/fleet-check section),
  cli-commands.md (flag row + receipts behavior bullet).
- 11 tests: two-profile fleet classification, docker not-in-place,
  dead-PID exclusion, PID-file fallback dedupe, all-probes-fail
  never-raises, JSON round-trip for the receipt, print output shapes,
  receipt integration.
2026-08-21 04:23:13 -07:00
Victor Kyriazakos 4e1dd1a74b feat(cron): per-job reasoning_effort override in job definitions
A cron job can now pin its own reasoning (thinking) effort, independent
of the global agent.reasoning_effort and per-model reasoning_overrides.
Heavy scheduled analyses can run at high while cheap recurring jobs run
at minimal, without touching the fleet-wide default.

- cron/jobs.py: new optional job field, validated at the storage choke
  point against the canonical grammar via the shared
  hermes_constants.parse_reasoning_effort (spelling-only; capability
  clamping stays owned by the provider transports at send time, same as
  config-set effort). Empty string clears on update; invalid values
  raise ValueError before anything persists. Not a drift-guard axis.
- cron/scheduler.py: _resolve_job_reasoning_config resolves per-job pin
  > agent.reasoning_overrides > agent.reasoning_effort at fire time,
  after the auth-fallback model swap (the pin is model-independent by
  design). A stored value that no longer parses warns and falls back to
  config resolution instead of killing the tick.
- tools/cronjob_tools.py: reasoning_effort on BOTH mutation verbs
  (create and update), conditional key in _format_job, schema documents
  grammar/precedence/transport clamping/clear semantics. Agent-settable,
  unlike model/provider pins: it cannot redirect spend to a different
  model.
- hermes cron create/edit --reasoning-effort (empty string clears).
- Docs: cron feature page tip + CLI reference rows.

Tests: tests/cron/test_cron_reasoning_effort.py (32) — store contract,
scheduler precedence incl. byte-identical absent-field behavior and
garbage fallback, tool create/update/clear/error paths, schema surface.
2026-08-20 19:56:14 -07:00
Teknium 5dd221d442 feat: desktop updates no longer re-apply local source edits (--keep-stash)
The desktop updater ran `hermes update --yes`, which auto-restored any
uncommitted source-tree edits onto the freshly updated checkout. On dirty
from-source installs this silently carried local modifications across every
update and could break the rebuilt app (field report: Windows update handoff
leaving the app 'crashed').

New `hermes update --keep-stash`: local changes are still autostashed so the
update can proceed, but are never re-applied — they stay parked in git stash
with printed recovery guidance. Both desktop handoff scripts (windows.ps1,
posix.sh) now pass it, probing `update --help` first so older installed
backends without the flag keep working. Failure paths are unchanged (stash
preserved, no restore); updates.non_interactive_local_changes: discard still
wins.

Tests: park/restore/failure-path coverage incl. a sabotage-verified
regression test; docs updated.
2026-08-20 04:54:14 -07:00
Teknium e69b8e561d feat: consolidate 'hermes version' into 'hermes --version', remove the subcommand
'hermes --version' (and -V) now prints the full version report — banner
version line with upstream SHA, install directory, authoritative install
method, Python and OpenAI SDK versions, and update status — making the
separate 'hermes version' subcommand redundant. The subcommand is removed.

- _startup_fast.print_fast_version_info() is now THE canonical version
  printer: static lines print instantly from stdlib probes, then the
  banner label, install-method resolver, and update check lazy-import
  after the first line is on screen (each degrades gracefully).
- main.py _print_version_info() delegates to it (used by /version in the
  CLI chat surface and the --version flag path); the old duplicate
  implementation is deleted.
- hermes_cli/subcommands/version.py removed; parser wiring, subcommand
  sets, console-engine extraction entry, and tests updated. Hermes
  Console keeps a 'version' command wired to the shared printer.
- Termux fast paths now include update status too (previously
  check_updates=False).
- Docs/i18n, CONTRIBUTING, SECURITY, and nix checks updated to
  'hermes --version'.
2026-08-20 03:05:46 -07:00
Brooklyn Nicholson a9eb99d172 fix(update): stop deferring shim renames to next boot
MOVEFILE_DELAY_UNTIL_REBOOT was the quarantine's last resort, and it is worse
than doing nothing. It writes to HKLM, so a non-elevated update — every
Desktop-driven one, and most terminal ones — gets ERROR_ACCESS_DENIED and
reports nothing. When it does succeed it frees nothing for the install
running right now, and the queued operation outlives that update: at the next
boot it moves aside whatever sits at the shim path, including a shim a later
repair just wrote.

Drops the fallback and sweeps entries older versions queued, matching only
our own <shim> -> <shim>.old.<stamp> pairs so unrelated installers keep
theirs.

Salvaged from #88121 by @fangliquanflq.
2026-08-19 13:14:40 -05:00
Noah Lee cd3f9b64de feat(skills): add --yes/-y flag to hermes skills uninstall
`do_uninstall` already accepted a `skip_confirm` parameter and the
slash-command handler already passed `skip_confirm=True`, but the CLI
argparse path never exposed a flag to reach it. This adds `--yes`/`-y`
to `hermes skills uninstall`, matching the existing pattern on `install`
and `reset`.
2026-08-19 10:19:35 -07:00
Teknium a1682376ca feat(profiles): rename any agent — the default profile gets a display name (#45624)
`hermes profile rename default <name>` (and the Desktop/dashboard rename
flows) now set a presentation-only `display_name` in profile.yaml instead
of erroring. The canonical id stays "default"; resolution, comparison,
and spawn paths are untouched. Named profiles keep real renames and their
display_name survives the move.

Surfaces: profile list/show/status, /profile (text only — data.profile
stays canonical), dashboard ProfilesPage, TUI-gateway profiles.list, and
Desktop (rail, switcher, Manage page, and the Bot Mode roster via a
displayName fallback so a renamed default shows its name, not "default").

Slimmer redo of the direction in PR #87760 by @yxssxn — thanks; see PR
body for what changed vs that approach.
2026-08-18 02:27:18 -07:00
Teknium 189de16280 fix(peer): live-verified cross-gateway bot DMs — session shape + virtual-model replay
Two bugs found by a REAL two-gateway live test (two isolated HERMES_HOMEs,
bravo running the api_server platform, alpha's agent autonomously running
`hermes peer dm` from its Bot Chat protocol; reply relayed correctly and
persisted in bravo's canonical Bot Chat):

1. peer dm parsed the session-create response flat, but api_server wraps
   the row: {"object": "hermes.session", "session": {...}} — every first DM
   to a fresh peer failed with "Peer did not return a session id" (and the
   orphaned Bot Chat then 400'd retries with duplicate-title). Parse the
   wrapped shape; the test fake now mirrors the real response shape so this
   class can't pass green again.

2. api_server: a session created with no model persists the advertised
   virtual model ("hermes-agent") on the row; session chat then replayed it
   as a REAL model id and the provider 400'd ("hermes-agent is not a valid
   model ID"). _request_agent_overrides already filters the virtual model
   for per-request bodies — apply the same filter to the stored session
   model at both chat sites (sync + stream), so it means "gateway default"
   exactly like the request-body path.

Live E2E transcript (bravo's Bot Chat, via /api/sessions/{id}/messages):
  user:      Message from 🤖 alpha (@alpha): What is your callsign?
  assistant: CALLSIGN-BRAVO-7
peer cmd unit suite 10/10 with the corrected fake.
2026-08-17 16:34:22 -07:00
Teknium 6229683b62 feat(cli): hermes peer — bot-to-bot DMs across machines and gateways
Bots could message teammates on their own machine (hermes -p <bot> chat) and
the desktop could relay user mentions over Connections, but a bot had NO
transport to a bot on another gateway. This adds one, with zero new server
surface: the peer's existing api_server platform is the wire.

- hermes_cli/subcommands/peer.py: `hermes peer add/list/remove/dm`.
  `dm <peer>[/<agent>]` resolves the remote agent's canonical "Bot Chat"
  (list by title, create when missing), runs one synchronous agent turn via
  POST /api/sessions/{id}/chat, and prints the reply on stdout — the exact
  cross-machine twin of the local bot-messaging command, so the Bot Mode
  protocol composes over it unchanged. Named profiles route via the peer's
  /p/<profile>/ multiplex mirror. Peer URLs live in config.yaml
  (`bot_peers`); the peer's API_SERVER_KEY is a credential and lives in
  ~/.hermes/.env as HERMES_PEER_<NAME>_KEY.
- hermes_cli/main.py: parser wiring + fast-path/session-flag command sets.
- tools/bot_mode_probe.py: when peers are registered, the injected Bot Chat
  messaging protocol gains a cross-machine paragraph (peer roster +
  `hermes peer dm` pattern) so agents discover remote teammates on their
  own; peers join the capability fingerprint so registering/removing one
  refreshes eternal Bot Chat prompts on the next message (loud, one-time,
  user-initiated — no per-turn cache drift).
- Docs: Bot Mode guide (bot-initiated DMs across machines) + cli-commands
  reference (`hermes peer` section + summary row).

Tests: tests/hermes_cli/test_peer_cmd.py (target parsing, /p/ scoping,
registry round-trip in isolated config, real-loopback-HTTP dm flow incl.
Bot Chat create-vs-reuse and bearer auth), bot_mode_probe peer-paragraph +
epoch tests. E2E: real `python -m hermes_cli.main peer ...` against a live
fake peer over HTTP with isolated HERMES_HOME (config/.env persistence,
bare + /p/<profile> routing, stdin, --json). 23 passed; ruff clean.
2026-08-17 16:13:30 -07:00
Teknium f891d702df feat: project-local skill discovery with per-repo trust gate
Sessions started inside a git checkout now source skills from
<root>/.hermes/skills/ and <root>/.agents/skills/ (the cross-tool
convention shared with other agent harnesses) as the highest-precedence
skill tier: project > local > external_dirs.

Loading is trust-gated per repo (skills.trusted_project_dirs, managed by
'hermes skills trust'/'untrust') because skills are executable procedure
documents — auto-sourcing them from any cloned repo is a prompt-injection
vector. Untrusted repos with skills get a one-line banner notice instead.

- agent/skill_utils.py: find_project_root, get_project_skills_dirs,
  get_untrusted_project_skills_root, get_scan_ordered_skills_dirs;
  project dirs join the curator read-only ownership boundary
- agent/prompt_builder.py: project tier scanned first, entries tagged
  [project], same-named local entries shadowed; cache key extended
- tools/skills_tool.py: skills_list scans project dirs first (first-wins);
  skill_view resolves cross-tier collisions in favor of the project tier
  (same-tier ambiguity still refuses); security warning recognizes the tier
- agent/skill_commands.py + hermes_cli/commands.py: /skill-name slash
  commands and gateway slash menus include project skills
- tools/credential_files.py: project dirs mounted into remote backends
- cli.py: banner notice (loaded count / trust hint)
- hermes_cli/main.py + subcommands/skills.py: hermes skills trust/untrust
- config: skills.project_discovery (default on), skills.trusted_project_dirs
- docs: Project-Local Skills section in skills.md
- tests: tests/agent/test_project_skills.py (18 cases)

Session cwd is fixed at agent build time, so the resolved tier is stable
for the conversation and the system prompt stays byte-stable (cache-safe).
2026-08-17 11:39:13 -07:00
Teknium 0e378e59aa Port from paperclipai/paperclip#10978: skip locally-edited hub skills on update unless --force
paperclip#10978 made destructive replacement an explicit caller choice
in their skill-sync and package-import paths: a rerun must never remove
operator edits by default. Our hub-skill updater had the same hazard --
'hermes skills update' calls do_install(force=True), which rmtree-replaces
the skill directory even when the user edited it after install.

do_update now compares the on-disk content hash against the hash the
lockfile recorded at install time; drifted skills are skipped with a
notice and only overwritten with the new --force flag (CLI + /skills
slash path). Bundled skills already had this protection via the
user-modified manifest in hermes update; this brings hub-installed
skills to parity.

Sabotage-verified: disabling the drift check makes the new skip test fail.
2026-08-16 22:10:37 -07:00
Teknium ea29702749 feat(cron): --continuity / --no-continuity flags on hermes cron create/edit
CLI parity for the continuity toggle:

- subcommands/cron.py: --continuity on create; --continuity / --no-continuity
  tri-state pair on edit (same store_const pattern as --no-agent/--agent)
- cron.py: forwarded to the cronjob tool; created/edited job summaries print
  a "Continuity: on" line
- cronjob_tools._format_job: reports continuity as an explicit boolean and
  strips the reserved 'self' entry from the reported context_from list
- cron-job.ts: form reader accepts both shapes (raw store record with 'self'
  inside context_from, or formatted record with the explicit flag)
- docs: CLI flag examples in the continuity section

E2E (real argparse -> cron_create/cron_edit -> jobs.json in temp HERMES_HOME):
create --continuity stores ['self']; edit --no-continuity clears; edit
--continuity restores; default-off unchanged. 91 cron/tool tests + 16 CLI
cron tests + vitest 10/10 pass.
2026-08-16 22:09:28 -07:00
Teknium 46e20083d8 feat(plugins): plugin packs — declarative, shareable plugin sets (#64166)
Adds hermes-pack.yaml: a single YAML file pinning a set of plugins to
exact 40-char commit SHAs with optional non-secret plugins.entries
config seeds and a declared (not yet installed) skills list.

CLI:
- hermes plugins pack install <path|https-url> [--force]: mandatory
  review screen (plugins + refs + declared capabilities), one summary
  confirmation, then fan-out through the existing pinned install path.
  Per-plugin capability consent rides the standard #64228 flow — a pack
  never bulk-grants. Partial failures reported per plugin; non-zero
  exit when any fail. Interactive only (no --yes in v1).
- hermes plugins pack export [--enabled-only] [--name]: pack YAML on
  stdout from install metadata (repo + exact SHA); local-only plugins
  become warning comments; secrets/capability grants stripped.
- hermes plugins pack show <path|url>: dry-run view.

Supply chain: refs must be exact 40-char SHAs (tags/branches rejected
naming the entry, same rule as the community index); config seeds
reject secret-shaped, capability, and allow_* keys; bare names resolve
through the community index; https-only URL fetch with size cap.

Tests: tests/hermes_cli/test_plugin_packs.py (36) — parse/validate,
SHA enforcement, mocked install fan-out, consent-per-plugin assertion,
export round-trip + sanitization, partial-failure exit code, parser
wiring. No live network.

Docs: user-guide plugins.md packs section (notes packs build on the
manifest v2 fields per #64165) + cli-commands.md rows.

Closes #64166
2026-08-12 19:56:44 -07:00
hans 17030939db feat(plugins): inter-plugin event bus with declared emits/listens
Give plugins a first-class, namespaced pub/sub event bus so plugin↔plugin
interaction is a declared, testable contract instead of ad-hoc imports.
Closes #64164 (sub-issue 03/14 of the plugin-interface expansion epic #64182).
Additive-only: when no plugin calls emit/subscribe, behavior is unchanged.

Interface (on PluginContext):
- `ctx.emit(event, payload=None) -> int` publishes to subscribers and returns
  the count invoked. The namespace is FORCED to the plugin's own registry key
  (`manifest.key or manifest.name`): pass only the bare event name, delivered
  as `<key>:<event>`. Fail-closed — any name containing `:` (a `hermes:`
  reserved-core prefix, a foreign `other:` namespace, or an own-colon'd name)
  is rejected with a ValueError + logged warning naming the plugin.
- `ctx.subscribe(full_event, callback)` registers an ordered listener for a
  fully-qualified `<plugin>:<event>`. Subscribing is unrestricted (any plugin
  may listen to any published event); only emitting is namespace-gated.

Delivery mirrors invoke_hook: registration-order iteration, per-callback
try/except isolation (one raising subscriber never breaks delivery to the
rest), payload passed as `cb(**payload)`. A per-thread depth counter caps
re-entrant emits at 8 — mutually-emitting plugins terminate cleanly with one
logged warning, never an infinite loop or RecursionError.

Discoverability: optional advisory `emits:`/`listens:` manifest fields (no
manifest-v2 dependency; not enforced) are parsed and surfaced by a new
`hermes plugins show <name>` (alias `info`) command. `get_plugin_subscriptions()`
module accessor mirrors `get_plugin_auxiliary_tasks()`.

Tests (tests/hermes_cli/test_plugin_event_bus.py, 22): two-plugin delivery +
listener count; forced namespace (delivered as `b:ping`); spoof rejection
(parametrized `hermes:x` / foreign / own-colon'd / `:x` / `x:` / empty) with
no delivery; name-fallback when key empty; per-callback isolation; recursion
cap termination + warning; manifest emits/listens parsed (present/absent/from
yaml); module accessor; `plugins show` output. `pytest test_plugin_event_bus.py
test_plugin_auxiliary_tasks.py` → 37 passed. I independently re-verified the
namespace rejection and recursion-cap termination outside the test suite.

Note: the reserved-name gate rejects any `:`-containing input rather than a
bare-name denylist — a bare `core_event` is allowed and delivered under the
plugin's own namespace. Say the word if a reserved bare-name list is wanted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b1XyXitAxV7phGmKWigJX
2026-08-12 18:57:51 -07:00