Claude Code's /simplify was renamed away, community-restored, and rebuilt
since our 3-agent port (v2.1.63 -> v2.1.154+). This brings simplify-code
in line with the current upstream design, re-expressed in our own wording
and layered onto our existing risk-tier/confidence machinery:
- New Reviewer 4 (Altitude): flags band-aid fixes layered on shared
infrastructure — special cases, symptom patches with unfixed sibling
sites, workaround stacks — and points at the deeper mechanism fix.
Matches our AGENTS.md 'fix the class, not the site' rubric.
- Explicit cleanup-vs-bug-hunt boundary: this skill improves working
code; correctness review stays with requesting-code-review.
- Inline single-pass fallback when delegate_task is unavailable (leaf
subagents, delegation disabled) — previously the skill just broke;
now all four angles run sequentially with honest disclosure.
- Finding format gains a concrete-cost field.
- Efficiency reviewer adds closure-capture scope-retention leaks.
- Pitfalls: fan-out cap 3 -> 4, band-aid-vs-deliberate-boundary caveat,
no-bug-hunting drift guard.
Kept our value-adds upstream lacks: SAFE/CAREFUL/RISKY tiers,
Chesterton's Fence via git blame, dry-run/focus/scope modifiers.
The yuanbao skill is platform-specific guidance for Tencent Yuanbao
group chats — niche for the default bundled set. Per the
'when in doubt, optional' rule, ship it as an optional skill.
Install via: hermes skills install official/yuanbao/yuanbao
The nano-pdf description repeated the skill name ('via nano-pdf CLI'),
violating the skill authoring standard, and spent its char budget
restating the name instead of distinguishing the skill from the
structural pdf skill. New description: 'Edit text in existing PDFs via
natural-language prompts.' (56 chars) — no name repetition, and
'text in existing PDFs' contrasts cleanly with pdf's
'Create, merge, split, fill, and secure PDF files.'
Propagated to the auto-generated docs pages and zh-Hans locale
(catalog row + per-skill page) for locale parity.
Per the 'when in doubt, optional' rule — zero-shot image segmentation
via SAM is a niche computer-vision capability with heavy deps
(torch, transformers), not something most users load.
Install via: hermes skills install official/mlops/segment-anything
Both are heavy-dep, GPU-bound local music-generation skills (8-16GB VRAM,
torch stacks, manual source patches) that were bundled in two different
categories (media/ and mlops/models/). Per the 'when in doubt, optional'
rule they now sit side by side in optional-skills/creative/, and the
bundled songwriting-and-ai-music skill points at them for local generation.
- git mv skills/media/heartmula -> optional-skills/creative/heartmula
- git mv skills/mlops/models/audiocraft -> optional-skills/creative/audiocraft
- cross-link related_skills both ways + songwriting-and-ai-music
- new section 10 in songwriting-and-ai-music with install commands
- docs: bundled pages -> optional pages (en + zh-Hans), catalogs, sidebar
Install via:
hermes skills install official/creative/heartmula
hermes skills install official/creative/audiocraft
The hermes-agent skill body was a 51KB monolith loaded in full on every
trigger. It is now a lightweight hub (~12KB): identity, quick start, the
tmux orchestration guide (kept in-body — the autonomous-ai-agents category
contract), surface orientation, and hard invariants, with a routing table
into 18 reference files that carry the depth.
Absorbed four Hermes-specific skills as first-class references so their
content gains a discoverable home and room to grow without bloating any
primary body:
- skills/hermes-themes -> references/themes.md + templates/skin.yaml
- skills/hermes-desktop-plugins -> references/desktop-plugins.md + templates/plugin.js
- skills/productivity/tui-widgets -> references/tui-widgets.md + templates/clock.mjs
- skills/productivity/petdex -> references/petdex.md
New references extracted from the old body: cli-reference, slash-commands,
providers-and-models, configuration, project-context-files,
security-privacy, background-systems, windows-quirks, troubleshooting,
contributor-guide (native-mcp and webhooks already existed). Also commits
delegate-task-concurrency-diagnosis and portal-auth-for-third-party-apps,
which the old body referenced but the repo never shipped.
Description updated to cover the widened scope:
'Use, configure, theme, extend, and orchestrate Hermes Agent.' (60 chars)
skills/dogfood/SKILL.md sat at the root of the bundled skills tree,
making it one of the few uncategorized skills (Discord /skill
autocomplete listed it under 'uncategorized'; hermes_cli/commands.py
cited it as the example). Move it to
skills/software-development/dogfood/ alongside the other QA/testing
skills (test-driven-development, systematic-debugging,
requesting-code-review).
- git mv skills/dogfood -> skills/software-development/dogfood
(history preserved)
- fix test fixture path in tests/tools/test_browser_console.py
- update root-level-skill docstring examples (commands.py,
generate-skill-docs.py) to cite computer-use, which is still root-level
- drop 'dogfood' from the category list in hermes-agent-skill-authoring
SKILL.md (en + zh-Hans docs mirrors)
- docs: regenerate/move the bundled page to
software-development-dogfood (en + zh-Hans), update sidebars.ts,
skills-catalog.md, and the adversarial-ux-test related-skills link
E2E validated: fresh sync_skills() copies to the new nested path;
existing installs with the old flat copy keep it untouched (manifest is
name-keyed, hash unchanged -> skipped, no duplicate);
_get_category_from_path resolves 'software-development'; docusaurus
build green.
Maintainer scoping decision for the #51226 salvage: document that
select_context() is for engines that must REPLACE per-request context
(retrieval/routing) — pre_llm_call is inject-only by documented cache
design; that observation-only plugins should implement a MemoryProvider
(sync_turn) rather than a context engine, with on_turn_complete scoped
as the observation mirror for engines that already select; and that a
non-no-op select_context naturally changes the prompt-cache prefix on
turns where the selection changes — engines should return stable
selections when nothing changed.
- website guide: on_turn_complete() now carries the same best-effort coverage
caveat as the ABC docstring (fires from the finalization seam; abnormal
early-return paths bypass it) — removes the doc/code inconsistency.
- test: finalization seam emits on_turn_complete with usage=None + the
interrupted flag for an interrupted finalized turn. Docstring records that
the negative early-return-bypass half is best-effort and deferred to a
shared-seam follow-up rather than pinned via a full run_conversation harness.
- _apply_context_engine_selection: reject an empty list. all([]) is True, so
a [] returned by a failing/buggy engine previously replaced a valid request
with an empty message list the downstream sanitizers can't restore; now it
falls open to the unmodified request (honors the fail-open contract).
Thanks @johnnykor82 for catching this on #41918's review.
- test: empty list keeps the original request (fail-open regression).
- docs: document select_context()/on_turn_complete() in the public
context-engine plugin guide (were still describing only the old contract).
Routine automatic compression stays silent-by-design on chat platforms
(default unchanged, byte-identical). New opt-in config key
compression.progress_notices (bool, default false) opens a gate on the
gateway noise filter (_prepare_gateway_status_message) that lets ROUTINE
compression progress statuses through to chat surfaces.
- Membership is derived from the #69550 status template constants in
agent/conversation_compression.py (compiled to a literal-escaped regex,
never re-inlined wording), so unrelated noisy statuses (aux failures,
provider retry/rate-limit chatter) stay suppressed even when enabled.
- The compaction completion notice (COMPACTION_DONE_STATUS, #69546
lifecycle 'compacted' edge) already flows through the status path and
passes the filter — no new emit site needed.
- Config wired everywhere: hermes_cli/config.py DEFAULT_CONFIG,
cli-config.yaml.example, gateway raw-YAML read (live, mtime-cached),
gateway hot-reload cache-busting key list, website configuration docs.
- Failure notices and manual /compress feedback remain always-visible;
VISIBLE_COMPRESSION_MESSAGES and emit sites untouched.
Design by @havok-training (issue #52995).
Per the 'when in doubt, optional' rule — the live-kernel workflow needs
uv + JupyterLab + a running server + a cloned hamelnb repo, a niche
setup that shouldn't ship active by default.
Renamed jupyter-live-kernel -> jupyter-notebook (skill name, page slugs,
catalogs, sidebar, zh-Hans mirror, darwinian-evolver related_skills).
Install via: hermes skills install official/data-science/jupyter-notebook
Extends the fanout enum with 'every_n:<N>' (N >= 2): advisors run on the
first iteration of each user turn and every Nth tool iteration after it;
off-cadence iterations REUSE the cached guidance from the last on-cadence
run via the same cache mechanism the user_turn fanout uses, so the
aggregator still gets advice on every step. The cadence counter is scoped
per user turn (resets on a new user message) and only advances when the
advisory state actually changes, so streaming retries never consume a
cadence slot. Mapping form {mode: every_n, n: N} normalizes to the
canonical string. Unknown/degenerate values fall back to per_iteration.
Addresses issue #63393 (advisor fan-out multiplies turn latency/cost by
the tool-iteration count). Redesigned from PR #63448: the submitted shape
skipped references entirely on off-cadence iterations (aggregator ran
advice-less); this version keeps the last advice in play, credited for
the idea and cadence framing.
Config-gated, default-off (default fanout remains per_iteration).
Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
Follow-up fixes on top of the salvaged #22566 mechanism:
- N-collector now counts only REAL actionable user turns via
_is_actionable_user_turn + _is_synthetic_compression_user_turn —
the same filter pair _find_last_user_message_idx uses post-#69291.
The contributor's bare role=='user' + _is_context_summary_content
check let blank platform echoes and continuation/todo rows consume
N slots, silently degrading the guarantee.
- Default flipped 3 -> 1 (behavior-preserving): a default of 3 was
measured to change the tail cut on transcripts whose budget covers
only the last turn. min_tail_user_messages=1 delegates to the
existing single-user anchor; N>1 is opt-in, and the call site is
gated so the default path is byte-identical to main.
- Hardened config parse in agent_init (bool rejected, fractional
floats rejected, floor 1) matching the max_attempts parser shape.
- Wired the recurring external-PR config gaps: hermes_cli/config.py
DEFAULT_CONFIG + cli-config.yaml.example (PR only had cli.py).
- Regression tests: blank echoes / synthetic rows don't count toward
N; tool-call/result pairs never split by the N-boundary (no-orphan
both directions); N-guarantee wins over tail_token_budget and the
_MAX_TAIL_MESSAGE_FLOOR (floor is a minimum, not a cap); default
parity pin; DEFAULT_CONFIG pin.
The phase-1 tool-result prune only runs inside compress(), which fires
near 50% of the context window, so it never triggers on large-window
models; old tool outputs then ride in history and are re-sent every turn.
Add prune_tool_results_only(): the same no-LLM prune on a separate, low
proactive_prune_tokens trigger, run as an elif to the compression branch.
Opt-in (default 0), protects the recent tail by message count.
Add the method to the ContextEngine base as a no-op default so pluggable
engines inherit it safely (the post-tool-call path never AttributeErrors on
a non-built-in engine); the built-in compressor supplies the real prune.
Register both keys under the top-level compression config with defaults and
document them.
Add --long-description / --long-description-file to `hermes slack
manifest` so the generated app manifest can carry Slack's
display_information.long_description (175–4,000 characters), with
validation of the length bounds, mutual-exclusion with --slashes-only,
and UTF-8 file input. Also propagate the manifest command's exit status
through cmd_slack so validation failures reach the shell.
Squash of the two commits from PR #65256 — one commit per contributor
on this salvage branch.
Salvaged from #65256
Build the full reaction pipeline on top of the #29916 base:
- Opt-in gate: slack.reaction_triggers (default OFF — reaction events
stay acked-and-dropped so busy channels don't wake the agent on every
emoji). 'true' routes reactions on the bot's OWN messages; an explicit
emoji-name list routes those emojis from any message (handoff flows).
- reaction_removed events now route too, distinguished by the
cross-platform text convention reaction:added:<emoji> /
reaction:removed:<emoji> (matches the Feishu and Photon adapters, so
agents and skills see one shape everywhere).
- Authorization: the reactor becomes the synthesized message's user, so
the early _is_user_authorized gate and allowed_channels whitelist
apply exactly as for typed messages. _hermes_force_process only skips
the mention requirement (a reaction on the bot's own message is
definitionally addressed to the bot), mirroring Feishu/Photon.
- Gateway hooks (#33111 by @johnkattenhorn): every human reaction on a
message item fires reaction:added / reaction:removed through the new
BasePlatformAdapter.set_reaction_handler → GatewayRunner
._handle_reaction_event → HookRegistry.emit, independent of the
routing opt-in. Documented in hooks.md.
- Channel handoff (#45265 by @Kev-fs): slack.reaction_trigger_target
routes the reaction turn to a configured channel (top-level via
_hermes_no_thread_response + reply-anchor suppression in
gateway/platforms/base.py) or C123:<ts> thread.
- Manifest: reaction_removed event subscription added alongside
reaction_added/reactions:read.
- Docs: slack.md Reaction Triggers section; hooks.md event table rows.
Also credits #44508 by @harrisonmedmedmetrics (inbound reaction_added
handling — same plumbing class, superseded by this consolidated shape).
Co-authored-by: johnkattenhorn <john.kattenhorn.personal@gmail.com>
Co-authored-by: Kev-fs <kevin@fleetsmarts.net>
Co-authored-by: harrisonmedmedmetrics <harrison@medmetricsrx.com>
Documents user-facing wave-1+2 Slack behavior that had no docs coverage:
- decision table for require_mention / free_response_channels /
require_mention_channels / thread_require_mention / strict_mention /
ignore_other_user_mentions and how they compose
- 'Accepting messages from other bots' section with the post-#69483
semantics: allow_bots=mentions requires a CURRENT mention from
peer bots (text or Block Kit blocks); thread state never admits them
- clarify one-tap buttons (choice buttons + Other free-text mode,
in-place resolution, double-click guard, expiry message)
- slash replies are ephemeral: replace-ack, chunking, 5-post cap with
explicit truncation notice, postEphemeral fallback, never-public rule
- cron deliver targeting (slack -> home channel, slack:C... channel,
slack:U... resolved to DM) incl. standalone sender + MEDIA uploads
- send_message media + bare-user-ID DM resolution and caption behavior
Refs #26184.
Salvaged from #45765 by @navahc09 — kept the PR's callout structure
and placement, rewrote the content to match current behavior:
Slack blocks native slash commands in threads and never delivers them,
so Hermes recognises a leading '!' as an alternate command prefix.
Post-C3 command fixes the bang form also works behind a mention
(@Hermes !cmd) and with leading whitespace; unknown '!' tokens pass
through to the agent unchanged. Cross-linked the detailed
slack.md section.
- Add SLACK_THREAD_REQUIRE_MENTION, SLACK_IGNORE_OTHER_USER_MENTIONS and
SLACK_REQUIRE_MENTION_CHANNELS to the conftest behavioral-env force-clear
list so config-loader side effects can't leak between tests (same class
of leak the existing SLACK_* entries guard against).
- Document thread_require_mention and require_mention_channels in the
Slack messaging guide next to the other mention-gating options.
Adds the option to the Mention & Trigger Behavior section: leading-
mention semantics, opt-in default, env var, and DM/MPIM scope (1:1 DMs
unaffected; MPIMs apply it like channels).
Claude-Session: https://claude.ai/code/session_01TKsNdptNdo9CqT2u7JMdkH
markVoicePlaybackInterrupted() / takeVoicePlaybackInterrupted() mirror
the backend latch in the renderer (the barge happens client-side, where
the audio plays). VAD barges and typing over playback mark it; the next
prompt.submit carries interrupted:true, which the TUI gateway latches
into the model note.
Removes Homebrew and PyPI wheel/sdist as Hermes distribution paths while
preserving the supported source, Docker, and Nix workflows.
Changes:
- Removes the Homebrew formula, PyPI publish workflow, sdist manifest
(MANIFEST.in), and wheel/sdist release-attachment logic from scripts/release.py.
- Keeps setuptools metadata and entry points required by editable installs
and Docker/Nix builds, but adds a setup.py guard that rejects wheel/sdist
builds outside a sealed Nix derivation (HERMES_NIX_BUILD=1).
- Removes pip/Homebrew install detection, PyPI update checks, the pip
self-update path, the deprecation-banner state, the postinstall subcommand,
wheel data-directory fallbacks in agent/i18n.py and hermes_constants.py,
and the ACP Registry manifest/version-lockstep release logic.
- Adds /nix/store/ path detection so `nix run` / `nix profile install`
installs (which don't set HERMES_MANAGED) are correctly identified as
"nix" rather than falling through to "git"/"unknown".
- Retired install-method values ("pip", "homebrew") in existing
.install_method stamps (both code-scoped and home-scoped) are ignored by
the allowlist reader and fall through to "unknown" instead of resurrecting
a retired enum value.
- Updates Nix packaging to ship bare runtime data (locales, optional-mcps)
through store symlinks and wrapper env vars instead of wheel data-files.
- Removes the ACP Registry manifest/icon and their version-lockstep tests.
- Deletes or rewrites packaging, pip-update, Homebrew, and ACP Registry
tests; adds parametrized coverage for the packaging build guard covering
BOTH sdist and wheel paths (the guards live in separate cmdclass entries
— a passing sdist test proves nothing about the wheel path).
- Updates installation/platform documentation and related user-facing copy.
- Adjusts the supply-chain scan so deleted install-hook files do not trigger
a finding, while additions or modifications still require the existing
ci-reviewed label gate.
Supported installation paths (unchanged):
- git installer (install.sh)
- Docker
- Nix/NixOS
- editable development installs (uv sync, uv pip install -e ., pip install -e .)
Update the CLI and messaging guides so the default `interrupt` mode reflects
the new behavior: a follow-up redirects the active turn (preserving displayed
reasoning and completed work, letting running tools finish at a safe boundary)
rather than hard-stopping it, with `/stop` still the explicit hard stop.
Follow-up for the salvaged #55800 idle-compaction commit:
- turn_context.py: treat a skipped _compress_context (per-session
compression lock held by another path, failure cooldown, anti-thrash
breaker, codex-native routing) as a strict no-op — only re-baseline
conversation_history and re-anchor current_turn_user_idx after a REAL
compaction. Also re-anchor the user-message index after idle compaction
(the PR predates the reanchor helper).
- hermes_cli/config.py: add idle_compact_after_seconds: 0 to
DEFAULT_CONFIG's compression block (the PR only had
cli-config.yaml.example).
- gateway/run.py: add the idle-compaction status wording to
_TELEGRAM_NOISY_STATUS_RE so the new 💤 message stays out of
human-facing chat surfaces (routine compaction is silent by design);
pin it in tests/gateway/test_telegram_noise_filter.py.
- docs: idle_compact_after_seconds in user-guide/configuration.md and
developer-guide/context-compression-and-caching.md parameter table.
- tests/agent/test_idle_compaction_lock_and_guards.py: end-to-end
coverage with a real AIAgent + SessionDB proving the idle path honors
the per-session compression lock (added after the PR), the persisted
failure cooldown, and the anti-thrash breaker, and that the lock is
released after an idle-triggered compaction.
Salvaged from #55800 by @iso2kx. Implements #27579.
Add compression.threshold_tokens config option that sets an absolute
token cap for auto-compaction. When configured alongside the existing
ratio-based threshold, the effective trigger point is the lower of the
two, so compression never fires later than the user's preferred token
count regardless of which model is active.
This solves the problem where switching between models with different
context windows (e.g. 1M → 400K) shifts the absolute trigger point,
causing premature or delayed compression.
Rework from PR #24279 addressing sweeper feedback:
- The cap is now a first-class compressor configuration value
(threshold_tokens_cap parameter on ContextCompressor.__init__),
not a post-construction patch on the live instance.
- Applied in both __init__ and update_model() so it survives model
switches and fallback activations (the old approach was undone by
update_model() restoring _configured_threshold_percent).
- Clamped to the model's context length so a cap above the window is
a no-op (ratio-based threshold wins).
- Works with max_tokens output-token reservations.
- Added 9 tests covering cap-vs-ratio selection, model switch survival,
context-length clamping, max_tokens interaction, and invalid values.
- Updated user-facing configuration docs.
- Removed unrelated background-review/curator/Honcho changes (main
already contains background-review memory isolation in 973f27e95).
Config example:
compression:
threshold: 0.50
threshold_tokens: 200000 # never compress later than 200K tokens
Follow-up to the salvaged contributor commit, closing the three gaps
flagged in the sweeper review:
1. Init ordering: assign compression.model_thresholds to a selected
plugin context engine BEFORE the initial update_model() call in
agent_init.py, so the initial model's override applies from init
(previously it only took effect after the first /model switch).
Base-class ContextEngine.update_model() now snapshots the
pre-override percent once so repeated switches fall back to the
engine's configured threshold, not a previous model's override.
2. DEFAULT_CONFIG: add compression.model_thresholds (empty map) to
hermes_cli/config.py — additive key, no _config_version bump.
3. Docs: document the key in
website/docs/developer-guide/context-compression-and-caching.md
(yaml example, parameter table, dedicated section) and update the
plugin-boundary note in context-engine-plugin.md to state the
explicit context-engine contract for model_thresholds.
Adds tests/run_agent/test_per_model_threshold_init_ordering.py:
plugin-engine AIAgent init regression (override applies at init,
empty map unchanged), DEFAULT_CONFIG key presence, floor interaction
on the model-switch path (override below the small-context floor is
raised to the floor; above the floor wins), and base-class config
snapshot across repeated switches. Also maps @bennybuoy in
contributors/emails/.
Children inherit the parent's env, repo, and toolsets but were denied
execute_code ('children should reason step-by-step, not write scripts').
That forces subagents doing mechanical multi-step work (batch file
reads, fetch-N-pages loops, filter-before-context reductions) to burn
reasoning iterations one tool call at a time.
- Remove execute_code from DELEGATE_BLOCKED_TOOLS
- Stop stripping the code_execution toolset from child bundles
- No recursion risk: the sandbox bridges only the 7 SANDBOX_ALLOWED_TOOLS
(web/file/terminal) — delegate_task and execute_code itself are not
reachable from inside a sandbox script
- Update schema text, AGENTS.md, and delegation docs
- Tests: blocked-constant, strip, and child-assembly tests updated;
new test pins execute_code as intentionally unblocked
The Desktop app can now sign in to a gated gateway using the user's SYSTEM
browser and OAuth 2.0 for Native Apps (RFC 8252) instead of an embedded
Electron BrowserWindow, and authenticates with bearer tokens it holds itself
instead of relying on HttpOnly browser session cookies.
Why brokered: the upstream IDP (Nous Portal) binds client_id to the gateway
instance and only permits redirect_uris on the gateway's own origin, so a
desktop loopback redirect can't be a direct Portal client. The gateway
therefore acts as the authorization server TO the desktop and an OAuth client
TO the Portal, reusing the existing PKCE start_login/complete_login provider
path unchanged.
Server (Ben's dashboard-auth lane):
- native_flow.py: in-memory broker — binds the desktop's PKCE challenge to a
completed Session, mints a single-use, short-TTL, PKCE-verified gateway
authorization code. Constant-time compare, single-use (consumed before the
PKCE check so a wrong verifier can't be retried), capacity-bounded.
- routes.py: GET /auth/native/authorize (starts the brokered PKCE login,
loopback-only redirect_uri, S256-only), POST /auth/native/token (loopback
code + verifier -> tokens in the JSON body, never Set-Cookie), POST
/auth/native/refresh (desktop-held RT rotation). /auth/callback branches to
mint a loopback code + 302 to 127.0.0.1 when a broker_state rides the PKCE
cookie; the cookie/SPA path is untouched.
- middleware.py: the gate accepts Authorization: Bearer <access_token>,
verified via the same verify_session provider stack (no cookie set/read),
with the same "provider unreachable -> 503, not logout" semantics.
- web_server.py /api/status: advertise auth_flows (["cookie","native_pkce"])
so clients can detect the capability; native_pkce only when a brokerable
OAuth provider is registered.
Desktop (Ben's lane):
- native-oauth.ts: pure PKCE/capability/URL/callback/token helpers.
- native-oauth-login.ts: loopback-listener orchestration (system browser via
openExternal, ephemeral 127.0.0.1 listener, state/PKCE verification), all
I/O injected for testability.
- main.ts: capability-gated oauth-login IPC — native flow when advertised,
automatic fallback to the existing embedded-webview cookie flow otherwise;
tokens stored encrypted (safeStorage/OS keychain), REST + ws-ticket
authenticated by bearer, transparent refresh, logout clears both shapes.
Tests: 18 server pytest (broker unit + full authorize->callback->token E2E +
cookieless bearer auth of a gated route + ws-ticket mint + capability
advertisement + refresh); desktop node --test/vitest for both pure modules
(PKCE, capability detection, callback CSRF, loopback round trip, timeout,
browser-open failure). Electron project typechecks clean.
Docs: website/docs/guides/desktop-native-signin.md.
Reworks the salvaged command module into a CommandSource(SecretSource)
registered as the third bundled source, composing with Bitwarden and
1Password through the apply_all() orchestrator — enable any combination
simultaneously. The original PR's secrets.provider single-selector is
deliberately dropped: multi-source is first-class and a mutually
exclusive provider switch would regress that.
- fetch() only fetches; precedence/override/conflicts/environ writes stay
in the orchestrator. ErrorKind classification + remediation hints.
- apply_command_secrets() kept as a legacy shim (parser/security helpers
unchanged: HERMES_SECRET_KEY data-only key passing, cross-key misroute
guard, base64-padding disambiguation, timeout + output cap, structured-
fields-only failure logging, stderr discarded).
- Dispatch tests rewritten for the registry path incl. an explicit
two-sources-compose test; selector tests removed with the selector.
- cli-config.yaml.example + docs page (command.md), secrets index entry.
- contributors mapping for mvalentin@valensys.net -> 0xr00tf3rr3t.
Fixes the profile-clobber bug cluster at the apply_all() chokepoint so
every secret source — bundled and plugin — gets both behaviors for free:
- secrets.preserve_existing (#58073): env var names whose existing .env /
shell value always wins, even against a source with
override_existing: true. Escape hatch for per-profile platform
secrets while everything else rotates centrally.
- Profile aliasing (#51447): under a named profile, an applied
FOO_<PROFILE> var (credential-shaped suffixes only) also hydrates the
canonical FOO, so adapters/plugins that read fixed env names see the
profile's value. Direct supply beats alias; protected/claimed/
override guards all apply; secrets.profile_alias: false disables.
Reimplements the intent of PR #58085 (tianma-if, preserve_existing on the
legacy Bitwarden apply shim) and PR #51616 (LeonSGP43, profile aliasing
inside the Bitwarden backend) on the SecretSource orchestrator that
superseded those code paths.
Fixes#58073. Fixes#51447.
Co-authored-by: tianma-if <5895871+tianma-if@users.noreply.github.com>
Co-authored-by: LeonSGP43 <154585401+LeonSGP43@users.noreply.github.com>
Keep the env-presence row, but add a real Bitwarden probe so revoked or malformed tokens no longer look healthy in hermes secrets bitwarden status.
Also document the new status behavior and lock it in with a dedicated regression test.
Refs: NousResearch/hermes-agent#40275
Tested: ./scripts/run_tests.sh tests/hermes_cli/test_bitwarden_status.py tests/test_bitwarden_secrets.py
Tested: .venv/bin/python -m ruff check hermes_cli/secrets_cli.py tests/hermes_cli/test_bitwarden_status.py
Add an opt-in battery indicator to the CLI and TUI status bars, shown as
the first element and colour-coded by charge (green/yellow/orange/red, or
green while charging). Off by default and a no-op on machines without a
battery.
- agent/battery.py: shared psutil-backed reader with a short TTL cache,
category bucketing, and a compact 🔋/⚡ label. Fails open to
"unavailable" everywhere.
- CLI: /battery [on|off|status] toggle persisted to display.battery,
rendered first in every status-bar width tier.
- TUI: /battery slash command, config sync, a system.battery RPC polled
while enabled, and a pinned first segment in StatusRule.
* feat(desktop): configure repository discovery
* fix(config): preserve additive default migration
* fix(desktop): stabilize session-actions-menu gateway mock for repo-scan subscribe
projects.ts now runs $gateway.subscribe(syncReposScanning) at module load, and
nanostores fires the subscriber synchronously. session-actions-menu.test.ts
reaches projects.ts transitively via the session store but mocked
@/store/gateway without $gateway, crashing the whole desktop vitest suite
("No \ export is defined"). Simply adding $gateway: atom(null) exposed a
second issue: the synchronous subscriber calls the mock's activeGateway()
during the transitive import, before the module-level const initializes (TDZ).
Hoist the mock fns via vi.hoisted() so activeGateway is defined before the
hoisted vi.mock factory runs, and add $gateway: atom(null) to the mock. Mirrors
the self-contained mock pattern already used in projects.test.ts. Also maps the
PR author's commit email for attribution.
Supersedes #67630; incorporates review feedback from that PR.
Co-authored-by: Rudimar Ronsoni <rudimar@outlook.com>
---------
Co-authored-by: Rudimar Ronsoni <rudimar@outlook.com>
Co-authored-by: Austin Pickett <austinpickett@users.noreply.github.com>
* feat(secrets): one-command token rotation + actionable startup errors for all secret sources
When a Bitwarden machine-account token expired, users saw a raw Rust
error dump (invalid_client + Location: + backtrace hints) and the only
fix was manually editing .env or re-running the whole setup wizard.
- New `hermes secrets bitwarden token` / `hermes secrets onepassword
token`: paste a new token (masked prompt or flag), the command probes
the backend BEFORE persisting — a rejected token changes nothing; a
good one is written to .env and the fetch caches are cleared.
- New optional SecretSource.remediation(kind, cfg) hook: startup
warnings now print a '→ Run `hermes secrets <name> token`…' fix-it
line after any fetch error, for bundled AND plugin sources (generic
per-ErrorKind defaults in the ABC).
- bws stderr is summarized to its cause line (Location:/backtrace noise
dropped) and invalid_client/invalid_grant/400 identity rejects are
now classified AUTH_FAILED (was INTERNAL) with a plain-English
explanation naming the token env var.
- op whoami probe accepts a candidate token so rotation validates the
NEW credential, not the ambient one.
Additive hook with defaults — no SECRET_SOURCE_API_VERSION bump.
* docs: fix MDX parse error in secret-source-plugin hook table
Escaped backticks around a <name> placeholder made MDX parse it as an
unclosed JSX tag, breaking the docs-site build. Use a plain code span
instead.
Three related messaging-approval fixes:
1. approvals.timeout default 60 -> 300. PR #63501 collapsed the gateway
wait onto the canonical approvals.timeout (previously
gateway_timeout=300), silently shrinking messaging approval windows
to 60s. Push-notification approvals routinely arrive later than a
minute; taps landed after the wait had already failed closed.
2. Stale-tap honesty: adapters resolved the approval AFTER rendering
'<checkmark> Approved by <user>' (Telegram/Discord/Slack), or ignored a zero
resolve count (WhatsApp Cloud/Feishu). A tap on an expired prompt
claimed approval while the command had already been denied. All
button paths now resolve first and render 'Approval expired -
command was not run' when nothing was waiting.
3. Mixed-warning prompts (dangerous pattern + tirith finding) now offer
Always: the persistence layer already permanently allowlists the
pattern key and downgrades the tirith key to session scope, but the
UI hid Always whenever ANY tirith warning was present. Pure-tirith
prompts still withhold Always (content findings are session-max by
design), and Smart-DENY overrides remain once-only.
* fix(billing): rename user-facing "terminal billing" copy to Remote Spending
The capability was renamed Remote Spending on the portal (consent CTA:
"Allow Remote Spending"; per-terminal states Granted/Stopped), but the
terminal, desktop, and docs still said "terminal billing" everywhere.
- Feature name: Remote Spending in titles/labels, lowercase mid-sentence.
- Step-up action verb is now "allow", matching the portal consent CTA.
- Kill-switch-off recovery copy points at the actual control ("a billing
admin can turn it on from the portal's Hermes Agent page") instead of
the dead-end "manage it on the portal".
- Per-terminal revoke copy uses the portal vocabulary ("stopped").
- Wire identifiers (cli_billing_enabled, cli_billing_disabled, ...) are
unchanged; copy, comments, docs, and test expectations only.
* fix(billing): correct the post-step-up denial diagnosis + finish the desktop rename
Adversarial review findings: (1) a repeated insufficient_scope after a
successful step-up is a per-terminal authorization failure, but the copy
blamed the org kill-switch and pointed at the wrong recovery control —
now: "Remote Spending still isn't active for this terminal — the
authorization didn't take. Retry, or make this change on the portal."
(2) the desktop step-up flow started in Remote Spending vocabulary but
finished in "billing management access" — renamed both end states.
(3) prettier formatting on the touched files (matches the post-merge
fmt bot).