4a3e3e20e5b2ed2fd0c2e727f8204efea4de8a5a
7223 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4a3e3e20e5 | fix(compression): preserve iterative summary continuity | ||
|
|
f8a6db68ca |
test(kanban): isolate HERMES_KANBAN_BOARD writes in pin-env tests
The helper under test writes to os.environ directly, bypassing monkeypatch tracking. Without an explicit snapshot/restore fixture, the mutation leaks into subsequent tests and breaks TestSharedBoardPaths (kanban path resolution reads HERMES_KANBAN_BOARD and routes through boards/<leaked-slug>/ instead of the test's own HERMES_HOME). Add an autouse fixture that snapshots the env var before the test and restores (or pops) it after, regardless of what the helper did. |
||
|
|
b22b3f506a |
fix(cli): pin HERMES_KANBAN_BOARD at chat boot to stop subprocess board drift
Without an explicit pin, in-process kanban tools and shelled-out `hermes kanban …` subprocesses resolve the active board on different paths: the env var when set, otherwise the global `<root>/kanban/current` file. When a concurrent session toggles the current-board pointer mid-turn, the same chat ends up routing tool calls to board A while its shell calls hit board B, surfacing as phantom "no such task" errors. Pin the resolved board into env once at `cmd_chat` boot when HERMES_KANBAN_BOARD isn't already set. Mirrors what the dispatcher does for spawned workers (kanban_db.py:2622-2623). Idempotent and a no-op when the env is already pinned by the caller. Closes #20074 |
||
|
|
d472d697cd | chore(release): map stevekelly622@gmail.com → @steezkelly | ||
|
|
8c82d0664d | fix(kanban): ignore stale current board pointers | ||
|
|
2a285d5ec2 |
fix(agent): stateful streaming scrubber for reasoning-block leaks (#17924) (#20184)
* revert(gateway): remove stale-code self-check and auto-restart Removes the _detect_stale_code / _trigger_stale_code_restart mechanism introduced in #17648 and iterated in #19740. On every incoming message the gateway compared the boot-time git HEAD SHA to the current SHA on disk, and if they differed it would reply with Gateway code was updated in the background -- restarting this gateway so your next message runs on the new code. Please retry in a moment. and then kick off a graceful restart. This is unwanted behaviour: users who run a long-lived gateway and do their own ad-hoc git operations on the checkout end up with their chat interrupted and the current message dropped every time HEAD moves, with no way to opt out. If an operator really needs the old protection against stale sys.modules after "hermes update", the SIGKILL-survivor sweep in hermes update (hermes_cli/main.py, also tagged #17648) already handles the supervisor-respawn case on its own. Removed: gateway/run.py: - _STALE_CODE_SENTINELS, _GIT_SHA_CACHE_TTL_SECS - _read_git_head_sha(), _compute_repo_mtime() module helpers - class-level _boot_wall_time / _boot_repo_mtime / _boot_git_sha / _stale_code_restart_triggered defaults - __init__ boot-snapshot block (_boot_*, _cached_current_sha*, _repo_root_for_staleness, _stale_code_notified) - _current_git_sha_cached(), _detect_stale_code(), _trigger_stale_code_restart() methods - stale-code check + user-facing restart notice at the top of _handle_message() tests/gateway/test_stale_code_self_check.py (deleted, 412 lines) No new logic added. Zero remaining references to any removed symbol. Gateway test suite passes the same 4589 tests it passed before; the 3 pre-existing unrelated failures (discord free-channel, feishu bot admission, teams typing) are unchanged by this commit. * fix(agent): stateful streaming scrubber for reasoning-block leaks (#17924) Per-delta _strip_think_blocks ran at _fire_stream_delta and destroyed downstream state. When MiniMax-M2.7 / DeepSeek / Qwen3 streamed a tag split across deltas (delta1='<think>', delta2='Let me check'), the regex case-2 match erased delta1 entirely, so CLI/gateway state machines never learned a block was open and leaked delta2 as content. Raw consumers (ACP, api_server, TTS) had no downstream defense at all. Replace the per-delta regex with a stateful StreamingThinkScrubber that survives delta boundaries: - Closed <tag>X</tag> pairs always stripped (matches _strip_think_blocks case 1). - Unterminated open at block boundary enters a block; content discarded until close tag arrives. At end-of-stream, held content is dropped. - Orphan close tags stripped without boundary gating. - Partial tags at delta boundaries held back until resolved. - Block-boundary rule (start-of-stream, after \n, or whitespace-only since last \n) preserves prose that mentions tag names. Reset at turn start alongside the existing context scrubber; flush at turn end so a benign '<' held back at end-of-stream reaches the UI. E2E-verified on live OpenRouter->MiniMax-m2 streams: closed pairs strip cleanly, first word of post-block content is preserved, pure content passes through unchanged. Stefan's screenshot case (#17924) — 'Let me check' getting chopped to ' me check' — no longer happens. Final _strip_think_blocks calls on completed strings (final_response, replay, compression) are preserved; only the streaming per-delta call site switched to the scrubber. |
||
|
|
28f4d6db63 |
fix(tool-schemas): reactive strip of pattern/format on llama.cpp grammar 400s
MCP servers commonly emit JSON Schema `pattern` (e.g. `\\d{4}-\\d{2}-\\d{2}`
for date-time params) and `format` keywords. llama.cpp's
`json-schema-to-grammar` converter rejects regex escape classes
(\\d/\\w/\\s) and most format values, returning HTTP 400
"parse: error parsing grammar: unknown escape at \\d" — the whole request
fails.
Cloud providers (OpenAI, Anthropic, OpenRouter, Gemini) accept these
keywords fine and use them as prompting hints. Stripping unconditionally
loses useful hints for every cloud user to fix a llama.cpp-only bug.
Approach: classify the llama.cpp grammar-parse 400 in the error
classifier, and on match do a one-shot in-place strip of pattern/format
from `self.tools`, then retry. Follows the existing
`thinking_signature` recovery pattern. Cloud users hit zero overhead;
llama.cpp users pay one failed request per session.
Changes
- agent/error_classifier.py: new `FailoverReason.llama_cpp_grammar_pattern`
+ narrow HTTP-400 branch matching "error parsing grammar",
"json-schema-to-grammar", or "unable to generate parser ... template".
- tools/schema_sanitizer.py: new `strip_pattern_and_format()` helper —
reactive, walks schema nodes, skips property names (search_files.pattern
survives). Returns strip count for logging.
- run_agent.py: new one-shot recovery block in the retry loop. Strips,
logs, continues. Falls through to normal retry if nothing to strip.
- tests: 4 classifier tests (3 variants + 1 non-400 negative), 7 strip
tests including the property-name preservation and idempotency checks.
Co-authored-by: Chris Danis <cdanis@gmail.com>
|
||
|
|
542e06c789 | fix: include default profile in kanban assignees | ||
|
|
fc4aa66ee4 |
feat(tips): add 100 new CLI startup tips (#20168)
Expands TIPS corpus from 280 to 380 entries covering untapped territory across slash commands, CLI flags, env vars, config keys, and platform features. Every tip verified against real code and docs. Batch 1 (50): advanced slash commands (/steer, /goal, /snapshot, /copy, /redraw, /agents, /footer, /busy, /topic, /approve, /restart, /kanban, /reload), no-agent cron, gateway hooks, curator, credential pools, provider routing, TUI/dashboard env vars and themes, checkpoints, Piper TTS, API server, GATEWAY_PROXY_URL, MATRIX_DEVICE_ID, TELEGRAM_WEBHOOK_SECRET, batch_runner --resume. Batch 2 (50): lesser-known slash commands (/new, /clear, /history, /save, /status, /image, /platforms, /commands, /toolsets, /gquota, /voice tts, /reload-skills, /indicator, /debug), CLI subcommands (hermes -z, --pass-session-id, --image, --ignore-user-config, --source tool, dump --show-keys, sessions rename/delete, import, fallback, pairing, setup, status --deep), agent behavior env vars (HERMES_AGENT_TIMEOUT, HERMES_ENABLE_PROJECT_PLUGINS, HERMES_DISABLE_FILE_STATE_GUARD, HERMES_ALLOW_PRIVATE_URLS, HERMES_OPTIONAL_SKILLS, HERMES_BUNDLED_SKILLS, HERMES_DUMP_REQUEST_STDOUT, HERMES_OAUTH_TRACE, HERMES_STREAM_RETRIES), gateway env vars, image_gen config, auxiliary.session_search, tirith_fail_open, source tool filtering, API_SERVER_MODEL_NAME, dashboard plugins. |
||
|
|
f25d3ec917 |
fix(kanban): suppress dispatcher stuck-warn when ready queue holds only non-spawnable assignees
After PR #20105 (dispatcher skips ready tasks whose assignee fails ``profile_exists()`` to prevent the orion-cc/orion-research crash loop), the gateway and CLI emit a spurious "kanban dispatcher stuck: ready queue non-empty for N consecutive ticks but 0 workers spawned" warning every 5 minutes on multi-lane setups where the queue is steadily full of human-pulled work assigned to terminal lanes. The warn is intended to catch real failure modes (broken PATH, missing venv, credential loss for a real Hermes profile). On a multi-lane host it fires forever even though everything is healthy: the dispatcher correctly chose not to spawn, and there is nothing for the operator to fix. Changes: * ``DispatchResult`` gains a ``skipped_nonspawnable`` field (separate from ``skipped_unassigned``) so callers can distinguish "task missing an owner — operator should route it" from "task owned by a control-plane lane — terminal will pull it". * ``dispatch_once`` routes the ``not profile_exists(assignee)`` skip into the new bucket (was lumped into ``skipped_unassigned``). * New helper ``has_spawnable_ready(conn)`` returns True iff at least one ready+assigned+unclaimed task in the DB has an assignee that maps to a real Hermes profile. Falls back to legacy "any ready+assigned" when ``profile_exists`` is unimportable so degraded installs still surface the original warn. * The gateway dispatcher (``gateway/run.py``) and the CLI standalone daemon (``hermes_cli/kanban.py``) both swap their cheap ``ready_nonempty`` probe to use ``has_spawnable_ready``. Stuck-warn now fires only when there is genuine spawnable work the dispatcher failed to start. * CLI dispatch output prints ``Skipped (non-spawnable assignee — terminal lane, OK)`` for visibility without alarm. Tests: * New ``has_spawnable_ready`` cases (empty queue, terminal-lane only, mixed real+terminal). * New ``test_dispatch_skips_nonspawnable_into_separate_bucket`` verifies the bucketing change. * Updated ``test_dispatch_skips_unassigned`` to assert no cross-leak. * Added ``all_assignees_spawnable`` fixture in ``tests/hermes_cli/conftest.py`` and threaded it through dispatcher tests that use synthetic assignees ("alice", "bob"). PR #20105 (the parent commit) silently broke 8 such tests by routing those assignees into ``skipped_nonspawnable`` instead of spawning; this PR repairs them as part of the same code area. Verified locally: 246/246 kanban-suite tests pass. Stacks on top of fix/kanban-dispatcher-skip-missing-profile-2026-05-05 (PR #20105). Reviewer: this PR is meant to merge AFTER #20105. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ca5595fe7b |
fix(kanban): dispatcher skips ready tasks whose assignee is not a real profile
The kanban dispatcher's `_default_spawn` invokes
``hermes -p <task.assignee> chat -q ...``. When ``assignee``
names a control-plane lane (e.g. an interactive Claude Code
terminal like ``orion-cc`` / ``orion-research``) instead of a
real Hermes profile, the subprocess fails on startup with
"Profile 'X' does not exist", gets reaped as a zombie, the
TTL/crash detector marks the task back to ``ready``, and the
next tick re-spawns the same crashing worker. Result: a
permanent crash loop emitting ``spawned=2 crashed=2 every tick``
in the gateway log and burning CPU forever.
Reproduce on a fresh Hermes-agent install:
# 1. Create a kanban task whose assignee names a non-profile.
hermes kanban create --assignee orion-cc --status ready \
--title "Review PR #N" --body "..."
# 2. Start the gateway with the embedded dispatcher.
hermes gateway run
# gateway.log lines every minute:
# kanban dispatcher: tick spawned=1 reclaimed=0 crashed=1 ...
# 3. ps -ef | grep '[h]ermes.*defunct' shows zombies.
Fix
---
``dispatch_once()`` now pre-checks ``hermes_cli.profiles.
profile_exists(assignee)`` before claiming. If False, the row
is added to ``skipped_unassigned`` (it's effectively
"unassigned-to-an-executable-profile") and the dispatcher
moves on without claiming, spawning, or counting a crash.
The check is opt-in safe: if the import fails (e.g. test
isolation, profile module restructured), ``profile_exists``
falls back to ``None`` and the original behaviour is preserved
unchanged.
This addresses the explicit hint in the kanban task body
(``t_2bab06e3``):
"Should ready-state tasks auto-spawn at all, or only on
explicit orion-cc claim? If spurious, gate the auto-spawn
behind a config flag (e.g. only assignee=hermes or
assignee=auto)."
Profile-existence is a tighter gate than a config flag — it
self-documents (the user already knows whether they have an
``orion-cc`` profile), and it doesn't require Mac to maintain
an allowlist as new lane names appear. New lanes that ARE
real profiles (created via ``hermes profile create``) auto-
qualify the moment the profile dir is created.
Validated live
--------------
On Orion's hermes-agent install, two ``orion-research``-
assigned tasks (Bug A and Bug C investigations) had been
crash-looping since 2026-05-05 06:58 local. After applying
the patch + restarting the gateway:
- Stale ``running`` claims released to ``ready`` cleanly.
- New gateway emitted ``kanban dispatcher: embedded`` and
has ticked silently for 2+ minutes — no spawned=,
crashed=, or stuck= log lines (all spawn skips are quiet).
- Tasks remain ``ready`` with ``claim_lock=None``,
``worker_pid=None``, ``spawn_failures=0``.
- Dashboard + telegram + freqtrade unaffected.
Confidence: high (live verified on Orion).
Scope-risk: narrow (additive guard inside one function).
Not-tested: behaviour when a profile is renamed mid-tick —
current code re-imports ``profile_exists`` per row so a
freshly created profile auto-qualifies on the next tick.
Machine: orion-terminal
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
91ce8fc000 |
fix(setup): offer Keep/Replace/Clear when API key already exists
hermes setup / hermes model used to silently skip the key prompt when any value was present in .env — even a malformed paste — leaving users with a stuck '✓' and no way to recover without hand-editing .env. Replace the silent acknowledgement at all three API-key provider flows (Kimi, Stepfun, generic) with a single [K]eep / [R]eplace / [C]lear menu via a shared `_prompt_api_key` helper. - K / Enter / Ctrl-C / unknown input → keep (never destroys the key) - R → getpass for new key; empty input cancels and preserves existing - C → clears the env var, tells user to rerun hermes setup, aborts flow LM Studio's no-auth-placeholder substitution stays on first-time entry only; on Replace an empty input means 'cancel', not 'overwrite with dummy key'. 11 unit tests cover all branches incl. garbage-input-keeps-key, Ctrl-C at the choice prompt, Replace-cancel preserving the old key, Clear wiping only the target env var, and lmstudio placeholder semantics. Fixes #16394 Reshapes #18355 — original PR pasted the menu inline at 3 sites with no tests; this consolidates to one helper (+88/-66) with coverage. Co-authored-by: Feranmi10 <89228157+Feranmi10@users.noreply.github.com> |
||
|
|
8ad5e98f8d | fix(gateway): preserve pending update prompts across restarts | ||
|
|
2785355750 | chore(release): map bjianhang@gmail.com → @bjianhang | ||
|
|
c3112adac5 | fix(tui): improve clipboard copy fallbacks | ||
|
|
13a7cbcd64 |
fix(nix): refresh stale tui npmDepsHash + fix cache-blind detection (#20144)
The fix-lockfiles script used 'nix build .#tui.npmDeps' to detect stale hashes. This always succeeds when the OLD derivation is cached in Cachix or cache.nixos.org — even when the source package-lock.json has changed. Fix: use prefetch-npm-deps to compute the hash directly from the lockfile and compare against what's in the nix file. Falls back to nix build only if prefetch-npm-deps fails. |
||
|
|
601e5f1d57 |
fix(teams): log reply() fallback for diagnostics
The previous bare except swallowed every exception from app.reply() silently. Log at debug so real failures (auth, chat gone) leave a trace while keeping the group-chat 400 fallback working. Also fix the Teams entry's indentation in the messaging flowchart. |
||
|
|
2333b7a7ec |
fix(tests): patch TypingActivityInput after mock on Python <3.12
The SDK requires Python >=3.12 so CI (3.11) falls to the except ImportError branch, leaving TypingActivityInput=None. After loading the adapter module, explicitly restore it from the mock so test_send_typing doesn't silently no-op. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
3f023450dd |
fix(teams): fall back to flat send when threading returns 400
Group chats return 400 for threaded sends. Catch the error and fall back to a flat send so messages always get delivered. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
69aeba0df7 |
feat(teams): implement threading via app.reply()
Wire reply_to into send() using App.reply(conv_id, msg_id, content) which constructs the threaded conversation ID internally. Threads supported in channels and group chats. Update comparison table: Threads ✅ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
10f89d7b72 |
docs(teams): add Teams to messaging/index.md
- Add to platform description and intro paragraph - Add row to platform comparison table (images + typing) - Add node to architecture mermaid diagram - Add TEAMS_ALLOWED_USERS to security examples - Add to platform-specific toolsets table - Add to Next Steps links Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
93869b48ab |
docs: add Microsoft Teams to platform lists across docs
Update all platform enumeration lists to include Teams: index.md, quickstart.md, integrations/index.md, sessions.md, slash-commands.md, updating.md, hooks.md, hermes-agent skill. Skipped PII redaction docs — Teams uses AAD object IDs, not phone numbers, so redaction doesn't apply there. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
ef94aa201f |
docs(teams): add Teams to sidebar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
c77a6e3faa |
chore(security): add OSV-Scanner CI + Dependabot for github-actions only (#20037)
Adds two supply-chain controls that complement our existing pinning strategy (full-SHA action pins, exact-version source dep pins via uv.lock / package-lock.json) without undermining it. .github/workflows/osv-scanner.yml Detection-only scan of uv.lock and the ui-tui/website package-locks against the OSV vulnerability database. Runs on PRs that touch lockfiles, on push to main, and weekly against main so CVEs published after merge still surface. Uses Google's officially- recommended reusable workflow pinned by full SHA (v2.3.5). Findings upload to the Security tab; fail-on-vuln is disabled so pre-existing vulns in pinned deps do not block merges — we move pins deliberately, not under CI pressure. .github/dependabot.yml Scoped to github-actions only. Action pins must be moved when upstream publishes patches (often themselves security fixes); Dependabot opens a PR with the new SHA + release notes for normal review. Source-dependency ecosystems (pip, npm) are deliberately NOT enabled — automatic version-bump PRs against uv.lock / package-lock.json would fight our pinning strategy. CVE-driven security updates for source deps are enabled separately via the repo's Dependabot security updates setting (GitHub UI), which fires only when a pinned version becomes known-vulnerable. |
||
|
|
1d938832a7 | test(kanban): patch dashboard websocket token stub | ||
|
|
f7918c9349 | test(teams): mock ClientOptions in adapter tests | ||
|
|
a1bed18194 |
docs: clarify that the Docker terminal backend is a single persistent container (#20003)
The docs were ambiguous about whether the Docker terminal backend spins up a fresh container per command or reuses a long-lived one. It's the latter — Hermes starts one container on first use and routes every terminal, file, and execute_code call through docker exec into that same container for the life of the process (across /new, /reset, and delegate_task subagents). Working-directory changes, installed packages, and files in /workspace persist from one tool call to the next, like a local shell. - configuration.md: lead the Docker Backend section with the persistence model before the YAML example; sharpen the Backend Overview table row. - features/tools.md: expand the Docker Backend block (previously just a 2-line YAML stub) with a clear statement of the persistent-container semantics and a pointer to the full lifecycle section. - docker.md: tighten the 'Docker as a terminal backend' bullet and the 'Skills and credential files' paragraph to call out the single-container model explicitly. |
||
|
|
d12f59aa53 |
Merge pull request #19866 from NousResearch/fix/clarify-placeholder-credential
clarify placeholder telegram credential in tests |
||
|
|
b816fd4e26 | fix(tui): complete absolute paths as paths | ||
|
|
b632290166 | fix(gateway): handle planned service stops | ||
|
|
20428f5e60 |
fix(tui): respect voice.record_key config (supersedes #19028, #19339) (#19835)
* fix(tui): respect voice.record_key config instead of hardcoded Ctrl+B
Classic CLI loaded ``voice.record_key`` from config.yaml and bound the
prompt-toolkit handler dynamically (``cli.py`` paths). The new TUI hard-
coded ``Ctrl+B`` everywhere — ``isVoiceToggleKey`` (input handler),
``/voice status`` ("Record key: Ctrl+B"), and ``/voice on`` ("Ctrl+B to
start/stop recording"). A user who set ``voice.record_key: ctrl+o``
(or any other key) saw the documented config silently ignored — only
Ctrl+B worked, the displayed shortcut lied about it.
Wire the configured key end to end through the existing channels:
* **Backend** (``tui_gateway/server.py``): ``voice.toggle`` action=status
AND action=on/off responses now include ``record_key``, sourced from
``config.get('voice', {}).get('record_key', 'ctrl+b')``.
* **Backend types** (``ui-tui/src/gatewayTypes.ts``): ``ConfigFullResponse``
now exposes ``config.voice.record_key`` and ``VoiceToggleResponse``
carries ``record_key`` so the TUI can both bind and display it.
* **Frontend parser/formatter** (``ui-tui/src/lib/platform.ts``):
``parseVoiceRecordKey()`` accepts ``ctrl+b`` / ``alt+r`` / ``cmd+space``
and the common aliases (``option``, ``cmd``, ``win``, …); falls back to
the documented Ctrl+B for empty / multi-character / malformed input so
a typo never silently disables the shortcut. ``formatVoiceRecordKey()``
renders for status text. ``isVoiceToggleKey`` now takes a parsed
``ParsedVoiceRecordKey`` argument; the hardcoded ``ch === 'b'`` is
gone. Default arg keeps existing call sites back-compat.
* **Hydration** (``ui-tui/src/app/useConfigSync.ts``,
``useMainApp.ts``): startup ``config.get full`` already runs; extract
``cfg.voice.record_key`` from it, parse, push into a new
``voiceRecordKey`` state, and forward to the input handler ctx
(``InputHandlerContext.voice.recordKey``). Mtime-poll path also
re-applies the parsed key so a hand-edit of config.yaml takes effect
the next tick — matches existing behaviour for display options.
* **Input handler** (``ui-tui/src/app/useInputHandlers.ts``):
``isVoiceToggleKey(key, ch, voice.recordKey)`` so the configured
binding fires.
* **Slash command** (``ui-tui/src/app/slash/commands/session.ts``):
``/voice status`` and ``/voice on`` use ``formatVoiceRecordKey`` on
the response's ``record_key`` instead of the hardcoded label.
Tests:
* ``parseVoiceRecordKey`` covers ctrl/alt/cmd/super aliases, multi-char
rejection, and empty fallback.
* ``formatVoiceRecordKey`` covers the doc examples (``Ctrl+B``,
``Ctrl+O``, ``Alt+R``, ``Cmd+B``).
* ``isVoiceToggleKey`` regression: ``ctrl+o`` configured → only ``o``
matches, not ``b``; ``alt+r`` matches both alt-bit and meta-bit
encodings (terminal protocol parity); omitted-arg call still binds
Ctrl+B for back-compat.
Full TUI suite (555 tests) passes; ``tsc --noEmit`` clean.
Fixes #18994
Co-authored-by: asheriif <ahmedsherif95@gmail.com>
* fix(tui): support named-key tokens in voice.record_key (space, enter, …)
Reviewer caught that the round-1 parser in #18994 rejected every
multi-character token, so a config value like ``ctrl+space`` (which the
CLI happily binds via prompt_toolkit's ``c-space`` rewrite in
``cli.py``) silently fell back to the documented Ctrl+B default —
re-introducing the same false-shortcut bug the PR was meant to fix,
just at a different surface.
Add explicit named-key support that mirrors what the CLI accepts:
* ``space`` (alias: ``spc``) → matches ``ch === ' '``
* ``enter`` (alias: ``return``, ``ret``) → matches ``key.return``
* ``tab`` → matches ``key.tab``
* ``escape`` (alias: ``esc``) → matches ``key.escape``
* ``backspace`` (alias: ``bs``) → matches ``key.backspace``
* ``delete`` (alias: ``del``) → matches ``key.delete``
``ParsedVoiceRecordKey`` gains an optional ``named`` field; ``ch``
holds either a single char (back-compat) or the canonical named token,
and the runtime matcher dispatches on ``named`` before checking the
modifier shape. Aliases collapse to one canonical name so
``ctrl+esc`` and ``ctrl+escape`` behave identically.
Unrecognised multi-character tokens (e.g. ``ctrl+spcae`` typo, or
unsupported keys like ``ctrl+f5``) still fall back to the Ctrl+B
default rather than silently disabling the binding — keeps the "typo
never silently kills the shortcut" guarantee.
Tests:
* ``parseVoiceRecordKey`` parametrised over every named token + each
alias variant.
* New ``isVoiceToggleKey`` cases for space (ch-based match), enter
(``key.return``), tab, escape, backspace, delete, including
modifier-mismatch negatives.
* ``formatVoiceRecordKey`` renders named keys in title case
(``Ctrl+Space``, ``Ctrl+Enter``).
* Existing fall-back-to-Ctrl+B contract preserved for empty input
AND unrecognised multi-char tokens.
Full TUI suite: 559/559 pass; ``tsc --noEmit`` clean.
Refs #18994 (round-1 review feedback)
Co-authored-by: asheriif <ahmedsherif95@gmail.com>
* test(tui): assert voice.toggle returns configured record_key
Salvage the backend regression from #19339 — asserts ``voice.toggle``
action=on AND action=status responses carry the configured
``voice.record_key`` end-to-end through ``_load_cfg()``. Keeps the
CLI→TUI parity contract visible in the Python test suite alongside
the existing frontend parser/matcher/formatter coverage from #19028.
* fix(tui): address Copilot review on #19835 voice.record_key wiring
Five tightenings on the parser + matcher + hydration surface, all
caught by the Copilot review on the PR — each one turns a silent
false-fire or display/binding skew into a deterministic behaviour.
* **isVoiceToggleKey ctrl branch was too permissive for named keys.**
The doc-default macOS Cmd+B muscle-memory fallback
(``isActionMod(key)`` on top of ``key.ctrl``) fired for every
configured key, so bare Esc — which hermes-ink reports with
``key.meta`` on some macOS terminals — triggered ``ctrl+escape``,
and Alt+Space / Alt+Tab triggered ``ctrl+space`` / ``ctrl+tab``.
Gate the fallback to the literal ``ctrl+b`` binding so any custom
chord requires the real Ctrl bit.
* **Alt branch guarded against Ctrl/Cmd co-press.** Without this,
Ctrl+Alt+<letter> and Cmd+Alt+<letter> also fired ``alt+<letter>``.
* **Dropped the ``meta`` modifier variant and its alias.** In
hermes-ink ``key.meta`` is Alt on xterm-style terminals and Cmd on
legacy macOS ones, so a literal ``meta+b`` config displayed as
``Cmd+B`` while matching Alt+B — exactly the kind of false
shortcut the PR was meant to remove. ``cmd`` / ``command`` now
collapse onto ``super`` (kitty-style ``key.super``, with a macOS
``key.meta`` fallback) and render as ``Cmd+B``. Unknown modifier
tokens fall back to the documented Ctrl+B default rather than
silently coercing to Ctrl.
* **Slash-command display/binding skew.** ``/voice status`` and
``/voice on`` rendered from the fresh gateway ``record_key``
response, but ``useInputHandlers()`` still bound the old key
until the next 5s mtime poll. Thread ``setVoiceRecordKey``
through ``SlashHandlerContext.voice`` and push the parsed spec
into frontend state on every response so text and binding stay
consistent.
* **Test coverage for the two paths Copilot flagged.** Added
vitest coverage for (a) the three-case ``/voice`` slash output
in ``createSlashHandler.test.ts`` and (b) the
``applyDisplay → voice.record_key`` hydration + omit-setter
back-compat paths in ``useConfigSync.test.ts``. Plus regression
cases for every false-fire scenario above.
Suite: 575/575 green, tsc --noEmit clean.
* fix(tui): address Copilot round-2 review on #19835
Three tightenings on the surface introduced in the round-1 fix:
* **``/voice tts`` reset custom bindings to Ctrl+B.** The ``tts`` branch
of ``voice.toggle`` omitted ``record_key`` from its response, so the
frontend's ``r.record_key ?? 'ctrl+b'`` coerced a user's custom
binding back to the default on every TTS toggle. Two-sided fix:
the backend now includes ``record_key`` on the ``tts`` branch (parity
with ``status``/``on``/``off``), and the slash handler only pushes
frontend state when the response actually carries ``record_key`` —
belt-and-suspenders against any future branch forgetting to include
it.
* **``super+b`` / ``win+b`` / ``cmd+b`` displayed "Cmd+B" on Linux and
Windows.** ``formatVoiceRecordKey`` rendered ``mod === 'super'`` as
``Cmd`` universally, which told non-mac users the wrong modifier to
press even though ``isVoiceToggleKey`` matched the right event bits.
Gate the label to ``isMac`` so non-mac renders ``Super+B``.
* **``control+b`` / ``ctrl + b`` lost the macOS Cmd+B fallback.**
``_isDefaultVoiceKey`` keyed off ``parsed.raw`` — so
semantically-equal aliases of the documented default dropped into
the strict branch even though they bind Ctrl+B. Compare on the
parsed spec (mod + ch + named) instead.
Coverage added: Linux ``Super+B`` rendering (and macOS ``Cmd+B``),
``control+b`` / ``ctrl + b`` accepting the Cmd+B fallback on darwin,
``/voice tts`` without ``record_key`` not clobbering cached binding,
and a backend regression asserting every ``voice.toggle`` branch
carries the configured key.
Suite: 579/579 TUI vitest green, 2/2 backend voice tests green,
tsc --noEmit clean.
* fix(tui): address Copilot round-3 review on #19835
Three classes of robustness issue caught on the second pass — all
revolve around malformed YAML tipping ``parseVoiceRecordKey`` or
``_voice_record_key`` into a crash instead of the documented
fallback.
* **Parser crashed on non-string YAML scalars.** ``config.get full``
returns raw ``yaml.safe_load`` output, so ``voice.record_key: 1``
or ``voice.record_key: true`` in a hand-edited config would hit
``.trim()`` on a number/bool and throw, breaking startup and
every mtime re-apply. Accept ``unknown`` at the signature, guard
with ``typeof raw !== 'string'``, and fall back to the default.
* **Backend blew up on non-dict ``voice:``.** Same YAML hazard on
the gateway side: ``voice: true`` / ``voice: cmd+b`` left
``_load_cfg().get("voice")`` as a bool/str, so ``.get("record_key")``
raised AttributeError and took every ``voice.toggle`` branch down
with it. Centralised the lookup in a single
``_voice_record_key()`` helper that ``isinstance``-guards both
``voice`` and ``record_key`` and falls back to ``ctrl+b``.
* **Multi-modifier chords silently dropped extras.** The previous
validator only checked the first modifier token, so ``ctrl+alt+r``
silently parsed as ``ctrl+r`` and ``cmd+ctrl+b`` as ``super+b`` —
a typo bound a different shortcut than the user configured.
Reject multi-modifier spellings outright; the classic CLI only
supports single-modifier bindings via prompt_toolkit's ``c-x`` /
``a-x`` rewrite, so this matches CLI parity.
Coverage added:
* ``parseVoiceRecordKey`` fallback on ``1`` / ``true`` / ``null`` /
``undefined`` / ``{}``.
* ``parseVoiceRecordKey`` fallback on ``ctrl+alt+r`` /
``cmd+ctrl+b`` / ``alt+ctrl+space``.
* ``test_voice_toggle_handles_non_dict_voice_cfg`` exercises
every non-dict ``voice:`` shape (bool, str, None, int, list) and
asserts each falls back to ``record_key: 'ctrl+b'``.
Suite: 581/581 TUI vitest green, 3/3 backend voice tests green,
tsc --noEmit clean.
* fix(tui): address Copilot round-4 review on #19835
Four final corners of the voice.record_key surface:
* **Bare-char configs silently coerced to ``ctrl+<key>``.** A config
like ``voice.record_key: o`` / ``space`` / ``escape`` fell through
to the default ``mod = 'ctrl'`` and silently bound Ctrl+O, while
the classic CLI's prompt_toolkit would bind the raw key (no
rewrite) — so the two runtimes silently disagreed on what "o"
means. Require an explicit modifier; bare-char configs fall back
to the documented Ctrl+B default.
* **Reserved ctrl+<letter> bindings would never fire.**
``useInputHandlers()`` intercepts ``ctrl+c`` (interrupt),
``ctrl+d`` (quit), and ``ctrl+l`` (clear screen) before the voice
check runs, so those configs would be advertised in /voice
status but the advertised shortcut never actually triggers
push-to-talk. Added ``_RESERVED_CTRL_CHARS`` at parse time so
the user gets the documented default instead of a dead shortcut.
(``alt+c``, ``cmd+l``, etc. are not intercepted and stay usable.)
* **``_load_cfg()`` root itself may be a non-dict.**
``_voice_record_key()`` isinstance-guarded the ``voice`` subkey
but not the root — a malformed config.yaml that collapsed to a
scalar/list at the top level (``config.yaml: true`` or ``[]``)
would still raise on ``.get("voice")``. Added the top-level
guard too so every malformed shape falls back to ``ctrl+b``.
* **Stale header comment on ``isVoiceToggleKey``.** The doc-comment
still claimed "On macOS we additionally accept the platform
action modifier (Cmd) for the configured letter" even though the
implementation gates the Cmd fallback to the documented default
only. Rewrote to match.
Coverage added:
* ``parseVoiceRecordKey`` fallback on bare chars (``o``, ``b``,
``space``, ``escape``).
* ``parseVoiceRecordKey`` fallback on ``ctrl+c`` / ``ctrl+d`` /
``ctrl+l``; positive case for ``alt+c`` / ``cmd+l`` still usable.
* Backend ``test_voice_toggle_handles_non_dict_voice_cfg`` now
exercises 5 non-dict shapes at the YAML root too.
Suite: 583/583 TUI vitest green, 3/3 backend voice tests green,
tsc --noEmit clean.
* fix(tui): address Copilot round-5 review on #19835
Three follow-ups on the voice matcher's modifier + shift discipline:
* **``super`` branch falsely fired on Alt+<key> / bare Esc on macOS.**
``isVoiceToggleKey`` accepted ``isMac && key.meta`` as a Cmd
fallback for the ``super`` modifier — but hermes-ink sets
``key.meta`` for plain Alt/Option AND for bare Escape on some
macOS terminals. A ``cmd+b`` config silently fired on Alt+B;
``cmd+space`` on Alt+Space; ``cmd+escape`` on bare Esc. Drop the
fallback and require the literal ``key.super`` bit. Legacy-
terminal users who need Cmd should upgrade to a kitty-protocol
terminal or bind ``alt+X`` explicitly.
* **Shift bit was never checked.** The parser rejects multi-
modifier configs like ``ctrl+shift+tab``, but the runtime
matcher didn't check ``key.shift`` — so ``ctrl+tab`` also fired
on Ctrl+Shift+Tab and ``alt+enter`` on Alt+Shift+Enter.
Early-return on ``key.shift === true`` so the runtime only fires
the exact chord the user configured.
* **Test leaked ``HERMES_VOICE=1`` into later tests.**
``voice.toggle`` action=on writes to ``os.environ`` directly
(CLI parity, runtime-only flag); ``test_voice_toggle_returns_
configured_record_key`` dispatched action=on without letting
monkeypatch take ownership of the var first. Any later test
that read voice mode in the same Python process could inherit a
stale enabled state. Added ``monkeypatch.setenv("HERMES_VOICE",
"0")`` up front so monkeypatch restores the original value at
teardown.
Coverage added:
* ``cmd+b`` / ``cmd+space`` / ``cmd+escape`` do NOT fire on
``key.meta``-only events on darwin.
* ``ctrl+tab`` / ``alt+enter`` / ``ctrl+o`` reject matches when
``key.shift`` is held; sanity cases without Shift still fire.
Suite: 585/585 TUI vitest green, 3/3 backend voice tests green,
tsc --noEmit clean.
* fix(tui): address Copilot round-6 review on #19835
Three classes of modifier-discipline tightening + one config-surface
honesty fix:
* **Default ``ctrl+b`` Cmd fallback leaked Alt+B.** The default's
macOS Cmd+B muscle-memory path used ``isActionMod(key)``, which
returns ``key.meta || key.super`` on darwin. hermes-ink also
reports plain Alt as ``key.meta``, so Alt+B silently fired the
default binding. Replaced with strict ``isMac && key.super ===
true`` — kitty-style Cmd+B still works, Alt+B correctly
rejected. Legacy-terminal mac users (Terminal.app without
CSI-u) now get raw Ctrl+B only; the documented default still
works everywhere.
* **ctrl / super branches accepted extra modifier bits.** The
parser rejects multi-modifier configs like ``ctrl+alt+o``, but
the runtime matcher was permissive — ``ctrl+o`` fired on
Ctrl+Alt+O / Ctrl+Cmd+O, and ``super+b`` fired on Cmd+Alt+B /
Ctrl+Cmd+B. Added strict ``!key.alt && !key.meta && key.super
!== true`` on ctrl, and ``!key.ctrl && !key.alt && !key.meta``
on super, so the runtime only fires the exact chord the parser
would let you configure.
* **Dropped ``cmd`` / ``command`` aliases.** They parsed to
``super`` and rendered as ``Cmd+X``, but legacy macOS terminals
report Cmd as ``key.meta`` (same signal as Alt), so a
``cmd+o`` config was advertised as working but never actually
fired on Terminal.app-without-CSI-u. That recreated the
"displayed shortcut does not work" problem this PR was meant to
remove. Users who want the platform action modifier spell it
``super`` / ``win`` — that matches the unambiguous ``key.super``
bit, and kitty-style macOS terminals render it as ``Cmd+X`` via
platform-aware formatter.
Coverage updated:
* Default ctrl+b no longer fires on Alt+B via ``key.meta`` leak;
raw Ctrl+B and kitty-style Cmd+B still fire.
* ``ctrl+o`` rejects Ctrl+Alt+O / Ctrl+Cmd+O / Ctrl+Meta+O chords.
* ``super+b`` rejects Cmd+Alt+B / Cmd+Meta+B / Ctrl+Cmd+B chords.
* ``cmd+b`` / ``command+b`` / ``meta+b`` all fall back to the
documented default at parse time (joined the ambiguous-mac-mod
rejection class).
* Round-2 expectations that asserted ``cmd+b`` parsed as super
and accepted ``key.meta`` on darwin updated to reflect the new
stricter contract.
Suite: 588/588 TUI vitest green, 3/3 backend voice tests green,
tsc --noEmit clean.
* fix(tui): address Copilot follow-up on wire typing + escape precedence
Two follow-ups from the latest Copilot pass:
* **Config wire typing honesty (`gatewayTypes.ts`)**
`config.get full` forwards raw `yaml.safe_load()` output, so
`voice.record_key` can be any scalar/container when hand-edited.
Typing it as `string` suggests a normalized contract that the
backend does not guarantee and makes unsafe callers more likely.
Change `ConfigVoiceConfig.record_key` to `unknown` with an
explicit comment that callers must normalize at runtime.
* **Escape-based voice bindings were swallowed before voice check**
`useInputHandlers()` handled `key.escape` for queue-edit cancel and
selection clear before `isVoiceToggleKey(...)`, so configured
`ctrl+escape` / `alt+escape` / `super+escape` chords were advertised
but never toggled recording in those UI states.
Add an early escape+voice check before generic Esc handlers so
escape-based voice bindings win when configured, while plain Esc
behavior remains unchanged.
Also updated PR #19835 description text to remove stale cmd/command
alias claims and match the current parser contract.
* fix(tui): pass configured voice shortcut through TextInput layer
Thread the live parsed voiceRecordKey into TextInput so configured voice.record_key chords bubble to useInputHandlers instead of being consumed as editor input. This removes the last hardcoded Ctrl+B pass-through in the composer path while preserving existing global control chord behavior.
* fix(tui): require explicit alt bit for escape-based alt chords
Hermes-ink reports bare Escape as meta=true+escape=true on some terminals, so a configured alt+escape binding was firing on bare Esc. Require an explicit key.alt bit when the configured named key is escape so plain Esc stays plain Esc; kitty-style alt+escape still fires.
* fix(tui): harden voice.record + TextInput paste + super-mod reserved list
Three round-7 Copilot follow-ups on #19835:
- voice.record start handler used _load_cfg().get('voice', {}).get(...) without
shape checks, so malformed YAML (bool/scalar/list) returned 5025 instead of
using VAD defaults. Centralized _voice_cfg_dict() helper and type-guarded
silence_threshold/silence_duration with numeric fallbacks.
- TextInput pass-through check moved above paste/copy handling so configured
voice chords (ctrl+v / alt+v / cmd+v) beat the composer's paste/copy
defaults.
- parser now also rejects super+{c,d,l,v} — on macOS those are
copy/exit/clear/paste and would be advertised in /voice status but never
actually toggle recording.
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(tui): round-8 Copilot review — allow ctrl+x, gate super reservations to macOS, preserve voice key on transient RPC failure
Three round-8 Copilot follow-ups on #19835:
- Revert ctrl+x addition to _RESERVED_CTRL_CHARS (landed via Copilot Autofix
commit
|
||
|
|
109c3e468c |
fix(terminal): guard background process spawn against deleted cwd (#19933)
Follow-up to #19928 which fixed the foreground path in _run_bash. The background process spawn in process_registry.py had the same vulnerability: Popen(cwd=session.cwd) and PtyProcess.spawn(cwd=...) would raise FileNotFoundError if the directory was deleted. Apply _resolve_safe_cwd() at session creation time so both the PTY and pipe-mode Popen paths receive a validated cwd. |
||
|
|
9fa3a093f2 |
fix(local): test root as ancestor candidate; use real pipe for fake stdout
Address Copilot review on PR #17569: 1. _resolve_safe_cwd never tested the filesystem root because the loop exited when `os.path.dirname(parent) == parent`, which is true once `parent == '/'`. Restructure so the root is checked before the self-equal exit. Adds `test_returns_root_when_only_root_exists` — regression-guarded by reverting the loop and watching it fail. 2. The fake `Popen.stdout` was a `MagicMock`; `BaseEnvironment._wait_for_process` calls `proc.stdout.fileno()` then `select.select`/`os.read` against it, which raised `TypeError: fileno() returned a non-integer` (visible as a thread exception in test output) and could in theory read from an unrelated real fd. Hand `fake_popen` a real `os.pipe()` with the write end pre-closed so the drain loop sees EOF immediately. Helper records each fd so the test cleans up after itself. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9644b8ae67 |
fix(local): recover when persistent_shell cwd is deleted (#17558)
When a tool call deletes its own working directory (`cd /tmp/foo && rm -rf /tmp/foo`), the next `subprocess.Popen(args, cwd=self.cwd)` raised `FileNotFoundError: [Errno 2]` before bash even started — every subsequent terminal/file-tool call hit the same wedge until the gateway restarted. Fix in `LocalEnvironment._run_bash`: before handing `self.cwd` to Popen, resolve a safe alternative when the path is gone (walk up to the nearest existing ancestor, falling back to `tempfile.gettempdir()` only as a last resort). Log a warning so the recovery is visible — not silent — and update `self.cwd` so the next call doesn't repeat the message. Defense in depth in `LocalEnvironment._update_cwd`: only adopt the new cwd when it still exists as a directory. `pwd -P` from a deleted cwd can leave a stale value in the marker file; refusing to store a missing path keeps `self.cwd` valid by construction. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b8fb9270c4 |
refactor(cli): drop dead c-S-c key binding (follow-up to #19895) (#19919)
#19884 added a prompt_toolkit key binding for Ctrl+Shift+C to "prevent Hermes from intercepting the keystroke as an interrupt signal." #19895 then wrapped the binding in try/except after discovering it crashed startup with ValueError on every platform. Both PRs were based on a misreading of how terminal key events propagate: 1. Terminal emulators (GNOME Terminal, iTerm2, kitty, Windows Terminal, etc.) intercept Ctrl+Shift+C before the keystroke reaches the application's stdin. prompt_toolkit never sees it. The binding could never have intercepted anything. 2. prompt_toolkit's key spec parser doesn't recognise 'c-S-c' on any platform — the Shift modifier is meaningless on control-sequence keys. Verified: every prompt_toolkit version raises 'Invalid key: c-S-c' at registration time. The handler is dead code. Delete it and leave a comment explaining why no binding is needed here. Ctrl+Q alias (#19884's other addition) stays — that's a real prompt_toolkit key and a legitimate interrupt shortcut. Verified the CLI starts cleanly — key binding phase no longer raises and the subsequent chat flow reaches the provider setup check without error. |
||
|
|
56a78e74b2 |
feat(kanban-dashboard): sharper home-channel toggle contrast, drop → running action (#19916)
Follow-up polish to the kanban dashboard from #19864 and #19705. **Home-channel toggle contrast.** The `.hermes-kanban-home-sub--on` class previously used `color-mix(var(--color-ring) 14%, transparent)` which was nearly invisible on both the default teal and NERV themes — the on/off distinction relied almost entirely on the ✓ prefix glyph. Bump to 32% fill + full-opacity ring border + inner ring shadow + font-weight 600. Still theme-scoped (no hardcoded colors), but reads at a glance on both tested themes. **Drop the → running status action.** Since #19705, `PATCH /tasks/:id` rejects `status=running` with HTTP 400 — only the dispatcher's `claim_task` path legitimately enters that state (so the run row, claim lock, and worker PID are created atomically). The UI button was still present and produced a 400 on click, which is a confusing dead affordance. Remove it from `StatusActions`; add a comment pointing to #19535 so future editors know why it's missing. Live-tested on the default Hermes Teal theme. 53/53 kanban dashboard plugin tests still pass. |
||
|
|
429b8eceb4 |
fix(cli): guard c-S-c key binding with try/except to prevent startup crash (#19895)
PR #19884 added @kb.add('c-S-c') unconditionally. prompt_toolkit raises ValueError("Invalid key: c-S-c") during HermesCLI.__init__ on platforms where this key spec is not recognised — the process exits before reaching the prompt loop. Reported on macOS (#19894) and Linux (#19896) immediately after #19884 landed. Fix: wrap the registration in try/except ValueError so that startup continues cleanly on any platform/version that rejects the spec. Where the spec is accepted the binding is registered normally as a no-op, allowing the terminal to handle Ctrl+Shift+C natively as before. Fixes #19894 Fixes #19896 |
||
|
|
e493b1c482 |
docs(skill): add hyperframes inspect command to cli.md + SKILL.md
- references/cli.md: add Inspect step (5/7) to Workflow + dedicated `## inspect` section between validate and preview, covering --json/--samples/--at flags and the legacy `hyperframes layout` alias - SKILL.md: rename procedure step 7 to "Lint, validate, inspect, preview, render" with the full pipeline; explain inspect as the layout-side companion to validate (catches overflow / off-frame / occluded text issues that static lint can't see) - SKILL.md verification: lint + validate + inspect as a single combined pass - SKILL.md References list: include `inspect` in the cli.md command list Brings the optional skill in sync with hyperframes-oss main as of 2026-05-03 — `inspect` was added in heygen-com/hyperframes#480 (2026-04-25) and is documented as a real workflow step in skills/hyperframes-cli/SKILL.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
20859cc408 |
docs(skill): sync hyperframes skill with upstream changes
Pulls the hyperframes skill up to the latest state of heygen-com/hyperframes skill content. Opened 2026-04-17; upstream has shipped CLI, layout, and path changes since. - SKILL.md: promote the visual-style check to a proper HARD-GATE (DESIGN.md > named style > ask 3 questions, with the #333/#3b82f6/Roboto tells); expand Step 6 to cover audio-reactive (mandatory per-frame tl.call() sampling loop — a single long tween does NOT react to audio), caption exit guarantee (hard tl.set kill after group.end), marker highlighting, and scene transitions; add the animation-map script to Verification; link the new features.md. - references/cli.md: add capture and validate (both shipped commands, both referenced from the workflow but missing from the reference). Add --lang to tts with the voice-prefix auto-inference table and espeak-ng dependency note (heygen-com/hyperframes#351, 2026-04-20 — after this PR opened). - references/website-to-video.md: update all paths to the capture/ subfolder layout introduced in heygen-com/hyperframes#345 (capture/screenshots/, capture/assets/, capture/extracted/tokens.json). Old captured/ prefix was broken — agents following the skill were looking for files in wrong locations. - references/features.md (new): distilled coverage for captions (language rule, tone table, word grouping, fitTextFontSize, exit guarantee), TTS (multilingual phonemization, speed tuning), audio-reactive (data format, mapping table, sampling pattern), marker highlighting (highlight/circle/burst/scribble/sketchout), and transitions (energy/ mood tables, presets, shader-compatible CSS rules). Five topics the original PR didn't cover. |
||
|
|
50aabb9eb2 |
feat(skill): add hyperframes optional creative skill
Adds an optional creative skill that integrates HyperFrames, an HTML-based video rendering framework, as a sibling to manim-video. Complements manim's math-focused animation with motion-graphics, captioned narration, audio-reactive visuals, shader transitions, and website-to-video production. Scope: - optional-skills/creative/hyperframes/SKILL.md — entry point - references/composition.md — data-attr schema, timeline contract - references/cli.md — every npx hyperframes command - references/gsap.md — GSAP core API for compositions - references/website-to-video.md — 7-step capture-to-video workflow - references/troubleshooting.md — OpenClaw / Chromium 147 fix - scripts/setup.sh — idempotent one-time setup OpenClaw / Chromium 147 fix (hyperframes#294): Pinning hyperframes@>=0.4.2 (commit 4c72ba4 ships the HeadlessExperimental.beginFrame auto-detect + screenshot fallback). setup.sh pre-caches chrome-headless-shell so the fast BeginFrame path is preferred over system Chrome. The PRODUCER_FORCE_SCREENSHOT=true escape hatch is documented in troubleshooting.md and in SKILL.md Pitfalls. Placed under optional-skills/ (not bundled) per CONTRIBUTING.md guidance for heavyweight deps: requires Node.js >= 22, FFmpeg, and ~300 MB chrome-headless-shell download. |
||
|
|
8fabef9d35 |
fix(docs): register cron-script-only guide in sidebar (#19893)
PR #19709 added website/docs/guides/cron-script-only.md but never added the entry to website/sidebars.ts, which is explicitly enumerated (not autogenerated). Two consequences: 1. The guide didn't show up in the left-nav "Guides & Tutorials" list — users could only reach it via cross-links from other pages. 2. Landing on the guide page directly made the sidebar disappear entirely (Docusaurus treats unregistered docs as orphaned and renders them without their parent sidebar). Added 'guides/cron-script-only' next to 'guides/automate-with-cron' so it slots in alongside the other cron content. Verified with `npm run build`: no orphan warnings, no broken links, page builds with sidebar intact. No content change, docs only. |
||
|
|
81cd678291 |
fix(google-workspace): restore required_credential_files in SKILL.md (#16452)
PR #9931 ("feat(google-workspace): add --from flag for custom sender display name") accidentally removed the required_credential_files frontmatter block that tells hermes to bind-mount google_token.json and google_client_secret.json into Docker and Modal remote terminals before running setup.py. Without this header the credential files are never registered in the session-scoped ContextVar, so get_credential_file_mounts() returns an empty list at container creation time and the OAuth files are invisible inside the sandbox. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
60b143e9df |
fix(tui_gateway): guard sys.path against local package shadowing (#15989)
When the TUI backend (tui_gateway/entry.py) is spawned by Node.js with the user's CWD containing a local utils/ directory, that directory shadows the installed utils module, causing ImportError in run_agent and hermes_cli. Strip '' and '.' from sys.path and prepend HERMES_PYTHON_SRC_ROOT (already set by hermes_cli before spawning the subprocess) so installed packages always win over CWD artifacts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
645a2f482d | fix(cli): fix shortcut config conflict in hermes_cli | ||
|
|
a919269eb5 |
fix(skills/email/himalaya): document v1.2.0 folder.aliases syntax
The bundled himalaya skill documented folder aliases using a stale
TOML schema (`[accounts.NAME.folder.alias]`, singular) that himalaya
v1.2.0 silently ignores. The TOML parses without error, but the
alias resolver never reads the sub-section — every lookup then falls
through to the canonical folder name.
Source: in `pimalaya/core` (the `email-lib` crate himalaya v1.2.0
depends on, currently v0.27.0), `email/src/folder/config.rs` defines
`FolderConfig { aliases: Option<HashMap<String, String>>, ... }`
(plural, no `#[serde(rename)]`/`alias` aliases, no
`deny_unknown_fields`), and `account/config/mod.rs::get_folder_alias`
returns the input verbatim when no alias is found. So the singular
`alias` key deserializes to nothing and lookups silently fall
through.
On Gmail (where `sent` resolves to `[Gmail]/Sent Mail`, not `Sent`)
this means save-to-Sent fails *after* SMTP delivery already
succeeded, and `himalaya message send` exits non-zero. Any caller
(agent, script, user) that retries on that exit code will re-run
the entire send — including SMTP — producing duplicate emails to
recipients. Silent ignore + caller-level retry is significantly
worse than a config that just doesn't work.
This commit updates SKILL.md and references/configuration.md to the
v1.2.0 `folder.aliases.X` syntax (plural, dotted keys, directly
under the account section), adds a Gmail-specific block with the
`[Gmail]/Sent Mail`-style mapping, and adds notes on the failure
mode so future readers don't hit the same trap. SKILL.md version
bumped 1.0.0 → 1.1.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
9cda237bb1 |
docs(cron): lead with agent-driven setup for no-agent mode (#19871)
The shipped no-agent docs introduced the feature via CLI first and
mentioned the chat path as a two-line afterthought. That buries the
actual value prop: the cronjob tool exposes no_agent directly to the
agent, so a user can describe a watchdog in plain language and Hermes
wires up the script + schedule + delivery without anyone opening an
editor.
Changes:
* cron-script-only.md: promote 'Create One from Chat' above
'Create One from the CLI', flesh it out with a worked transcript
(the actual tool calls the agent makes), add subsections covering
'what the agent decides for you' (when to pick no_agent=True vs
LLM mode) and 'managing watchdogs from chat' (pause/resume/edit/
remove all agent-accessible).
* user-guide/features/cron.md:
- Add 'no-agent mode' to the top-level feature list with a cross-
link, plus a sentence up top making it clear everything is
agent-accessible through the cronjob tool.
- Add 'The agent sets these up for you' subsection to the no-agent
section showing the exact tool call shape.
* automate-with-cron.md: tighten the existing tip box to mention the
agent-driven path, not just CLI scheduling.
No behavior change — docs only.
|
||
|
|
eadf34633e |
fix(models): strip :cloud/-cloud suffix from models.dev Ollama Cloud IDs
models.dev appends :cloud and -cloud suffixes to Ollama Cloud model IDs (e.g. kimi-k2.6:cloud, qwen3-coder:480b-cloud) that the live Ollama Cloud API does not use. Without normalisation, these suffixed IDs bypass the dedup check and appear alongside the correct clean IDs, causing 400/404 errors when users select them in /model or hermes model. Add _strip_ollama_cloud_suffix() and apply it to mdev entries before the dedup merge in fetch_ollama_cloud_models() so all model IDs stored in the disk cache use the canonical form the API accepts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c050ee6573 | fix(file_ops): resolve search_files path/line collision for hyphenated numeric filenames | ||
|
|
fbc477df71 |
fix(run_agent): acquire lock in IterationBudget.used property
The `used` property was reading `self._used` without holding the lock, while `consume()`, `refund()`, and `remaining` all properly acquire `self._lock` before accessing `_used`. This means a concurrent call to `used` during `consume()` or `refund()` could observe a partially- updated value, leading to incorrect iteration budget metrics reported to the gateway, or in extreme cases a ValueError from CPython's list implementation when the internal array resizes during iteration. Fix: acquire the lock in `used` just like `remaining` does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
64ad7dec0d | fix(file-ops): allow file search in hidden roots |