chore: sync with upstream main (2026-05-08)#24
Conversation
- Expand migration comment to name the primary failure mode (missing column OperationalError from NousResearch#20842) ahead of the secondary SQLite schema-reparse concern; also document the stale-cols-snapshot invariant - Add clarifying comments on from_row() legacy fallback branches noting they are belt-and-suspenders dead code post-migration - Add task_events comment in existing test explaining why the table is required by the migrator - Add test_legacy_migration_no_legacy_columns_at_all: Scenario A — explicitly asserts the exact NousResearch#20842 crash no longer occurs and that consecutive_failures defaults to 0 on a DB that never had spawn_failures - Add test_legacy_migration_both_columns_already_present: Scenario D — asserts the migration is a no-op when both columns already exist, preserving the existing counter value
change: enable ruff/ty
Switch top-level concurrency to cancel-in-progress=false so every push to main gets its own SHA-tagged image published — no more discarded builds when commits land back-to-back. Guard the :latest tag with a second job that has its own concurrency group with cancel-in-progress=true plus a git-ancestor check against the revision label on the current :latest. Together these guarantee :latest only ever moves forward in history: a slower run whose commit isn't a descendant of the current :latest refuses to clobber it, and a newer push mid-way through the move-latest job preempts the older one before it can retag. - Every main push publishes nousresearch/hermes-agent:sha-<commit> with an org.opencontainers.image.revision label embedded. - move-latest job reads that label off :latest, runs merge-base --is-ancestor, and only retags (via buildx imagetools create, registry-side, no rebuild) if our commit strictly descends. - fetch-depth bumped to 1000 so merge-base has the history it needs. - Release tag flow unchanged (unique tag, no race).
…ch#20827) Previous version read like internal API docs \u2014 leading with env var tables, config YAML, and 'precedence' rules before ever explaining the product. Complete rewrite inverts the structure so readers see value first, mechanics last. Structure now: - Lede: 'One subscription. Every tool built in.' + pitch paragraph - CTA: subscribe/manage button styled as a real call-to-action - What's included: emoji-led table with expanded descriptions per tool. Image gen lists all 9 models by name (FLUX 2 Klein/Pro, Z-Image Turbo, Nano Banana Pro, GPT Image 1.5/2, Ideogram V3, Recraft V4 Pro, Qwen) - Why it's here: value bullets \u2014 one bill, one signup, one key, same quality, bring-your-own anytime - Get started: two-command flow (hermes model \u2192 hermes status) - Eligibility: paid-tier note with upgrade link - Mix and match: three realistic usage patterns - Using individual image models: ID reference table for power users - --- separator --- - Configuration reference (demoted): use_gateway flag, disabling, self-hosted gateway env vars moved below the fold where they belong - FAQ: streamlined, removed redundant content Fact-checked against code: - 9 FAL models confirmed from tools/image_generation_tool.py FAL_MODELS - Status section output verified against hermes_cli/status.py - Portal subscription URL preserved - Self-hosted env vars (TOOL_GATEWAY_DOMAIN etc.) kept accurate Verified: docusaurus build SUCCESS, page renders, no new broken links.
…n profile Profile processes (kanban workers, cron subprocesses, delegated subagents) read the profile's auth.json only. If a provider was authenticated at the global root but not inside the profile, the profile's credential_pool comes back empty and the process fails with 'No LLM provider configured' — even though the credentials are sitting in ~/.hermes/auth.json. NousResearch#18594 propagated HERMES_HOME correctly, which is what surfaced this: workers now land in the right profile, and the profile turns out to shadow global with no fallback. Semantics (read-only, per-provider shadowing): * Profile has any entries for provider X → use profile only (global ignored). * Profile has zero entries for provider X → fall back to global. * Writes (write_credential_pool, _save_auth_store) still target the profile. * Classic mode (HERMES_HOME == global root) skips the fallback entirely — _global_auth_file_path() returns None. Also mirrors the fallback in get_provider_auth_state so OAuth singletons (nous, minimax-oauth, openai-codex, spotify) inherit cleanly — the Nous shared-token store (PR NousResearch#19712) remains the authoritative path for Nous OAuth rotation, this just makes the read side consistent with it. Seat belt: _load_global_auth_store() refuses to read the real user's ~/.hermes/auth.json under PYTEST_CURRENT_TEST even when HERMES_HOME points to a profile-shaped path. Guard uses $HOME (stable across fixtures) rather than Path.home() (which fixtures often monkeypatch to a tmp root). Reported by @SeedsForbidden on Twitter as the credential_pool shadowing follow-up to the NousResearch#18594 fix.
Adds an opt-out toggle on PlatformConfig that gates both restart lifecycle pings: the "♻ Gateway restarted" message sent to the chat that issued /restart, and the "♻️ Gateway online" home-channel startup notification. Defaults to True so existing deployments are unaffected. The motivating split is operator vs. end-user surfaces: a back-channel like Telegram should keep these pings, while a Slack workspace shared with end users should not surface gateway lifecycle noise. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extend the gateway_restart_notification flag to cover
_notify_active_sessions_of_shutdown — the message that fires just
before drain ("⚠️ Gateway restarting — Your current task will be
interrupted. Send any message after restart and I'll try to resume
where you left off.") sent to active sessions and home channels.
Same operator/end-user reasoning: on a Slack workspace shared with
end users, "Gateway restarting" reads as "the bot is broken" — the
operator should be able to suppress it consistently with the other
two lifecycle pings rather than having a partial opt-out.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
For cherry-picked commits in PR NousResearch#20801.
ci(docker): don't cancel overlapping builds, guard :latest
* fix(tui): steady transcript scrollbar Keep the visible scrollbar tied to committed viewport position while virtual history can still prefetch against pending scroll targets, and preserve drag grab offset synchronously for native-feeling scrollbar drags. * fix(tui): smooth precision wheel scroll Replace the opt-scroll throttle with frame-sized coalescing so modifier wheel gestures stay line-precise without stepping.
* fix(tui): restore classic CLI voice push-to-talk parity (cherry picked from commit 93b9ae3) * fix(tui): harden voice push-to-talk stop flow Address review feedback from PR NousResearch#16189 by stopping the active recorder before background transcription, documenting single-shot voice capture, and covering the TUI gateway flags with regression tests. * fix(tui): preserve silent voice strike tracking Keep single-shot voice recording's no-speech counter alive across starts so the TUI can still emit the three-strikes auto-disable event, and bind the auto-restart state at module scope for type checking. * fix(tui): clean up voice stop failure path Address follow-up review by naming the TUI flow as single-shot push-to-talk and cancelling the recorder when forced stop cannot produce a WAV. * fix(tui): report busy voice capture starts Return explicit start state from the voice wrapper so the TUI gateway does not report recording while forced-stop transcription is still cleaning up. * fix(tui): handle busy voice record responses Apply the gateway busy status immediately in the TUI and route forced-stop voice events to the session that sent the stop request. * fix(tui): clear voice recording on null response Treat a null voice.record RPC result as a failed optimistic start so the REC badge cannot stick after gateway-side errors. * fix(tui): count silent manual voice stops Preserve single-shot voice no-speech strikes through forced stop transcription so empty push-to-talk captures still trigger the three-strikes guard. --------- Co-authored-by: Montbra <montbra@gmail.com>
… is installed The setup wizard dropped non-root users at a bare shell prompt when trying to start a system-scope gateway service. Previously _require_root_for_system_service called sys.exit(1), which the wizard's `except Exception` guards cannot catch (SystemExit is a BaseException). Users with a pre-existing /etc/systemd/system unit (e.g. from an earlier `sudo hermes setup` run) hit this whenever they re-ran `hermes setup` as a regular user. - Convert _require_root_for_system_service to raise a typed SystemScopeRequiresRootError (RuntimeError subclass) instead of sys.exit(1). The direct CLI path (`hermes gateway install|start|stop| restart|uninstall` without sudo) still exits 1 cleanly via a new catch at the top of gateway_command, matching the existing UserSystemdUnavailableError pattern. - Add _system_scope_wizard_would_need_root() pre-check and _print_system_scope_remediation() helper. Both setup wizards (hermes_cli/setup.py and hermes_cli/gateway.py::gateway_setup) now detect the dead-end before prompting and print actionable guidance: either `sudo systemctl start <service>` this time, or uninstall the system unit and install a per-user one. - Defense-in-depth: all 5 wizard prompt sites also catch SystemScopeRequiresRootError and fall back to the remediation helper if the pre-check is bypassed (race, etc.). Tests: 12 new tests in TestSystemScopeRequiresRootError, TestSystemScopeWizardPreCheck, TestSystemScopeRemediationOutput, and TestGatewayCommandCatchesSystemScopeError covering the exception contract, pre-check matrix (root vs non-root, system-only vs user-present vs none vs explicit system=True), remediation output for each action, and the direct-CLI exit-1 path.
Previously, /personality in the TUI called _reset_session_agent() which destroyed the agent, cleared conversation history, and effectively started a new session. This made personality switching disruptive — users lost their entire conversation context. Now /personality updates the agent's ephemeral_system_prompt in-place and injects a pivot marker into the conversation history. The marker tells the model to adopt the new persona from that point forward, which is necessary because LLMs tend to pattern-match their prior responses and continue the established tone without an explicit signal. Changes: - tui_gateway/server.py: Rewrite _apply_personality_to_session to update the agent in-place instead of resetting. Inject a user-role pivot marker so the model actually switches style mid-conversation. - ui-tui/src/app/slash/commands/session.ts: Update help text (no longer mentions history reset). - tests/test_tui_gateway_server.py: Update test to verify history is preserved, pivot marker is injected, and ephemeral prompt is set.
Two follow-ups on top of helix4u's slash-command sync hardening: - Only suppress exceptions that are actually Discord 429 rate limits (discord.RateLimited, HTTPException with status 429, or a clearly rate-limit-named duck type). Arbitrary failures that happen to expose a retry_after attribute now re-raise to the outer handler instead of silently swallowing a cooldown. - Move the sync-state JSON under $HERMES_HOME/gateway/ so the home root stops collecting ad-hoc runtime files. Added a test verifying unrelated exceptions don't get misclassified as rate limits.
…0960) Follow-up to NousResearch#20958. The worker skill section had the same stale 'hermes skills install devops/kanban-worker' command — kanban-worker is also bundled, so that command fails with 'Could not fetch from any source.' Replace with bundled-skill verification + restore pattern, matching the orchestrator section. Uses <your-worker-profile> placeholder since assignees vary (researcher, writer, ops, linguist, reviewer, etc.) rather than a single fixed 'worker' profile.
…arch#20986) Adds `hermes profile create <name> --no-skills` to create a profile with zero bundled skills. Writes a `.no-bundled-skills` marker file in the profile root so `hermes update`'s all-profile skill sync loop also skips the profile — without the marker, every update would re-seed skills and the user would have to delete them again. Use case (from @hiut1u): orchestrator profiles and narrow-task profiles don't need 100+ bundled skills polluting their system prompt. - create_profile() gains a `no_skills` param, mutually exclusive with `--clone` / `--clone-all` (cloning explicitly copies skills). - seed_profile_skills() no-ops on opted-out profiles and returns `{skipped_opt_out: True}` so callers can report cleanly. - Web API (POST /api/profiles) accepts `no_skills: bool`. - Delete `.no-bundled-skills` to opt back in — next `hermes update` re-seeds normally. 6 new tests in TestNoSkillsOptOut cover marker write, mutual exclusion with clone, seed_profile_skills opt-out, fresh profile unaffected, and delete-marker-re-enables-seeding.
The Dockerfile dropped the manual `@hermes/ink` materialisation gymnastics
in favour of letting npm workspaces resolve the bundled package
naturally. Two contract tests still asserted the older flow:
`test_dockerfile_installs_tui_dependencies` required:
'ui-tui/packages/hermes-ink/package-lock.json' in dockerfile_text
…but the lockfile is no longer COPIED individually \u2014 the entire
`ui-tui/packages/hermes-ink/` tree is COPIED instead (the workspace
reference from `ui-tui/package.json` is `file:` so npm needs the
real source, not just a manifest stub).
`test_dockerfile_materializes_local_tui_ink_package` required a 7-clause
conjunction matching specific `rm -rf` / `npm install --omit=dev`
`--prefix node_modules/@hermes/ink` / `rm -rf .../react` invocations
that were stripped out when the workspace resolution was simplified.
Update the assertions to pin the *contract* the image actually has to
carry rather than the *exact shell incantations* the old flow used:
* TUI deps install: ui-tui/package.json + ui-tui/package-lock.json +
ui-tui/packages/hermes-ink/ tree are all COPIED, and an npm
install/ci step runs in ui-tui.
* Bundled hermes-ink: the workspace package source is COPIED (so
`await import('@hermes/ink')` resolves at runtime).
This keeps the spirit of NousResearch#15012 / NousResearch#16690 (zombie reaping + bundled
workspace materialisation must continue to work) without locking the
Dockerfile into one specific implementation flavour.
Validation:
$ pytest tests/tools/test_dockerfile_pid1_reaping.py -q
6 passed in 1.43s
No production code change. Fixes the two failures observed on `main`
(run 25250051126):
`tests/tools/test_dockerfile_pid1_reaping.py::test_dockerfile_installs_tui_dependencies`
`tests/tools/test_dockerfile_pid1_reaping.py::test_dockerfile_materializes_local_tui_ink_package`
…ests Two CI tests for the new `--yes` update flag (NousResearch#18261) flaked under `pytest-xdist` on Linux/Python 3.11 even though they passed every local run on macOS/Python 3.14.4: FAILED tests/hermes_cli/test_update_yes_flag.py ::TestUpdateYesConfigMigration::test_no_yes_flag_still_prompts_in_tty `AssertionError: assert <MagicMock 'input'>.called is False` FAILED tests/hermes_cli/test_update_yes_flag.py ::TestUpdateYesStashRestore::test_yes_restores_stash_without_prompting `AssertionError: assert <MagicMock '_restore_stashed_changes'>.called is False` Captured stdout for the first failure shows `cmd_update` taking the "Non-interactive session \u2014 skipping config migration prompt." branch \u2014 i.e. the `sys.stdin.isatty() and sys.stdout.isatty()` check at `hermes_cli/main.py:7118` evaluated to `False` despite the test doing: with patch("hermes_cli.main.sys") as mock_sys: mock_sys.stdin.isatty.return_value = True mock_sys.stdout.isatty.return_value = True The whole-module mock is fragile under xdist worker reuse: a sibling test that imports `hermes_cli.main` first can leave another `sys` reference resolved inside the function (re-import in a helper, etc.), and the wholesale module replacement never gets consulted. Switch to `patch.object(_sys.stdin, "isatty", return_value=True)` (and the same for `stdout`). That patches the *attribute on the real stream object* \u2014 every call site, no matter how it reached `sys.stdin`, hits the patched method. Same fix applied to the stash-restore test (it took the "non-TTY \u2192 skip restore prompt" branch for the same reason). Validation: $ pytest tests/hermes_cli/test_update_yes_flag.py -q 3 passed in 5.47s No production code change. Fixes the two failures observed on `main` (run 25250051126): `tests/hermes_cli/test_update_yes_flag.py::TestUpdateYesConfigMigration::test_no_yes_flag_still_prompts_in_tty` `tests/hermes_cli/test_update_yes_flag.py::TestUpdateYesStashRestore::test_yes_restores_stash_without_prompting` Refs: NousResearch#18261 (added the `--yes` flag + these tests).
The Documentation tab embeds the public Hermes Agent docs site via an <iframe>. On any system where the browser's prefers-color-scheme resolves to dark — the default on macOS with system dark mode, and common on Linux/Windows too — the docs body text rendered nearly invisible against its own background. Cause: Docusaurus intentionally leaves <html> and <body> transparent and relies on the browser's Canvas color to fill the viewport. Inside our iframe, the iframe element had bg-background (the dashboard's dark canvas) AND inherited the dashboard's dark color-scheme, so the browser set the iframe's Canvas to a dark value. Docusaurus's transparent body exposed that dark Canvas, and the docs body text (tuned for a light Canvas) became near-illegible. Affects every built-in dashboard theme. Fix: replace bg-background on the iframe with [color-scheme:light] (spec-blessed cross-origin override of the inherited color-scheme; forces the iframe's Canvas to light) and bg-white (belt-and-suspenders fallback during the brief paint window before content loads). The docs site's own theme toggle keeps working — Docusaurus stores its choice in localStorage and applies opaque dark backgrounds to its layout elements that cover the white Canvas we forced.
_write_json (the persistence helper used by HermesTokenStorage for both tokens and client_info) created the temp file via Path.write_text and only chmod'd it to 0o600 afterward. Between create and chmod the file existed on disk at the process umask (commonly 0o644 = world-readable), briefly exposing MCP OAuth access/refresh tokens to other local users. Use os.open with O_WRONLY|O_CREAT|O_EXCL and an explicit S_IRUSR|S_IWUSR mode so the file is created atomically at 0o600, plus tighten the parent dir to 0o700 so siblings can't traverse to the creds file. The temp name also gains a per-process random suffix to avoid collisions between concurrent writers and stale leftovers from a crashed prior write. Mirrors the fix shipped for agent/google_oauth.py in NousResearch#19673. Adds a regression test asserting the resulting file mode is 0o600 and the parent directory is 0o700 (skipped on Windows where POSIX mode bits aren't enforced).
…ing (NousResearch#21455) - Add pricing entries for Claude Opus 4.5/4.6/4.7, Sonnet 4.5/4.6, and Haiku 4.5 with updated source URLs (platform.claude.com) - Add _normalize_anthropic_model_name() to handle dot-notation variants (e.g. claude-opus-4.7 → claude-opus-4-7) for pricing lookups - Fix silent token loss: ensure session row exists before UPDATE in both run_agent.py and hermes_state.py (INSERT OR IGNORE is idempotent) - Log token persistence failures at DEBUG level instead of swallowing them silently — makes undercounted analytics diagnosable - Surface reasoning tokens in CLI /usage and TUI usage panel - Add 'reasoning' and 'cost_status' fields to TUI Usage type
channels_list was iterating directory.items() directly, yielding
("updated_at", str) and ("platforms", dict) pairs — neither passed
the isinstance(entries_list, list) check, so the inner loop never ran
and every call returned count=0 even when channel_directory.json was
populated.
The writer (gateway/channel_directory.py) wraps the payload as
{"updated_at": ..., "platforms": {...}}; every other reader in the
codebase unwraps via directory.get("platforms", {}). This aligns
channels_list with that convention.
Also tightens the existing test_channels_with_directory test, which
bypassed the bug by asserting against _load_channel_directory() directly
instead of calling channels_list. It now calls the tool end-to-end and
a new test_channels_with_directory_platform_filter covers the filter
path. Both tests fail against the pre-fix code.
Closes NousResearch#21474
Co-authored-by: chrisworksai <262485129+chrisworksai@users.noreply.github.com>
…udo -u When the installer is run via , uv resolves config file paths against the process owner's (root) home directory rather than the effective user's, causing a Permission denied error when trying to read /root/uv.toml. Setting UV_NO_CONFIG=1 prevents uv from discovering any config files (uv.toml, pyproject.toml) during installation, which is the correct behavior for a bootstrap script that manages its own environment. Fixes NousResearch#21269
…essions-skills-menu feat(tui): add /sessions slash command for browsing and resuming previous sessions
…rsonality fix(tui): preserve session when switching personality
Co-authored-by: austinpickett <260188+austinpickett@users.noreply.github.com>
…ch#21541) Makes first-time use of the kanban view self-explanatory. Every control that wasn't already labelled now has a `title` tooltip describing what it does, and a `?` icon next to the board switcher opens the kanban docs page in a new tab. Coverage: - BoardSwitcher: board select, + New board button, docs-link icon (both compact and full variants) - BoardToolbar: Search, Tenant, Assignee, Show archived, Nudge dispatcher, Refresh - BulkActionBar: → ready, Complete, Archive, reassign group, Apply, Clear - Column header: hovering the header now surfaces COLUMN_HELP as a tooltip in addition to the visible sub-text; column count also labelled - Card: task id, priority badge, tenant badge, assignee/unassigned, comment count, link count, age timestamp - InlineCreate: assignee, priority, parent-task selectors Closes the community feedback from @CharlieDePew asking for tooltips and a docs link in the kanban view. Relevant docs page: https://hermes-agent.nousresearch.com/docs/user-guide/features/kanban
Route goal status notices through the platform adapter send API and register post-delivery callbacks so completed-goal notices appear after the final assistant response. Also cancel queued synthetic goal continuations on /goal pause and /goal clear while preserving normal queued user messages.
Weak judge models (e.g. deepseek-v4-flash) return empty strings or prose
when asked for the strict {done, reason} JSON verdict. The old code
failed-open to continue on every such turn, burning the entire turn
budget with log lines like
judge returned empty response
judge reply was not JSON: "Let me analyze whether the goal..."
and /goal clear could not stop it mid-loop without /stop.
After N=3 consecutive *parse* failures (transport/API errors don't
count — those are transient), the loop auto-pauses and prints:
⏸ Goal paused — the judge model (3 turns) isn't returning the
required JSON verdict. Route the judge to a stricter model in
~/.hermes/config.yaml:
auxiliary:
goal_judge:
provider: openrouter
model: google/gemini-3-flash-preview
Then /goal resume to continue.
The counter resets on any usable reply (both "done"/"continue" and
API errors) and persists across GoalManager reloads so cross-session
resumes carry the correct state.
Also fixes test_goal_verdict_send.py sharing a hardcoded session_id
across tests — the shared id only worked because the previous
_post_turn_goal_continuation was a never-awaited coroutine. Now that
PR NousResearch#19160 made it properly awaited, the xdist test-leakage bug
surfaced. Each test gets a unique session_id via uuid suffix.
…itch (NousResearch#21703) When switching from a custom local provider (e.g. ollama-launch) to a cloud provider, two bugs caused the CLI to misbehave: 1. _explicit_api_key/_explicit_base_url were only updated when the switch result had non-empty values (guarded by `if result.api_key:` etc.). If the previous provider set these to Ollama values ("ollama", "http://127.0.0.1:11434/v1"), those stale values leaked into the next turn's _ensure_runtime_credentials() call and were forwarded to the new provider's API endpoint, causing authentication/routing failures. Fix: unconditionally write result.api_key/base_url into the explicit fields after every successful switch. An empty string is the correct sentinel — it tells _ensure_runtime_credentials to re-resolve from the auth store / config rather than forwarding a stale override. 2. In AIAgent.switch_model(), `self.base_url = base_url or self.base_url` kept the old Ollama localhost URL whenever the incoming base_url was an empty string. For providers that use a native SDK (not an OpenAI-compat endpoint), the caller passes base_url="" and expects the agent to clear the field — not silently inherit Ollama's address. Fix: only update self.base_url when base_url is truthy. 3. _handle_model_picker_selection() was called from the prompt_toolkit Enter key binding without any exception guard. Any unexpected error in the model-selection code path propagated through prompt_toolkit's key-binding dispatcher and caused the entire TUI to exit — which the user sees as "the terminal exits when I switch providers". Fix: wrap the call in try/except and close the picker on failure.
The previous revision of this PR added six GMI-specific branches
(`elif base_url_host_matches(..., 'api.gmi-serving.com')`) across
run_agent.py and agent/auxiliary_client.py, plus a _HERMES_UA_HEADERS
constant in auxiliary_client.py.
ProviderProfile already has a `default_headers: dict[str, str]` field
commented as 'Client-level quirks (set once at client construction)'.
Other plugins (ai-gateway, kimi-coding) already use it. Two of the four
auxiliary_client sites we previously patched already had a generic
`else: profile.default_headers` fallback that picked it up (so did
both run_agent sites).
This revision:
* Sets `default_headers={'User-Agent': 'HermesAgent/<ver>'}` on the
GMI profile in plugins/model-providers/gmi/__init__.py.
* Reverts all six GMI-specific branches in run_agent.py and
auxiliary_client.py.
* Adds the generic profile-fallback `else` block to the two
auxiliary_client sites (`_to_async_client`, `resolve_provider_client`)
that didn't have it yet. This benefits every provider whose profile
declares default_headers, not just GMI — e.g. Vercel AI Gateway's
HTTP-Referer/X-Title now flow through the async client path too.
* Replaces the GMI-specific URL-branch tests with a profile-level
assertion and keeps the run_agent integration test (with
`provider='gmi'` so the fallback picks up the profile).
Net diff vs main: +82/-0 across 5 files, touching only the GMI plugin,
two generic fallback blocks in auxiliary_client.py, AUTHOR_MAP, and
tests. No core files change.
Based on NousResearch#20907 by @isaachuangGMICLOUD.
…channel (NousResearch#21495) Adds one reserved token to the cron `deliver` field: - `all` — expand to every platform with a configured home channel Resolves at fire time, not create time, so a job created before Telegram was wired up picks it up once `TELEGRAM_HOME_CHANNEL` is set. Composes with existing targets: `origin,all`, `all,telegram:-100:17`. Inspired by Vellum Assistant's reminder routing-intent system. ## Changes - cron/scheduler.py: _expand_routing_tokens + integrate into _resolve_delivery_targets - tools/cronjob_tools.py: schema description updated - tests/cron/test_scheduler.py: TestRoutingIntents (5 cases) - website/docs/user-guide/features/cron.md: docs + table rows ## Validation - tests/cron/test_scheduler.py -k 'Routing or Deliver' → 57 passed
…eck-live setup.py --check only validated token shape/expiry but did not detect when Google had disabled the OAuth client or account. Users got AUTHENTICATED even when actual API calls failed with disabled_client. Changes: - Catch disabled_client and invalid_client in check_auth() refresh path with actionable guidance (check Cloud Console, check account status, do not retry) - Add check_auth_live() that performs a real Calendar API call to detect disabled_client errors that survive token refresh - Add --check-live CLI flag backed by check_auth_live() Fixes NousResearch#19570
Small follow-ups on top of NousResearch#19643: - check_auth() takes quiet kwarg to suppress its AUTHENTICATED print when called from check_auth_live(), so the final status line reflects the live-call outcome only. - Drop redundant _ensure_deps() call in check_auth_live() (check_auth() already calls it). - Add AUTHOR_MAP entry for ygd58 so release attribution script works.
The quick setup flow (recommended for first-time users) silently defaulted terminal.backend to 'local' without ever presenting the choice. This meant new users who wanted Docker, SSH, Modal, Daytona, or any other backend had to know about 'hermes setup terminal' — which most wouldn't discover until later. Now the quick setup flow is: 1. Provider selection 2. API key 3. Terminal backend (local/Docker/Modal/SSH/Daytona/Vercel/Singularity) 4. Messaging platform 5. Done The terminal backend is a foundational decision (where ALL commands run) and belongs in the onboarding path alongside provider selection.
…cker dead space (NousResearch#21846) Multi-turn transcripts ran together visually because every user message got the same vertical rhythm regardless of position. Adds a short ─── in the border colour above every user message after the first, so each turn reads as its own block. Height estimator gains a `withSeparator` flag so virtual scrolling pre-allocates the extra two rows (rule + top margin) and avoids a jump on first measurement. While in the area: the busy-indicator duration was padded with `padStart(7)`, leaving five visible spaces between `·` and the digits (`⠋ · 2s`) — especially loud under the verb-less `unicode` style. Drop the padding entirely (`⠋ · 2s`); the model label now shifts a few columns as the duration grows, which is the right trade-off for the minimal indicator styles. The verb-padding test stays; the duration-padding test is removed alongside the function it covered.
…uralization fix(cli): use proper singular/plural in doctor and claw messages
🚨 CRITICAL Supply Chain Risk DetectedThis PR contains a pattern that has been used in real supply chain attacks. A maintainer must review the flagged code carefully before merging. 🚨 CRITICAL: Install-hook file added or modifiedThese files can execute code during package installation or interpreter startup. Files: Scanner only fires on high-signal indicators: .pth files, base64+exec/eval combos, subprocess with encoded commands, or install-hook files. Low-signal warnings were removed intentionally — if you're seeing this comment, the finding is worth inspecting. |
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-attribute |
11 |
unresolved-import |
4 |
invalid-parameter-default |
1 |
First entries
tests/hermes_cli/test_kanban_cli.py:329: [unresolved-attribute] unresolved-attribute: Attribute `status` is not defined on `None` in union `Task | None`
tests/hermes_cli/test_kanban_specify.py:129: [unresolved-attribute] unresolved-attribute: Attribute `title` is not defined on `None` in union `Task | None`
tests/gateway/test_goal_verdict_send.py:64: [invalid-parameter-default] invalid-parameter-default: Default value of type `None` is not assignable to annotated parameter type `str`
tests/hermes_cli/test_kanban_cli.py:330: [unresolved-attribute] unresolved-attribute: Attribute `title` is not defined on `None` in union `Task | None`
tests/hermes_cli/test_kanban_specify.py:131: [unresolved-attribute] unresolved-attribute: Attribute `body` is not defined on `None` in union `Task | None`
tests/hermes_cli/test_kanban_specify.py:323: [unresolved-attribute] unresolved-attribute: Attribute `status` is not defined on `None` in union `Task | None`
tests/hermes_cli/test_kanban_specify_db.py:8: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/gateway/test_goal_status_notice.py:122: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_pending_messages` on type `FakeAdapter`
tests/hermes_cli/test_kanban_specify.py:15: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/hermes_cli/test_kanban_specify_db.py:172: [unresolved-attribute] unresolved-attribute: Attribute `title` is not defined on `None` in union `Task | None`
tests/gateway/test_goal_status_notice.py:5: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/hermes_cli/test_kanban_specify_db.py:173: [unresolved-attribute] unresolved-attribute: Attribute `body` is not defined on `None` in union `Task | None`
tests/hermes_cli/test_kanban_specify_db.py:160: [unresolved-attribute] unresolved-attribute: Attribute `status` is not defined on `None` in union `Task | None`
tests/hermes_cli/test_goals.py:495: [unresolved-attribute] unresolved-attribute: Attribute `consecutive_parse_failures` is not defined on `None` in union `GoalState | None`
skills/productivity/google-workspace/scripts/setup.py:140: [unresolved-import] unresolved-import: Cannot resolve imported module `googleapiclient.discovery`
tests/gateway/test_goal_status_notice.py:146: [unresolved-attribute] unresolved-attribute: Object of type `FakeAdapter` has no attribute `_pending_messages`
✅ Fixed issues: none
Unchanged: 4020 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
Daily sync with upstream. Auto-created by cron job.
Commits ($ (git log --oneline origin/main..upstream/main | wc -l) total):
a313186 Merge pull request NousResearch#19830 from NousResearch/austin/fix/pluralization
42f9234 feat(tui): segment turns with rule above non-first user msgs; trim ticker dead space (NousResearch#21846)
7190e20 fix: include terminal backend in quick setup wizard (NousResearch#21842)
83c23e8 fix(google-workspace): cleanup for --check-live salvage
617ac05 fix: correct docstring syntax error in check_auth_live
5fa493a fix(google-workspace): detect disabled_client in --check and add --check-live
80775d7 test(auth): assert Nous refresh rotation payload
b32461f fix(auth): send Nous refresh token via header
486b14b feat(cron): routing intent — deliver=all fans out to every connected channel (NousResearch#21495)
81928f0 refactor(gmi): move User-Agent to profile.default_headers
5d1bdf1 Add AUTHOR_MAP entry for Isaac Huang
7338e5d fix(model-switch): prevent stale Ollama credentials after provider switch (NousResearch#21703)
faa13e4 docs(web): fix SearXNG env configuration
1bdacb6 chore(release): add BennetYrWang to AUTHOR_MAP
34f7297 Serialize Hermes config access
307c85e fix(goals): auto-pause when judge model returns unparseable output
03ddff8 fix(gateway): defer goal status notices until after response delivery
7d66d30 feat(kanban): add tooltips and docs link across dashboard (NousResearch#21541)
901eccc Merge origin/main and resolve conflict in nix/tui.nix
7f92e55 Merge pull request NousResearch#20942 from NousResearch/austin/fix/personality
b0393af Merge pull request NousResearch#20805 from NousResearch/austin-feat-sessions-skills-menu
7f369bf chore(release): add hllqkb to AUTHOR_MAP for PR NousResearch#21288 salvage
c80fa72 fix(installer): set UV_NO_CONFIG=1 to avoid permission denied under sudo -u
292f468 fix(mcp): unwrap platforms key in channels_list
d87c7b9 fix(analytics): prevent silent token loss and add Claude 4.5–4.7 pricing (NousResearch#21455)
cff821e docs: register triage_specifier in the aux-models enumerations (NousResearch#21494)
2214ab1 chore: fix AUTHOR_MAP for johnsonblake1@gmail.com → voteblake
9076a2e fix(agent): keep Nous GPT-5 fallback on chat completions
24d48ff feat(kanban): add
specify— auxiliary LLM fleshes out triage tasks (NousResearch#21435)732a6c4 feat: add termux doctor fallback guidance for blocked extras
...