287c56e95afe5c528beacb7ca8f7ef0ad6216f2a
724 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
81140e4546 |
fix(profiles): ship a retry path for the rename identity migration
A rename under a live multiplexer that could not reach the control verb warned and stopped there, leaving the operator with no way to finish: the rename cannot be repeated (profiles/<old> is gone) and the CLI deliberately never rewrites the routing DB a live gateway holds in memory. - `hermes profile migrate-identity <old> <new>`: retries the migration — delegates to the gateway control verb while a multiplexer is live, performs the durable rewrite of both state DBs when none is. Idempotent, and exits non-zero naming the offending database on a collision, a lock, or a partial failure. Only the name format and the existence of the new profile are checked; the old profile directory is expected to be gone. - An older gateway that does not implement the verb is reported as such (`identify` answers while the migrate verb does not), not as "no gateway". - `_migrate_profile_identity` returns an explicit success/failure result so the command can set its exit code; the rename warning now names the exact invocation. - A failed control answer keeps the raw payload when it carries no reason field. - The offline failure branch called `click.echo` in a module that never imports `click`: a failed second database raised NameError instead of printing its warning. |
||
|
|
3abeca16e6 |
fix(kanban): 5xx and timeouts requeue the worker instead of spending its retry budget
`server_error` and `timeout` join the transient-provider set that makes a Kanban worker exit 75 (EX_TEMPFAIL). A provider outage or a hung connection says nothing about the task, so the dispatcher requeues without a failure tick rather than counting toward the circuit breaker (#91206 proposed the same set). |
||
|
|
08f36192b5 |
fix(config): drop the "Hermes does not read this" note on config set
The code registry cannot tell a plugin-only name from one the gateway reads straight off os.environ (TELEGRAM_GROUP_ALLOWED_USERS), so the note was false for real settings. Every UPPER_SNAKE name simply lands in .env; the docs say so. |
||
|
|
f8e8cacf35 |
fix(config): route every UPPER_SNAKE key from hermes config set to .env by shape
`hermes config set TELEGRAM_GROUP_ALLOWED_USERS ...` (and ~290 other documented variables Hermes reads straight from os.getenv without registering them in OPTIONAL_ENV_VARS) still landed as a config.yaml top-level scalar with a notice, while the setup flows write .env and one-shot CLI readers never bridge YAML scalars — two writers, two readers. #112250 routed the registered names; this closes the class with a shape rule: any bare ^[A-Z][A-Z0-9_]*$ key is an environment setting. - set: writes .env, drops a stale config.yaml copy, never writes UPPER_SNAKE into config.yaml (--force included); the env writer's denylist (HERMES_YOLO_MODE, PATH, ...) now refuses cleanly instead of the YAML detour bridging the value into os.environ; a name neither registered nor in the environment-variables reference gets a one-line note but is still saved. - get: .env first; a leftover top-level config.yaml copy is reported as stale. - unset: removes the .env entry and the stale copy. - Registered names, credentials (credential lifecycle + masking), dotted paths and lowercase bare keys are unchanged. Fixes #111848 (first half landed in #112250). |
||
|
|
23863ccbaf |
fix(cli): one-shot chat -q exits non-zero on failure; 75 covers upstream 429 and overload
The non-quiet one-shot path exited 0 unless a Kanban worker was running, so scripts could not tell a failed `hermes chat -q` from a good one and an incomplete turn (partial, iteration budget) still read as success (#111770). Both one-shot paths now share one contract: 0 completed, 1 failed / partial / incomplete / never ran, 130 interrupted. The Kanban EX_TEMPFAIL sentinel also fires for `upstream_rate_limit` (aggregator's upstream 429) and `overloaded` (503/529): neither says anything about the task, so the dispatcher should requeue without a failure tick rather than count it toward the breaker. |
||
|
|
abdb402701 |
fix(mcp): carry the lazy status across the TUI wire, tests and docs
Follow-up to the ported status fix: - `tui_gateway/contracts/tools_mcp_plugins.py::McpRuntimeStatus` is a closed wire enum; `mcp.servers.status` would raise `ContractViolation` on the new `lazy` value. Declare it and regenerate the TS/OpenRPC contract files. - `ui-tui` session panel: an unknown status fell through to the red `failed` branch; render `lazy` with its cached tool count (inline branch, no component extraction). - Two invariant tests, both red on origin/main: the real discovery path yields `status: lazy` with the cached tool count and a summary without `failed` (eager control stays `configured`, live control stays `connected`); a lazy-only run neither warns nor re-arms the startup retry, while a configured-only run still does. - Document the per-server `lazy` key (undocumented until now) in `cli-config.yaml.example`, the MCP config reference and the MCP guide. |
||
|
|
e133f3f607 |
fix: device OAuth login scans every advertised authorization server
`hermes mcp login <server> --flow device` took `authorization_servers[0]` from the protected-resource metadata and failed when that entry was a browser-only or issuer-inconsistent server, even though a later entry was the issuer-bound device_code server meant for headless clients (Higgsfield advertises exactly this shape: a PKCE server first, the device server second). Discovery now tries each advertised server in order and binds to the first whose metadata issuer matches its advertised URL and that offers device authorization. Issuer validation (RFC 8414 / SEP-2468) is unchanged per server; a single-server resource raises exactly the error it raised before, and a multi-server resource with no usable entry reports every attempt. The browser path (`tools/mcp_oauth_manager.py` pre-flight) is deliberately left on the SDK's own first-entry selection: the SDK's 401-branch discovery re-selects `authorization_servers[0]` itself, so a divergent pre-flight pick would only desynchronise the cached metadata from what the SDK authorizes against. |
||
|
|
43e7e830fd |
fix(tools): fold ~/.local/bin into the POSIX PATH completion siblings, tests + docs
Slim follow-up to the salvaged #111790: the helper becomes a list-returning sibling of _managed_runtime_path_entries (same shape, same "only when it exists" convention) and loses the Windows check the caller already performs. Why here and not in the Electron remote spawn: propagating the login-shell PATH that locateHermes discovered into `exec env HERMES_DESKTOP=1 … hermes serve` would fix only the Desktop SSH surface; the terminal environment's PATH completion is the seam every thin-PATH launcher (SSH, systemd, launchd, cron) already goes through, so the class closes once. Windows twin out of scope. Tests move to the mirror dir tests/tools/environments/ with an absent-dir control; FAQ documents the terminal PATH composition. Fixes #111778 |
||
|
|
da9810387d |
docs(config): scope the .env routing claim to registered env settings
Only names in OPTIONAL_ENV_VARS / _EXTRA_ENV_KEYS and the platform *_HOME_CHANNEL / *_ALLOWED_USERS suffix family route to .env; other documented ALL-CAPS names still land in config.yaml as top-level scalars with a notice. Say so instead of 'every documented environment variable' (#111848 stays open for the remaining names). |
||
|
|
0e63a1bc5c |
fix(config): refuse an unknown path under a known section before writing
`hermes config set gateway.discord.gateway_restart_notification true` wrote the
typo into config.yaml and only then printed the "not a recognized config key — it
was saved anyway" notice (#112003). Under a KNOWN section an unknown sub-key can
only be a typo, so `set_config_value` now exits non-zero via `_exit_invalid`
before reading or writing config.yaml, with the did-you-mean hint.
Scope preserved from
|
||
|
|
70e4938c07 |
fix(config): route every registered env setting through .env from config set/get/unset
`hermes config set FEISHU_HOME_CHANNEL oc_x` wrote the top level of config.yaml while the platform setup flows and /sethome write the same name to .env via save_env_value, so two writers fed two readers: the gateway bridges the yaml copy into the environment only when .env lacks the name, one-shot CLI readers never bridge, and the two copies diverged silently (#111848). Only credential-shaped names were routed to .env because `_is_env_config_key` is the provider-credential predicate. Follow-up to KoNit-K's cherry-picked fix (#111850), which routed the `setup_hidden_env` suffix family: the predicate now lives in the topical sibling `hermes_cli/config_env_routing.py` and covers every bare name Hermes itself registers as an environment variable (OPTIONAL_ENV_VARS, _EXTRA_ENV_KEYS — "env var names written to .env" — plus the setup-hidden suffixes for plugin adapters nobody enumerated), so `*_ALLOWED_USERS`, `WHATSAPP_MODE`, `MATRIX_PASSWORD` and the rest of the adapter-saved family take the same file. `set` and `unset` also drop a stale same-named top-level config.yaml copy so the reporter's drift cannot come back, and `get` resolves .env first then that copy — the gateway's own read order. Provider credentials keep the credential_lifecycle rotation path. Docs: environment-variables.md tip, hermes_cli/AGENTS.md config rule. |
||
|
|
0e0692240a |
test: fold the named-source control into one clone-all test; document the skipped trees
Trim the three contributor tests from #101340 to two invariants: the root-only ignore test now also asserts that a named profile used as source keeps its own models/ (the exclusion is gated on the default root), and the end-to-end create_profile(clone_all=True) test stays. Same assertions, two tests — the salvage bar. Docs: profile-commands.md and profiles.md list models/, runtimes/ and node/ among the --clone-all exclusions so users know why a clone from the default profile does not carry the local-model weights. |
||
|
|
d1bd778a5e |
fix(checkpoints): clear-legacy trims to two real-path tests, aligns failure line with the log
Replace the three monkeypatch-heavy tests from the salvaged commit (which faked clear_legacy's return dict, so they could not catch the manager hunk regressing) with two invariant tests that drive the real cmd_clear_legacy against a temp checkpoint base: an undeletable legacy-* dir yields exit 2 plus the "Could not delete" line while the archive stays on disk; a clean sweep keeps exit 0 and the unchanged success line. The green-path guard is harvested from #111789. Reword the CLI failure line to "Could not delete N archive(s) (see logs)." so it matches the manager's WARNING wording and the text proposed in #111776, and document the exit code in the CLI reference. Co-authored-by: KoNit-K <124019182+KoNit-K@users.noreply.github.com> Co-authored-by: Konstantin Khlopkov <47825603+kokhlo@users.noreply.github.com> |
||
|
|
fa915784cb |
fix: merge shipped skill categories instead of replacing them on update
The per-root merge in _copy_dist_payload treated the first level under skills/ as the replace unit, but skills live at skills/<category>/<skill>. A distribution shipping one skill inside a category rmtree'd the whole category directory, wiping every skill the user (or hermes update) had placed there — the exact scenario of issue #25120 that this PR claims to fix. A shipped directory with no files of its own is now a container: its children are merged one level down, and the replace unit becomes the nearest directory that holds a file (a skill dir always holds SKILL.md). A symlinked category (skills/devops -> shared dir) is a container too, so the pre-write symlink check now covers it instead of silently unlinking the link and dropping the shipped copy in its place. Review finding: categorised skill payload rmtree'd skills/<category>/, destroying sibling user and bundled skills; symlinked category was unlinked rather than refused. |
||
|
|
ab7b97f55e |
docs(distribution): describe per-root merge of skills/ and cron/ on update
The user guide, command reference, `hermes profile update --help` and the install-over-plain-profile warning all said skills/ and cron/ are overwritten wholesale; they are now merged per skill / cron job, so say so, and document the symlinked-container refusal. |
||
|
|
a9a8a3fa2e |
fix(config): hermes config get masks credentials on every path; --raw opts out
`hermes config get providers`, `config get providers.<p>.api_key`, `config get <PROVIDER>_API_KEY` (the .env-routed branch) and `config get mcp_servers.<s>.env.X_API_KEY` all printed the full credential. The agent runs this command from sessions whose transcripts persist and get forwarded (a Gemini key surfaced in a Discord DM log), so `print` output is a leak path the logging redactor never sees. `get_config_value` now applies the structural masker used by `config show` before printing, honouring `security.redact_secrets` (default on), with a `--raw` flag for operators/scripts that need the real value. `_is_secret_config_key` extends the exact-name set with the same `*_API_KEY / *_TOKEN / *_SECRET / *_PASSWORD` suffixes `_is_env_config_key` already routes to .env, so env-map leaves under `mcp_servers.*.env` mask too, and the `config set` echo uses the same predicate. Slim redo of #84153 by @webtecnica (same direction: mask in get_config_value; dropped the redact_url_query_params re-export and the separate redaction-enabled reader in favour of agent.redact._redact_enabled, which already resolves the profile-scoped policy). Fixes #110758 Fixes #84106 |
||
|
|
7bb52c0b74 |
feat: profile clone can opt into staying synced with its source (--sync-imports)
`hermes profile create <name> --clone` copies whatever `hermes import-agent` had pulled into the source profile, but leaves import-sync.json behind, so the clone can never run `import-agent --sync` itself: its imported skills and memories freeze at clone time. `--sync-imports` (with --clone / --clone-from) also copies the manifest. It is deliberately narrow: the manifest points at EXTERNAL Claude Code / Codex trees, never at the source profile, so both profiles remain independent islands (root AGENTS.md ruling) — config.yaml, SOUL.md and skills are still one-off copies. Opt-in, one-directional, explicit; --clone-all already carries the file as part of the full copy. Refused without a clone source. |
||
|
|
5083d5f78e |
fix(gateway): honour allow_all_users from config.yaml by bridging it to GATEWAY_ALLOW_ALL_USERS
`gateway.allow_all_users: true` (and the top-level spelling) in config.yaml
was a silent no-op: `_TOPLEVEL_BRIDGE` never forwarded it, GatewayConfig has
no field, and every allow-all reader (authz mixin default-deny branch,
startup access check, own-policy adapters, Discord/Matrix/Email plugin
gates) consults the GATEWAY_ALLOW_ALL_USERS env var only.
Bridge the YAML key into that env var in `bridge_core_env_settings`, the
one seam every reader already shares, instead of threading a new config
attribute through ten readers:
- first-writer-wins: an explicit env var beats YAML (matches every other
{PLATFORM}_* gate);
- skipped inside a multiplexed secondary profile's scope (#80099 class):
the secondary's config.yaml must not become the default profile's policy,
and the isolation test now asserts GATEWAY_ALLOW_ALL_USERS stays unset;
- a startup warning names config.yaml as the grant source, because the key
was inert until now and a forgotten `true` flips the posture to open.
Tests: both spellings authorize a stranger through `_is_user_authorized`;
`false`, absent key, and env=false over YAML=true all stay denied.
Docs: security guide, env-var reference, gateway internals.
Fixes #110690
|
||
|
|
569b4242a3 |
fix(auth): a Portal-returned inference host is accepted only when the operator named it
#111809 accepted any *.nousresearch.com https host once the operator's Portal override pointed at a non-production Portal. That let a network-provenance value — the Portal's refresh response — pick any Nous-owned host as the bearer recipient, including hosts that are not inference gateways. Owning the DNS suffix is not the same as being an authorized recipient, and the validator's threat model (an injected refresh response) is exactly the case a suffix rule fails to bound. The recipient is now the operator's own NOUS_INFERENCE_BASE_URL: a non-production host returned by the Portal is accepted exactly when it equals that override's host, otherwise the strict production set stands. The Portal override grants nothing by itself. What the operator gains over plain use of the override is that the Portal's value is then persisted and used for the pricing scope and proxy instead of being healed to production, and the per-turn "refusing inference URL host" warning stops. No environment is named in code. Raised on #111809 review. Tests: recipient match accepted, unrelated Nous host refused, no override refused, Portal override alone grants nothing, the match follows the profile scope under multiplexing; the widening cases are red on main. Docs row for NOUS_INFERENCE_BASE_URL. Co-authored-by: Ben Barclay <ben@nousresearch.com> |
||
|
|
398279fd6a |
feat(skills): add ip-as-logo optional skill (minimal cute IP mascot marks)
Ports s1dashu/ip-as-logo-skill (MIT, 3.2k stars in 48h, snapshot of commit b1bf517c) into optional-skills/creative/. Generates extremely simplified, cute IP mascot characters readable at 32x32 — 3-color discipline, corner-emergence composition, complexity budget, and a copy-paste prompt skeleton. Hermes adaptations (blockquote header + inline edits, upstream body otherwise intact): - image path routed through the built-in image_generate tool (square aspect, main-prompt constraints mode — no negative_prompt parameter exists) - subagent parallelization mapped to delegate_task, optional - delivery per platform file conventions; no auto-QA (per upstream's own one-pass-draw rules) - live-test friction fixes folded in: reduced-batch labeling branch, proposal-round skip for pre-authorized batches, dimensions-reporting rule when the backend returns only a URL, limbless-subject note Validated via a cold subagent run (2 candidates for a real brief): both generations succeeded first-draw, verdict SHIP; its three friction findings are addressed in this commit. Docs: catalog row + sidebar line + generated skill page (scoped to this skill only; regen drift for unrelated pages reverted). Credit: s1dashu (https://github.com/s1dashu/ip-as-logo-skill) |
||
|
|
b531023622 |
fix(gateway): drop the redundant whole-block lease; one invariant test per atom; document the watchdog env vars
The per-step leases inside maybe_auto_archive / maybe_auto_prune_and_vacuum
(archive, prune, sweep, vacuum) cover every long step of the construction-time
block, and each renews right before the step it protects, so the extra
report_startup_progress(900) at the top of GatewayRunner._init_session_db
added nothing but a stale phase label ("gateway_startup_state_maintenance"
would outlive the archive step and mask the phase name in the fired record).
Dropped; gateway/run.py is back to origin/main.
Tests: the two contributor tests monkeypatched report_startup_progress in the
module and asserted phase names (change-detectors on the strings). Replaced by
one test that arms a REAL StartupWatchdogHandle and asserts the maintenance
block renews it four times with lease_until in the future — the property the
poller's `lease_until > now` branch actually needs (#111092). Red on
origin/main: lease_count stays at the schema-init lease.
Docs: HERMES_STARTUP_WATCHDOG / HERMES_STARTUP_WATCHDOG_TIMEOUT_S existed only
in the module docstring; add them to website/docs/reference/environment-variables.md
next to the respawn-storm variables (existing env vars only, no new surface).
|
||
|
|
ac63d0eea5 |
fix(agent): execution guidance and browser hints drop the web_search stripper; tests assert the invariant
The rebased guidance text no longer names web_search anywhere, so
execution_guidance_text()'s replace() calls (
|
||
|
|
8731bb91f5 |
refactor(gateway): move the supervised-restart handback into gateway_supervised_restart.py
The handback logic was appended to the hermes_cli/gateway.py facade; it now lives in a topical sibling. Supervisor detection also reads the gateway's own declaration (control socket `identify` -> supervisor: "external", then the live argv marker, then the argv the gateway stamped into gateway_state.json) so a gateway whose command line cannot be read via psutil is still handed back rather than SIGTERMed and shadowed by a foreground run. Tests trimmed to the two invariants (handback with fresh-PID success; either failure branch never takes ownership) plus the plain-manual control. Docs: `hermes gateway restart` is now part of the --external-supervisor contract. |
||
|
|
251ab05000 |
feat(skills): add auteur optional skill — cinematic web design with executable anti-slop gates
Port of agiwhitelist/auteur (MIT, ~1k stars), snapshot 9bca227d. Three registers (build / direct / system) on one taste core: commit-sheet-first art direction, asset generation via image_generate + local CLIs, and node-based quality gates (slopscan anti-slop linter, motionqa frame-drop check, systemscan cross-route drift) run through playwright. - optional-skills/creative/auteur: SKILL.md (de-Clauded, Hermes tool framing), LICENSE (upstream MIT), 11 references, 8 verbatim upstream .mjs scripts (all pass node --check; slopscan smoke-run verified), 6 templates. README gallery assets not vendored (size cap). - tests/skills/test_auteur_skill.py: frontmatter, path-annotation invariant, de-Claude residue, related_skills resolution. - Docs: catalog row, sidebar entry, generated skill page (scoped regen). |
||
|
|
288fdc1a4c |
fix(auth): accept a non-production Portal's own inference host when the operator selected it
A token minted by a non-production Portal is meant to be spent at that environment's own inference gateway, and the Portal's refresh response names that host. The allowlist applied to Portal-returned inference URLs was production-only, so the value was refused as "not in allowlist" and healed to the production host — a token the production Portal never issued, sent to the production gateway, which 401s it. Every hosted non-production instance hit this on every gateway turn once #108319 made the deploy-wide NOUS_INFERENCE_BASE_URL invisible inside a routed profile scope (by design, #65941). The widening is keyed on the operator's trusted HERMES_PORTAL_BASE_URL override, never on the stored portal_base_url: when that override names a Portal outside the production allowlist, any https host under the Nous domain is accepted; otherwise the strict production set stands. So a poisoned auth.json cannot widen the set, a production-Portal session that finds a foreign inference URL in its state is still refused and healed, and the bearer can only ever go to a Nous-owned host. No environment is named in code. Because the override is read through the profile scope (previous commit), each multiplexed profile decides for itself. Validation: 4 invariant tests (accepted only under a non-production override; look-alike domains, dotless suffix and http still refused; stored portal alone does not widen; the decision follows the profile scope under multiplex) — the new-behaviour ones red on the previous commit. Main's existing validation tests are unchanged and green. Live receipt for the symptom and the fixed chain on a hosted instance: #111589. Based on #102863 and its rebase onto the decomposed auth_nous.py in #111589, whose portal-keyed pairing this replaces with the same behaviour and no environment literals. Co-authored-by: Ben Barclay <ben@nousresearch.com> |
||
|
|
b91ee8c72d |
docs: remove a stray conflict marker from the optional-skills catalog
|
||
|
|
6fcd011c01 |
Inspired by ChatGPT Work: keep imported agent setups in sync (hermes import-agent --sync)
ChatGPT Work's desktop import (Settings > Import, Aug 11 2026 release) keeps setup imported from Claude Code / Cursor automatically up to date. This ports the idea to `hermes import-agent`: - Every successful import registers its source + a content digest of everything the importer read in HERMES_HOME/import-sync.json. - `hermes import-agent --sync` re-imports every registered source whose files changed since the last run (digest compare; unchanged = no-op). Prompt-free and cron-friendly; `--sync --dry-run` previews. - Skills previously imported by import-agent are refreshed in place on sync; user-created skills under the import category keep conflict semantics and are never clobbered. - Credential files never affect the digest, so token refreshes cannot trigger (or leak into) a sync. Tests: 13 new tests in tests/hermes_cli/test_agent_import.py (61 total passing), including a sabotage-verified in-place-refresh test; E2E run against a temp HERMES_HOME exercised register -> no-op sync -> changed sync through the real command path. |
||
|
|
e819846b10 |
Inspired by Amp: relative time bounds (7d/24h/2w) + wrapper forwarding for session_search after/before
Amp's thread feed supports relative time filters (`after:7d`, `updated_before:7d`) alongside ISO dates. Extend the salvaged after/before bounds (PR #86067 by @Moodtuner997) the same way: - `_parse_iso_bound()` now accepts relative durations `Nh`/`Nd`/`Nw` (case-insensitive) meaning "now minus N", alongside ISO dates/datetimes. Clearer error message names both accepted forms. - Forward after/before/exclude_session_ids through the public `session_search()` wrapper (the PR predates the wrapper/impl split; without this the SQL bounds were unreachable from the registry handler — same class as the earlier `detail` forwarding fix). Appended after `detail` to preserve positional compatibility. - Tool schema descriptions teach both forms. - Tests: relative after/before against the discovery shape, unit checks for h/d/w math, case-insensitivity, and bad-unit rejection. - Docs: tools-reference row mentions time bounds + exclude_session_ids. |
||
|
|
4613f895ab |
test: give the rewrite-hint fixtures a read baseline; align docs row with schema
The stale-write guard now refuses write_file on an existing file the task never read in full, so test_write_file_rewrite_hint's overwrite-without-read fixtures were refused before the hint could be computed. Reading first is the exact read->whole-file-rewrite pattern the hint exists for. tools-reference.md's write_file row now mirrors the WRITE_FILE_SCHEMA description (one-sentence contract + the recovery step) instead of a longer paraphrase. |
||
|
|
6569651b87 |
fix: harden salvage of #65605 — redaction-gated test, sibling test baseline, docs
- test_file_staleness redacted-read case now force-enables redaction (matches tests/agent/test_redact.py convention) so it exercises the sentinel path in hermetic CI where security.redact_secrets is unset. - test_write_verification CRLF case establishes a read baseline first (the new guard refuses unread existing-file overwrites by design). - tools-reference.md documents the read-before-overwrite contract. - contributors/emails mapping for DanSpicyTaco. |
||
|
|
1657a1ce2d |
feat(cli): add --format stream-json for structured JSONL output
Adds a --format flag to hermes chat single-query mode. stream-json emits newline-delimited JSON events (init, text, tool_use, tool_result, result envelope with token stats + exit code) to stdout for CI pipelines and external tooling. Session ID stays on stderr. Salvaged from PR #12278 by @ProDrifterDK onto current main, including the follow-up commit enforcing the single-query contract (implies quiet, rejects --tui, emits a final result record with exit code 130 on interrupt). |
||
|
|
081421d838 |
fix: keep the write-side outcome-uncertain verdict when no server can reconnect
_handle_session_expired_and_retry only reached the at-most-once guard when a reconnectable server record existed; without one (server torn down, MCP loop not running) a write-capable call fell through to the generic "MCP call failed" error, which invites the model to replay a write that may already have landed. The session-expired classification now runs first and a write-capable call always gets the outcome_uncertain error; the reconnect is attempted only when a server can be signalled. _track_inflight_rpc's teardown RuntimeError said "retry the request on the rebuilt session" for every op; for a write-capable tools/call it now says the request may already have been dispatched and must be verified first, so the wording matches the at-most-once contract the recoverer enforces. Docs: the readOnlyHint row explains that the same hint gates auto-retry after a mid-call session expiry, and that unannotated tools on an idle-TTL Streamable-HTTP server return outcome_uncertain on the first call after idle instead of being transparently replayed. |
||
|
|
87ce653d1d |
feat(relay): migrate legacy HERMES_NEMO_RELAY_ATIF_*/ATOF_* vars into a validated relay-plugins.toml
|
||
|
|
e860b8e4e4 |
fix(context): compute-host /context and session.context_breakdown carry the per-file manifest; report blocked files
Why: the tui_gateway live formatter (`_format_live_context_output`, used when the session runs on a compute host) renders its own summary and never got the "Context files" block, and `session.context_breakdown` had no structured rows, so Desktop's popover could not show them. The formatter now appends render_context_file_lines() with the session cwd bound (the RPC thread has no session context, so the discovery walk would key on the backend's cwd), and the RPC payload gains a `context_files` list (contract + generated TS/OpenRPC + Desktop type). The docs sentence is scoped to the surfaces that render it. A file whose content _scan_context_content replaces with a BLOCKED marker was reported "loaded"; the manifest now runs the same scan and reports `blocked`. The module docstring names the frontmatter-strip / chain-cap approximations and drops the product-name attribution (credit stays in the PR body). |
||
|
|
f271ba09b0 |
fix(context): derive the /context file listing from the builder's own discovery walk
Review follow-up (Enough1122) on the salvaged #91272: the original list_context_file_sources() hand-mirrored the priority ladder inside build_context_files_prompt, so the two would drift the moment the builder gained a context type or changed precedence — misreporting what the prompt holds is worse than not showing it. Now prompt_builder exposes one candidate finder per context type (_CONTEXT_FILE_CANDIDATES → discover_context_files) and BOTH the loaders and the manifest walk it. The manifest lives in the new sibling agent/context_file_sources.py (not appended to the facade) and: - reports empty / unreadable files truthfully instead of "✓ 0 tokens", - mirrors the install-tree guard ("suppressed") so a Desktop session that fell back into the Hermes tree sees why nothing loaded, - lists every .cursor/rules/*.mdc as loaded, matching the builder which concatenates all of them, - measures truncation on the rendered "## label" section like the builder. The block now renders on every surface that shows the /context category table: CLI/TUI (hermes_cli/cli_info_mixin.py) and the messaging gateway (gateway/slash_commands_status.py). The Desktop popover consumes the raw session.context_breakdown payload (no text table) and is left as-is. Tests trimmed to the two invariants: manifest/prompt parity across every context type at once, and truncated/suppressed follow the builder. |
||
|
|
5349aa609d |
Inspired by Copilot CLI: /context now lists each context file with load status and token cost
Copilot CLI 1.0.81-6 shows each user instruction file separately in /instructions. Hermes loaded AGENTS.md/.hermes.md/CLAUDE.md/.cursorrules/ SOUL.md through a priority ladder but gave the user no visibility into WHICH files were discovered, which one won, which were shadowed, or how much context each costs — the /context 'rules' category was one opaque number. - agent/prompt_builder.py: list_context_file_sources() — read-only manifest mirroring build_context_files_prompt discovery (priority ladder, AGENTS.md directory chain with AGENTS.override.md precedence, cwd-only CLAUDE.md/.cursorrules, SOUL.md from profile home) with per-file chars, est_tokens, and loaded/truncated/shadowed status - cli.py /context: 'Context files' section rendering the manifest with status glyphs and shadowing/truncation notes; zero prompt/cache impact - docs: reference/slash-commands.md /context row - tests/agent/test_context_file_sources.py: 11 tests incl. E2E parity with build_context_files_prompt shadowing |
||
|
|
788601358d |
refactor(skills): live-dashboard becomes an optional skill reconfigured for the Desktop app
Why: the web dashboard is being deprecated in favour of the Electron Desktop app, and a skill that ships a bespoke cron blueprint should not be bundled by default. Reconfigure instead of just rebasing: - Move skills/productivity/live-dashboard -> optional-skills/productivity/ live-dashboard (install with `hermes skills install official/productivity/live-dashboard`); register it like every other optional skill: per-skill docs page under user-guide/skills/optional/, optional-skills-catalog row, sidebars entry. - Desktop reality: add a "Show the dashboard" step — when `desktop_preview` is in the toolset (Desktop/GUI sessions) render index.html in the in-app preview pane after every build/tick and on request; otherwise report the absolute file path. Prerequisites section added (Enough1122 review). - Drop the hard-wired cron/blueprint_catalog.py entry: the curated catalog is for bundled skills and would preload a skill that may not be installed. Use the skills-pipeline blueprint instead — `metadata.hermes.blueprint` on the SKILL.md registers a daily all-dashboards sweep as a /suggestions entry at install time (opt-in, never auto-scheduled), which is exactly the mechanism main provides for optional skills. - Never hardcode ~/.hermes in prose the agent executes: refer to the Hermes home directory's dashboards/<slug>/ and write absolute paths into cron prompts (also answers the review's "tick prompt must name the state-file path" point). - Tests follow the skill to optional-skills/, the catalog-blueprint tests are replaced by one parse_blueprint/blueprint_to_job_spec invariant and one desktop_preview-with-path-fallback invariant. |
||
|
|
931387dd42 |
test(cron): two invariant ESTOP tests for the fire webhook and misfire backstop; docs
Trim the salvaged suite from four tests to the two invariants that were red on main: (1) with the sentinel engaged POST /api/cron/fire answers 503 + Retry-After 60 and never calls claim_fire, and the same job is admitted (202, claimed, fired) once the sentinel is removed; (2) fire_overdue_jobs dispatches nothing and leaves next_run_at untouched while engaged, and the first sweep after resume catches the job up through claim_fire. The webhook test lives beside the other cron-fire webhook tests (test_cron_fire_webhook.py) and uses their real spy provider instead of a MagicMock resolver; the "verifier crashes -> 401" case was already covered there. Docs: cron.md gains a "Pausing everything: hermes pause" section stating that all three automated doors honour pause, that in-flight runs are never killed, and that manual runs are an operator override; the CLI reference table lists hermes pause / hermes resume. |
||
|
|
cf35e7351e |
fix(connectors): manage_connections is absent for accounts the portal has not enabled (#111238)
A signed-in, paid Nous account that the portal had not enabled for connectors got `manage_connections` in its schema and a raw "tool gateway request failed with status 404" back from every call. The gateway answers 404 for any such account by design, and Hermes gated the tool on paid access or a free tool pool, which says nothing about that. The gate now reads the portal's own answer: a `managed_tools` token claim, plus the existing free-tier leg. The gate is also the tool's check_fn, so a session without the claim never sees the tool and the model has no 404 to narrate. A token without the claim reads as not enabled. |
||
|
|
ee2f5629b8 |
Desktop connect runs on the connection operation: one card, no link to the model, no renderer polling (NS-868) (#110574)
* refactor(connectors): cut comments that restate the code
Connector modules (tools/connectors, tui_gateway connector RPCs, desktop
connector card/store) keep only comments that carry a non-derivable why or
a cross-module contract. No behaviour change.
* feat(connectors): managed connect runs on the connection operation
Managed `connect` / `reconnect` mint one ConnectionOperation for every target and, on a
desktop session, block the tool turn until the operation settles; the result is per-target
outcomes and never carries a connect link. Off the desktop the result carries the links and
returns at once (PR3 delivers them as their own message).
Why: the previous leg handed the model a URL and a `wait` verb, and the renderer ran its own
2s poller on top of the backend's 5s one; both walked the whole gateway catalog at two vendor
calls per page to read one row (~3 Composio calls/s per pending target). A hidden composer
message started the model's `wait` on the user's behalf. None of it was observable from the
operation the MCP leg already used.
What the operation looks like now:
- `contract.py`: TargetState / Actor / SettleReason enums and the `(kind, from) -> {to: actor}`
transition table. `operation.transition()` enforces it; a card cannot claim a managed
target `connected`, only the backend watcher can.
- `live.py`: one open operation per session, found by `op_id`. `connectors.operation.status`
reads it, `connection.respond` drives it, `pending_connection` on resume replays it.
- `run.py`: the one lifecycle for both target kinds (prepare -> card -> wake/observe loop ->
settle -> result). The managed `observe` hook polls the gateway list once per tick for the
whole operation; the exact-status route replaces that call when the gateway ships it.
- `connection.update` is emitted on every transition and on settlement; registered in the
shared event contract with the operation vocabulary typed on the TS side.
- `wait`, `_rendered_links`, `_seen_instructions`, the just-minted bounce and `_clamp_timeout`
are deleted. `force` on `reconnect` always reinitiates; plain `reconnect` repairs only what
the gateway reports disconnected.
- `connections.wait_timeout_seconds` is removed from config defaults, the example and the
docs. The deadline is `OPERATION_DEADLINE_SECONDS = 300` in `operation.py`; the key was
added on this unmerged train so no migration is needed.
- Wire model: `statusReason` parsed on connection results; the seven-state `connectionStatus`
is typed on list items and an unknown value fails validation; `CONNECTION_REQUIRED` carries
`connect_card_available` instead of the link when the session platform is `desktop`.
Session platform, not callback presence, decides whether a card exists: the GUI bridge
attaches callbacks to every backend session, terminal TUI included.
* feat(desktop): connector card subscribes to the connection operation
The card renders from the backend's operation instead of driving its own: `connector-flow.ts`
(the renderer's 2s `connectors.list` poller, its 120s client deadline and `keepWaiting`) is
deleted, and both hidden composer submits in `connector-tool.tsx` go with it. The model is
never nudged into a `wait`; the tool call is blocked on the backend until the operation
settles.
- `connection-request.ts` is the operation store: keyed by `op_id`, one entry per session,
`applyOperationStatus` / `applyConnectionUpdate` as pure reducers, `respond` leaves the
entry in place (the backend answers with `connection.update`), `ConnectionTargetOutcome`
is a discriminated union the backend's transition table accepts.
- `input-requests.ts` applies `connection.update`; `connection.expire` and the resume
snapshot correlate by `op_id` (a snapshot has no `request_id`).
- `ConnectorOffer` renders one `ConnectorCard` per target from a single
`Record<ConnectionTargetState, phase>` table; Connect opens the stored link, Try again on
failed / expired reissues through `connectors.connect` on the open operation, Not now is a
per-target `skipped`, Continue settles. A settled operation renders `ConnectorSummary` rows
with no live control.
- `tool-render-class.ts`: `manage_connections` renders the card regardless of
`HERMES_GUEST_ONBOARDING`; the flag still gates the onboarding flow, not the card. The
backend gate already decided admission; a card only exists because the tool was admitted.
- `mcp-setup-tool.tsx` speaks the same outcome vocabulary (connected / skipped / failed).
- `ConnectorRow.connectionStatus` is the seven-state literal union, not `string | null`.
- The guided-onboarding poller (`first-build-connectors.ts`) keeps its own row/phase types
and compiles unchanged; PR3 moves it onto the operation.
anti-slop: no net-new findings (17 touched files vs 11d1a12472).
* fix(connectors): the card never parks the tool thread; every update carries the snapshot
Found by the pre-PR adversarial review and a real-path E2E test (both left in the tree).
- The desktop `connection_callback` was still `_block("connection.request", ...)`, which parked
the tool thread on a private request-id Event until a `_respond` that no longer exists for
this event. `connection.respond` settled the operation but the tool waited its full deadline
before the watcher loop even started. The callback now only emits the card; the operation's
own wake loop is the wait. The MCP leg's blocking bridge goes with it: the card answers
through `connection.respond` like every other card.
- `connection.request` and every `connection.update` frame carry the full target snapshot
(state, link, detail). The initial mint happened before the card existed, so the renderer
never saw the links and Connect stayed disabled; a Continue settlement stamped
`not_connected` on the backend while the card still showed `initiated`. The store now
overlays the snapshot; no state is reconstructed from deltas.
- The `connection.update` emitter is a class-level `on_change` slot on the operation, set
once by `register()` (a second `register()` no longer stacks wrappers); session lookup takes
`_sessions_lock`; a re-minted link on an `initiated` target goes through `refresh_link()`
and emits, instead of a bare attribute write.
- `session.interrupt` is checked before the first observe, so an interrupted call settles
`interrupt`, not `all_resolved`.
- A gateway list reporting `expired` for an initiated target is recorded with actor `clock`
(the contract's owner of that edge); it raised `IllegalTransition` before.
- Dead `keepWaiting` i18n keys from the deleted renderer poller removed.
tests/tui_gateway/test_connector_operation_e2e.py runs the desktop lifecycle through the real
tool, registry, gateway RPC handlers and callback bridge with only the HTTP client faked.
* docs(connectors): prompts and docs describe the operation, not the deleted wait verb
The onboarding prompts told the model to call action="wait" with timeout_seconds and to
expect a hidden [setup]/[connectors] note; both are gone. tool-search.md and
toolsets-reference.md said the model gets a connect link on the desktop. tui_gateway/AGENTS.md
gains the connection-operation row of the surface table.
* fix(connectors): the panel re-mints only a dead link
Try again on a failed or expired target mints a fresh link on the open operation. A waiting
target keeps the link it was minted with; the card reopens it and connectors.connect refuses
to spend a second mint (LINK_STILL_VALID). The unused refresh_link() goes. The package
docstring names the new siblings; the nine-name public surface is unchanged.
* test(connectors): the local-batch test answers the operation the way the card does
The callback stopped returning an answer in f782b26d98 (the card answers through
connection.respond); this test still returned one and waited out the 300s deadline in CI.
* ci: retrigger
* fix(connectors): the desktop card appears outside guided onboarding
Live on a signed-in macOS desktop, the two-app connect never showed a card. Three
defects, each hidden by a test that bound state the running app never binds.
The backend read the surface from HERMES_SESSION_PLATFORM only. The desktop and TUI
gateway bind it as HERMES_SESSION_SOURCE (_set_session_context), so session_platform()
was "" and managed connects took the off-desktop branch: links in the model's message,
no operation. session_platform() now reads platform, then source. The E2E test binds
through server._set_session_context instead of set_session_vars(platform="desktop").
The renderer routed manage_connections to the card only under isOnboardingEnabled(),
the HERMES_GUEST_ONBOARDING launch flag, in message-parts.tsx and the run splitter in
fallback.tsx. tool-render-class.ts had already dropped that gate in this PR; the two
routers had not. Both now route on the tool name alone.
ConnectorTool resolved the session owner by the runtime id. Owner routes, hints and
session rows are keyed by the stored id, so in registry topology the owner never
resolved and the card rendered null while the tool blocked. It now resolves by the
stored id, matching the PR1.5 card and every other owner lookup.
message-parts-connectors.test.tsx mounts the real Fallback router with the onboarding
flag off and distinct runtime/stored ids; red before each renderer fix, green after.
* style(connectors): shorter comments, no module mock in the card router test
The router test mocked isOnboardingEnabled to false; jsdom has no preload bridge, so the
real function already returns false. Comments that restated the code are cut to one line.
anti-slop: no net-new findings (25 touched files)
* fix(connectors): Connect on a waiting row opens the stored link
ConnectorCard derived the button's loading state from the phase label, so a managed row that
read "Finish connecting in your browser" (every row, since links are minted up front) had a
disabled Connect button. Nothing on the desktop could open the sign-in link; every managed
connect ended skipped, not_connected, or at the deadline.
The card now takes `busy` for "the action itself is running" and keeps `phase` as a label.
The MCP card passes its in-flight flag; the connector card passes the re-mint wait. Red before:
the Connect button on an initiated row rendered disabled and a click opened nothing.
* fix(connectors): a settled card stays dead; the card binds to its tool call only
A second connect for the same apps revived the finished card on the old tool row. The
connection.request payload carried no id, so the renderer fell back to matching rows by
connector names, and any row with those names qualified, settled or not.
The operation now records the model's tool_call_id and sends it in connection.request and in
the resume snapshot. The card binds to the tool row with that id and to nothing else; the
name-match fallback is deleted. A payload without the id is rejected by the store.
`reason` is removed from the tool: it was the only text the card ever showed from the model
and its absence forked a second tool part, since `reason` doubled as the row-correlation key
in tool-parts.ts. The card never needed it.
`connection.expire` is deleted from the contract and from _EXPIRING_REQUESTS: the card is
raised with _emit, not _block, so nothing has emitted it since the operation lifecycle landed.
Sid's rule of record: a resolved card is fully dead; no path brings it back.
* fix(connectors): the watch loop settles once, on time, and never raises into the result
Three findings from the live review, one loop.
Continue racing a finished sign-in: the loop ran the gateway read, then settled. A read that
returned `connected` for an already-settled or failed target raised IllegalTransition out of
the tool and the model got a generic error instead of the per-app outcomes. The read now skips
targets that are not live (pending, initiated) and skips a settled operation; the loop checks
`settled` after every read.
Settle reason as row text: `settle()` wrote `continue`/`deadline` into each unresolved target's
`detail`, and the card printed it in red. The reason stays on the operation only.
Stop and the deadline waited for the next tick: `/stop` sets a per-thread flag with no wake
hook, so the sleep is sliced at 250 ms and the flag and clock are read each slice. The clock is
also checked before each read, not only after.
Tests: a failed mint that later reads connected settles cleanly; Continue during a read keeps
the settled result; no reason in detail; an interrupt settles within the same second.
* fix(connectors): MCP setup off the desktop returns unavailable instead of blocking
run_mcp_operation treated a non-None connection_callback as "a card exists". Every tui_gateway
session has that callback, the Ink TUI included, so an MCP install from the terminal UI blocked
until the 300 s deadline while the docs promised `unavailable` with the terminal commands.
The MCP path now reads the session surface the same way the managed path does; the callback is
never the predicate. Test binds the surface to `tui` with the callback attached.
* fix(connectors): a failed Try again shows the failure, not the old dead link
The panel's re-mint ignored the gateway's per-app status and moved the row to `initiated` with
whatever link came back, `None` included, so a mint that failed again rendered as waiting on the
link that had already died.
One reader of a mint response now serves both the first mint and Try again
(`managed.mint`, with the actor as a parameter). A repeated failure keeps the row `failed`,
drops the link, and carries the vendor's new text through `operation.refresh`, which emits a
frame without a state change so the card redraws.
* fix(connectors): a forced reconnect waits for the new sign-in before it reports connected
`reconnect` with `force: true` is the account switch. The vendor keeps the old account active
while the new link waits, so the first list read after the mint said `connected` and the
operation settled at once: the new link was dropped and the model was told the switch was done.
A forced target is marked awaiting_new_attempt after the mint. The watcher ignores its row until
the list shows the new attempt (`connectionStatus: initiated`) once, then trusts `connected`.
* fix(connectors): the operation registers under the gateway session key
The tool registered the operation under the agent's session_id; every RPC (connection.respond,
connectors.operation.status, the panel's connectors.connect) and the update emitter looked it up
by the gateway's session key. Those agree until compaction rotates the agent id mid-turn; then
the card's clicks find nothing, no update reaches it, and the tool waits out the deadline.
The registration key is now the bound HERMES_SESSION_KEY, with the agent id as the fallback for
callers with no gateway (unit tests, a bare CLI). The E2E passes a rotated agent id and drives
the card by the gateway key.
* fix(connectors): the forced-reconnect gate reads any non-active row; a failed re-mint of an expired row is failed
Three follow-ups from the verification of the fix pass.
The awaiting_new_attempt gate cleared only on the literal `connectionStatus: initiated`. The
field is optional on the wire and `initializing`, `failed`, `expired` are valid values, so a
forced reconnect could wait the full 300 s and swallow a failed new attempt. The gate now holds
only while the row still reads as the old account (`connected` or `active`) and releases on
anything else.
Try again on an `expired` row whose re-mint fails raised IllegalTransition (no expired → failed
edge). The re-mint steps through `initiated` as the user's attempt, then `failed`, then drops the
dead link.
`detail` never carries a state name any more: `failed` as detail rendered as the row label and
made agent/display.py tag the settled result as a tool error. Only vendor text goes there.
`connection.expire` removed from the renderer's unscoped-stream set; nothing emits it.
|
||
|
|
e0ef0eb9c3 |
manage_connections covers local MCP servers; setup_mcp leaves the schema (NS-867, PR1) (#109517)
* feat(connections): manage_connections covers local MCP servers; setup_mcp leaves the schema
One model tool now connects the user to apps of both kinds. A target
`{"name": "linear", "mcp": true}` is a locally configured MCP server;
`install` / `enable` / `authorize` are its verbs. Bare strings and
`{"name": ...}` stay managed connectors and that leg is unchanged.
MCP targets run through one backend-owned connection operation
(tools/connections_tool_operation.py): created with a server-side
deadline from the new config key `connections.wait_timeout_seconds`
(default 120, floor 5, no ceiling), per-target state, and exactly-once
settlement (all resolved / Continue / deadline / interrupt). Unresolved
targets freeze as `not_connected` with the settle reason.
Why the fold works now: the approval card is reached through
`agent.connection_callback` via the agent-level inline executor table,
which is the only path that carries a GUI callback. Registry dispatch
(every non-GUI surface) settles MCP targets as `unavailable` with the
`hermes mcp install / login` hint; managed targets in the same call
are unaffected.
`setup_mcp` is removed from every advertised toolset and from the
deferral list; an inline-table shim keeps calls from conversations
opened before this change dispatching (prompt-cache protection).
`_LEGACY_TOOL_ALIASES` is not the mechanism: inline tools bypass it.
Gateway: `mcp.setup.request/respond` are replaced by
`connection.request/respond/expire` (no wire compat; desktop ships
with this). The bridge waits exactly the operation's deadline. The
`session.resume` snapshot gains `pending_connection` so a reopened
window restores the card with the original deadline.
`manage_connections` joins `_SEQUENTIAL_DEADLINE_EXEMPT_TOOLS`: the
operation owns its wait; the 420s guard must not report `tool_timeout`
while the card is live.
The portal `check_fn` on the tool is dropped in favour of a
handler-level gate on the managed leg, so signed-out sessions can still
approve local MCPs.
* wip(desktop): connection.request store, resume restore, card routing for MCP targets
Renderer half of the setup_mcp fold, first slice: connection-request store
(mirrors clarify), connection.request/expire handling, pending_connection
resume restore, mcpTargets() + isCardTool(name, args) so MCP-target
manage_connections calls classify as cards. Not yet: the card component
rewrite (mcp-setup-tool.tsx), mcp-directory.ts removal, vitest, docs.
Does not typecheck until the card rewrite lands.
* fix(config): hermes update turns on the connections toolset for saved toolset lists
`hermes tools` writes an explicit `platform_toolsets.<platform>` list, and the
resolver reads absence from that list as "unchecked". The `connections`
toolset (#106842) shipped after most users last saved, so `manage_connections`
is stripped from the schema on every install that ever opened the picker.
The Nous entitlement gate never runs; the agent reports the tool as missing.
Migration 44 -> 45 (renumbered when folded into #109517; main was already at 44) appends `connections` to each explicit per-platform list
that lacks it and records the offer in `known_builtin_toolsets` where that
record exists, so a later uncheck reads as a decline. It skips: platforms
whose record already holds `connections` (the user saw the checkbox and left
it off), bare composite lists ([hermes-cli]) that already inherit it, platforms
where the toolset is not allowed, and any config whose `agent.disabled_toolsets`
names `connections` (Blank Slate, `hermes tools --disable`), because the
resolver subtracts that list last and the enable would never take effect.
The explicit-list test is the resolver's own: any configurable or plugin key.
`hermes update` runs migrations post-pull for the active profile and every
sibling, so one update is enough. Fresh installs and composite users were
never affected.
* refactor: anti-slop pass on the desktop slice; shorten added comments
Parse connection.request at the boundary with a typed wire interface instead of
unknown + typeof; mcpTargets reuses connectorText; comments cut to one or two
lines. slop-ratchet: no net-new findings in 13 touched files.
* feat(desktop): the MCP approval card answers manage_connections; MCP Directory removed
The existing card (mcp-setup-tool.tsx) now reads the connection-request store,
renders for manage_connections calls with mcp:true targets, answers through
connection.respond with a per-target outcome, and no longer calls reload.mcp
after Install; the new server's tools arrive on the between-turns refresh.
A settled operation renders the first target's frozen state.
session.resume restores a pending card with its original deadline on both the
activate and cold-resume paths.
lib/mcp-directory.ts is deleted along with its two fallback branches
(suggestion provider, card install). The catalog was already primary in both;
a catalog miss now yields no suggestion / a notInCatalog error. The GitHub
never-suggest test is rewritten on catalog-shaped data.
vitest: connection-request store (6), suggestion provider, clarify restore.
slop-ratchet: no net-new findings in 19 touched files.
* chore: drop __pycache__ files swept in by an over-broad git add
* fix(desktop): correlate the connection.request row with the model's tool call by reason
The synthetic row from connection.request and the tool.start row carried
different ids and no shared match value (op_id is not in the model's args),
so the card mounted twice. reason is the arg both sides carry.
* docs: manage_connections covers local MCP servers; connections.wait_timeout_seconds
* fix(connections): settle reason derives from target state, never from the renderer
A card that answers one of two targets and claims all_resolved must settle as
continue with the other target not_connected; found live with a two-target call.
* fix(desktop): a pending connection card re-arms on resume and activate
The store entry was restored but the transcript row was not, so navigating
away and back (or reloading) lost the card while the backend kept waiting.
restorePendingClarifyToolCall's core is generalized to any blocking tool
name and both resume paths project the connection row through it.
Verified live: card restored after navigate-away and after a full renderer
reload, deadline_at unchanged, approve settles connected.
* style: literal wording in added comments, docstrings and docs
* fix: shared gateway-event contract and config-schema category for the connection events
connection.request/expire replace mcp.setup.* in apps/shared gateway-events
(json list, BACKEND_EVENT_NAMES, GatewayEventMap) so the renderer's event
union includes them and the tui_gateway contract test passes. The new
`connections` config section folds into the agent tab like the other
single-field sections.
* style: import order (perfectionist) in the desktop and shared files this PR touches
* chore: retrigger CI (zero-job dispatch failure, auto-heal)
|
||
|
|
a79ff58d65 |
feat(skills): ai-presenter-video optional skill (port of lanshu, 955★ MIT)
Ports cclank/lanshu-create-ai-presenter-video (MIT, 955 stars in 7 days) into optional-skills/creative/ai-presenter-video. Provider-neutral presenter-video production: locked narration as master clock, avatar generation with pilot-first cost discipline, lip-sync/identity QA, captions, deterministic ffmpeg finalization with loudness normalization and contact-sheet verification. Hermes adaptations in the hub SKILL.md: SKILL_DIR resolution (upstream hardcoded ~/.codex/skills), capability mapping to text_to_speech / FAL video families / vision_analyze / hyperframes, consent-flag JSON paths (input.* vs root), preflight error-vs-remote-blocker semantics. References kept substantively verbatim (all-English upstream). Scripts unmodified. LICENSE carried. Validated hands-on: init_job -> preflight gating (blocked until manual review booleans + input.remote_upload_approved) -> finalize_delivery on a synthetic 1080x1920 render (master+share decode-verified, delivery report, 9-frame contact sheet). Cold-subagent live test: SHIP; 3 friction fixes folded in (resolution guard, boolean-flip example, preflight-writes-job note). |
||
|
|
56cc2bd814 |
feat(skills): scrollcraft — premium scroll-driven landing pages (port of nateherkai/scroll-craft, 1.2k★ MIT)
Optional skill: scroll-as-timeline landing pages on a deterministic CSS/JS engine, with interview → page grammar → signature move workflow and screenshot-based scroll verification. Engine and scripts vendored verbatim; asset generation re-anchored on image_generate with the upstream kie.ai flow kept as an optional path. |
||
|
|
3304d205be |
feat(video-gen): Kling 3.0 Standard + Pro families on the FAL backend
Adds kling-v3 (fal-ai/kling-video/v3/standard/*) and kling-v3-pro (fal-ai/kling-video/v3/pro/*) to FAL_FAMILIES: start_image_url i2v key, aspect_ratio dropped on i2v, string duration 3-15s, generate_audio and negative_prompt real, no seed/resolution keys per the published llms.txt schemas. Payload shapes pinned in tests; docs mention updated. |
||
|
|
ce318290bd |
Inspired by Factory Droid: /queue prompts are now listable, editable, and reorderable before they run
Droid v0.203 (Aug 25 2026) added 'edit queued messages' — a queued steering message can be pulled back and changed before it is sent. Hermes /queue could only append blindly: no way to see, fix, drop, or reorder queued prompts. /queue now supports management subcommands in the CLI: - /queue — list pending prompts (bare prompt still enqueues) - /queue list — same - /queue edit N <p> — replace item N (keeps voice sentinel, #65827) - /queue rm N — remove item N - /queue move A B — reorder - /queue clear — drop everything - /queue add <p> — force-enqueue prompts starting with a management word Queue mutations hold queue.Queue's mutex and rebuild unfinished_tasks so join()/task_done bookkeeping stays consistent. Paste references expand on enqueue and edit, matching the old inline path. Reimplementation of PR #18833 by @abhinav11082001-stack (commit was authored under a fabricated 'Hermes Agent' noreply identity that cannot be carried into history; engineering credit is theirs), hardened for current main: voice-sentinel-aware previews/edit, paste-reference expansion, queue bookkeeping asserts, out-of-range no-op tests, and docs. |
||
|
|
48bd70b586 |
Port from nearai/ironclaw#7378: doc-fact contract test keeps slash-commands.md in sync with the command registry
Two-direction contract test (tests/website/test_slash_commands_doc_parity.py): every CommandDef must be documented under its name or an alias, and every doc table row must resolve to a registered command. Ported from IronClaw's doc-fact contract tests (nearai/ironclaw#7378), adapted from their clap --help parser to our COMMAND_REGISTRY single source of truth. Real drift it caught, fixed here: /loop (alias /proactive) shipped with a full feature page (user-guide/features/loops.md) and CLI+gateway handlers but never got a row in the slash-commands reference. Added to both the CLI Session table and the messaging table, plus the both-surfaces note. |
||
|
|
2c0bec33f9 |
feat(model-pickers): reasoning effort selection on every model picker
The Desktop composer got a reasoning-effort pill this morning; every other place a
model is picked still left the effort to a separate command (`/reasoning`) or a
hand edit of config.yaml. `hermes model` had one effort step for Copilot only, and
its auxiliary-model menu had none at all even though every aux block already reads
`auxiliary.<task>.reasoning_effort`.
One request now carries a model pick AND its effort on every surface:
- `hermes_cli/model_switch.py`: the single `/model` parser accepts `--reasoning
<level>` (validated against `parse_reasoning_effort`; unknown level ->
`MODEL_SWITCH_ERR_BAD_REASONING`; Unicode-dash normalized like the other flags).
`ModelSwitchRequest.reasoning_effort` rides with the pick.
- Classic CLI (`cli_model_switch_mixin`, `cli_tui_mixin`): `/model X --reasoning
high` applies the effort AFTER the agent swap (`switch_model` re-resolves
`reasoning_config` from config.yaml, so an earlier write is clobbered) with the
pick's scope (session; config on `--global`; `--once` snapshots and restores it).
The `/model` picker gains a third stage, "Reasoning effort for <model>", built
from `VALID_REASONING_EFFORTS` + none + "Keep current effort"; hidden when the
inventory capability map says the route has no reasoning control.
- TUI gateway (`tui_gateway/model_switch.py`, serves Ink TUI + Desktop):
`config.set model "X --reasoning high"` applies after the swap; session pin
(`create_reasoning_override`) by default, `agent.reasoning_effort` on --global,
one-turn restore carries `reasoning_config`; re-emits `session_info` so the
status bar shows the new effort.
- Ink TUI `ModelPicker`: step 3/3 (same rows, same capability gate) emitting
`<model> --provider <slug> --reasoning <level> <scope>`; the new-session draft
label strips the flag like `--provider`.
- Messaging gateway `/model`: `--reasoning` goes through the existing
`_apply_reasoning_selection` (the `/reasoning` applier) with the pick's scope.
- `hermes model`: one shared post-pick effort step for the MAIN model (replaces
the Copilot-only inline prompt; Copilot keeps its per-model level set via
`github_model_reasoning_efforts`, other routes get the ladder, catalog
`supports_reasoning=False` skips it) plus a "Reasoning effort for the current
model..." row. The auxiliary menu's provider->model and custom-endpoint flows end
with the same step (+ "Provider default"), stored as
`auxiliary.<task>.reasoning_effort` / `delegation.reasoning_effort`, shown in
the task list ("openrouter · model · high"), cleared by "Reset all to auto";
tasks whose block omits the key by design (MoA slots, memory_query_rewrite) skip
it.
Live (temp HERMES_HOME, stub key, no model call):
- `hermes model` -> aux -> Vision -> OpenRouter -> model: before ends at
"Vision: openrouter · <m>", no key written; after adds "Select reasoning effort"
and saves `reasoning_effort: high`.
- `hermes model` -> DeepSeek -> model: before no effort step; after the step
writes `agent.reasoning_effort: xhigh`.
- tui_gateway stdio: `config.set model "... --reasoning high --session"` before
errors "Model names cannot contain spaces"; after switches and `config.get
reasoning` returns high; bad level -> the canonical error text.
- classic CLI `process_command`: before the same spaces error; after "Reasoning
effort: high" under the switch summary, `--global` writes config.
- `hermes --tui` PTY: /model -> step 1/3 -> 2/3 -> 3/3 -> high; transcript
"reasoning: high", status bar "fable 5.1 high".
|
||
|
|
2dfd831d3b |
fix(webhook): bind a subscription to a profile with --route-profile, not --profile
The salvaged flag was spelled --profile, which collides with the global -p/--profile that hermes_cli.main scans BEFORE argparse: `hermes webhook subscribe x --profile compta` would switch this CLI process to compta's HERMES_HOME and write the subscription into compta's webhook_subscriptions.json — a file the default gateway's webhook adapter never reads — while the route still lacked the profile key. #109020 special-cased the scanner for the webhook subcommand; naming the flag --route-profile removes the ambiguity without touching _scan_profile_flag: -p picks the gateway whose subscriptions file is written, --route-profile picks which /p/<profile>/ prefix may hit the route. Docs: cli-commands reference row, multi-profile-gateways webhook section, the route `profile` field. Builds on #109020 (fangliquanflq). Fixes #109016. |
||
|
|
630a4eb3a1 |
docs(mcp): mTLS credentials count toward connection sharing; OAuth token path is per profile
The multiplex guide now says client_cert/client_key are part of the "same credentials" test and states the OAuth rule as its own sentence; the MCP config reference names the per-profile token directory and the never-shared-across-profiles rule next to the OAuth behaviour list. Co-authored-by: ly6751 <99090550+ly6751@users.noreply.github.com> |
||
|
|
0c0875b746 |
chore: delete orphaned bench data, datagen examples and stale one-off docs
Nothing in the tree reads any of these; they landed with feature PRs and were never routed to their proper home. - mcp-research-data/: 224K of July tool-search bench result rows. The harnesses (scripts/tool_search_livetest_ue*.py) write their output to a gitignored dir; the rows were committed by hand once and the headline numbers already live in the bench commit messages. - datagen-config-examples/: Feb 2026 RL datagen configs for a WebResearchEnv that no longer exists; the yaml paths point at a configs/ dir that was never created. - docs/: ADR log with one entry, an implemented cron-doctor spec, an RCA for a resolved bug, two RFCs whose work shipped, an unimplemented profile-builder proposal, the kanban dialog mock HTML and the kanban v1 spec PDF. profile-routing.md duplicated the profile_routes section of website/docs/user-guide/multi-profile-gateways.md. Kanban docs and the `hermes kanban` parser description pointed readers at the PDF; those now point at the user guide (the patterns table it was citing is on that same page). |