New desktop_ui tool: the agent proposes an MCP server (install/enable/
authorize + a one-line reason) and blocks on mcp.setup.request until the
renderer's consent card answers mcp.setup.respond with the outcome
(installed/enabled/authorized/declined/unanswered/error). Same lifecycle
as clarify: 10-min timeout, allow_expired late answers, tool lifecycle
events forced on so the card mounts even with tool progress off. Desktop
prompt hint steers the model to the tool instead of hand-editing config;
every other surface keeps the schema out and is pointed at hermes mcp
install.
Two follow-ups from live Windows sessions:
1. agent/prompt_builder.py: extend the Windows shell hint with the
native-binary path rule. Hermes disables MSYS path conversion for its
bash, so agents passing /c/Users/... or /tmp/... to NATIVE programs
(git -C, node, python, rg) hit 'cannot change to' / 'not found' while
the same path works in bash builtins — observed repeatedly in a live
session (git -C failures, git apply /tmp/x.patch failures). The hint
now says: forward-slash native form (C:/Users/x) for native tools,
$LOCALAPPDATA/Temp over /tmp for scratch files native tools read.
(/tmp is pure model habit from Linux training data — nothing
instructs it — so the hint is the right layer.)
2. tests: pin LF/CRLF preservation through write_file and patch_replace.
A live session saw a repo-LF file come back full-CRLF after an edit
(4699-line diff churn); not reproducible through current tool APIs,
so pin the correct behavior — LF files stay LF, CRLF files stay CRLF,
no mixed endings — to catch any regression on the Windows write path.
Sweep of open Windows issues affecting day-to-day agent operation
(explicitly excluding install/setup and locale classes):
- hermes_cli/_subprocess_compat.py: new split_command_line() — Windows-
safe command-line tokenizer (posix=False + quote stripping) so
backslash paths survive. POSIX behavior unchanged (plain shlex.split).
- hermes_cli/console_engine.py (#83934): console commands like
'sessions export C:\Users\me\out.jsonl' no longer silently mangle the
path into a relative filename in the cwd.
- agent/shell_hooks.py (#78293): hook commands with backslash paths now
spawn, resolve their script path, and pass hooks doctor instead of
reporting 'not executable'. All three shlex sites routed through the
shared splitter.
- agent/prompt_builder.py (#51755): system prompt now reports
Windows (11) on Windows 11 — platform.release() returns 10 for both;
distinguish via sys.getwindowsversion().build >= 22000.
- hermes_cli/commands.py (#42016): @ autocomplete no longer crashes the
prompt_toolkit event loop when rg emits a path on a different mount
(device paths \.\nul, other drive letters) — relpath ValueError is
skipped per-entry.
- tools/browser_use_cli.py (#83884): screenshot-path detection now
matches Windows drive-letter paths (C:\... and C:/...) in addition to
POSIX; Browser Use screenshots attach on Windows.
- tools/skills_hub.py + tools/skills_guard.py (#62310): the two 'MUST
stay symmetric' skill content hashes actually agree on Windows now.
Bundle keys are normalized to POSIX separators before hashing, and the
disk digest sorts by rel-posix STRING (case-sensitive) instead of Path
objects (case-insensitive on Windows). Fixes permanent false-positive
update_available for every installed skill.
Tests: tests/tools/test_windows_agent_loop_papercuts.py — 16 cases
covering each fix, including a disk-vs-bundle hash symmetry check built
with native Windows separators and a mixed-case filename.
* fix: warn agents off driving interactive console TUIs via pty on Windows
Driving 'gh auth login' (and other survey-style console TUIs) through a
pty background process on Windows silently hangs: these programs read
Win32 console key events via ReadConsoleInput, not the stdin byte
stream, so Enter keypresses submitted over process stdin never register.
The agent-visible symptom is a prompt frozen at 'Press Enter to open
browser...' while the user sees nothing, and a turn interrupt then kills
the process, invalidating any device code the user already entered on
github.com.
Two guidance fixes, both proven in a live session on Windows 10:
- agent/prompt_builder.py: extend _WINDOWS_BASH_SHELL_HINT to steer
agents toward non-interactive paths (flags, --with-token, config
files, curl-polled OAuth device flow) instead of answering console
prompts programmatically.
- skills/github/github-auth: document the pitfall and add the manual
OAuth device-flow procedure (curl against gh's public client_id,
poll for the token, finish with 'gh auth login --with-token'), which
succeeded first try after two interactive attempts hung.
* fix: send CRLF for Enter on Windows PTY submit; correct root cause in guidance
Review feedback (helix4u) was right on both counts:
1. Root cause correction. gh's 'Press Enter to open browser' prompt is
waitForEnter -> bufio.Scanner reading stdin, not a survey/console-API
prompt. The real bug is ours: submit_stdin appended a bare \n, and
through pywinpty/ConPTY a lone \n is not delivered as a line
terminator, so the child's blocking line read never returns. Verified
empirically against pywinpty 2.0.15 with a readline() child:
\n -> hang, \r -> line delivered, \r\n -> line delivered.
Fix: submit_stdin now appends \r\n for Windows PTY sessions (POSIX
PTYs and Popen pipes keep \n). Windows-only regression tests cover
the PTY and pipe branches.
2. Prompt hint rewritten: instead of claiming Windows console TUIs
cannot be driven, it now says to use process(submit) rather than raw
writes with bare \n, and to prefer non-interactive paths when a CLI
offers one.
3. Skill device flow rewritten as an executable script: parses the
device-code response, polls per the returned interval, handles
authorization_pending / slow_down (+5s per GitHub docs) /
expired_token / access_denied / unexpected responses, pipes the token
straight into gh without echoing it, and drops the undocumented
workflow scope (repo,read:org,gist is the documented minimum for
gh auth login --with-token). The pitfall note is narrowed to the
reproduced condition.
Adds the comment-based hotspot convention (no new primitives) across three
guidance surfaces:
- KANBAN_GUIDANCE worker lifecycle: new step 7 — when a file keeps colliding
with siblings or appears in other cards' recent comments, leave a
'hotspot: <path> — <reason>' kanban_comment and repeat it in completion
metadata so the orchestrator can decompose the file first.
- kanban.md (en + zh-Hans): 'Collision hotspots in parallel campaigns'
subsection — the convention, the orchestrator response (2+ flags on one
path => dedicated decomposition card before queuing more work touching
it), and the cross-link to merge-reconciler for conflicts that already
happened.
- merge-reconciler SKILL.md Pitfalls: repeated conflicts on the same file
across rounds are a hotspot signal — flag for decomposition rather than
serially reconciling.
Live-verified: guidance renders once via real import (6152 chars); hotspot
comment round-trips through add_comment -> list_comments -> worker context
on an isolated HERMES_KANBAN_DB; kanban tools, review-surfaces, and
merge-reconciler skill tests green (45 passed).
Design decisions belong to the orchestrator: decide naming schemes,
schemas, file formats, and API shapes before fanning out; never let two
subtree cards decide the same question; stamp every decision into each
dependent card body since workers cannot see sibling context. Mirrored
in the kanban docs (en + zh-Hans) with an exporter/importer worked
example, and bounded KANBAN_GUIDANCE size with an invariant test.
Add a non-terminal "review" status so a worker that finished implementation
can hand off for human review without abusing kanban_block. The old
kanban_block(reason="review-required: ...") convention routed the handoff
through the unblock-loop breaker, so a normal review -> changes -> review
cycle was falsely escalated to triage.
- kanban_db: request_review (running/ready -> review, non-block, emits
review_requested), reopen_review_task (review -> ready/todo, review_reopened),
complete_task accepts review -> done, and a review_dispatch gate (default off,
shared by the dispatcher loop and the gateway health probe).
- kanban_request_review worker tool + `request-review` / `reopen-review` CLI
verbs; tool wired through toolsets, EXPOSED_TOOLS, _POLISHED_TOOLS.
- Gateway notifier wakes the origin subscriber on review_requested and
block_loop_detected; the subscription survives until done/archived, so every
review cycle re-notifies.
- Dashboard PATCH + bulk route the review transitions (request_review /
reopen_review_task) and render the review column.
- goals.py goal-loop and KANBAN_GUIDANCE recognize review as a terminator.
- Docs (reference tables, user guide, AGENTS.md, zh-Hans mirrors) + tests.
needs_input / failed are unchanged: they still route through kanban_block,
still count toward block_recurrences, and still escalate to triage.
The HUD-mode note tells the model that an unqualified "this" means the app
behind the strip. It says nothing about the app that was behind it a minute
ago, and the user drags the strip from app to app mid-thought: parked over
Spotify, "pause that and play X here" is one request spanning two apps, and
only the second half has a window under it.
Those earlier windows are already in context as read_window_below results, so
the note only has to say they still count. Without it the latest window reads
as the only one and half the request is silently dropped.
No new tool names, so the existing gating tests cover it unchanged.
In HUD mode Hermes is a strip over the app the user is actually working
in, so "what's under you?" or "look up the weather" is almost always
about that app — but the agent had no way to know it was floating, and
answered from its own browser and panes instead.
The desktop tags a HUD submit with `surface: 'hud'` and the gateway turns
that into a per-turn note pointing at read_window_below, and at carrying
the work out in the app underneath. It rides the model-bound message
beside the reaction and speech-interrupted notes rather than the system
prompt: one session can be driven from the app window on one turn and the
HUD on the next, and the system prompt has to stay byte-stable.
Every tool the note names is checked against the agent's own schema
first, so a session without computer_use or read_window_below is never
pointed at a tool it cannot call.
Commit b45a217e0 gated the TELEGRAM_RICH_MESSAGES_HINT extension behind
a config read at the top-level ``platforms.telegram.extra.rich_messages``
key, but the Telegram adapter reads the same setting from the canonical
``gateway.platforms.telegram.extra.rich_messages`` path. When users set
the setting in the canonical location (the only one documented), the
lookup returned None and the extension never fired — the model degraded
pipe tables to bullet lists, task lists to plain dashes, and never
produced <details> blocks or block math.
Fix: merge both ``gateway.platforms.telegram.extra`` and the top-level
``platforms.telegram.extra`` with the same precedence the adapter uses
(top-level leaf wins), so config-wizard writes and dashboard-setup keys
are visible alongside the canonical gateway location. Narrow the
except-guard to ImportError so real config-stack failures surface.
Port from nanocoai/nanoclaw#2748: Docker's built-in 64 MB /dev/shm silently
breaks shared-memory-hungry workloads inside the sandbox — Chromium/Playwright
renderers crash tabs, and PyTorch DataLoader workers die with 'bus error' /
'insufficient shared memory'. tmpfs is lazily allocated, so the higher ceiling
costs nothing until actually used, and usage still counts against the
container's --memory cgroup limit.
- tools/environments/docker.py: --shm-size 1g in resource args (not
cgroup-gated; tmpfs mount option). Skipped when docker_extra_args already
sets --shm-size, or when configured empty/'0' (Docker default).
- terminal.docker_shm_size config key (DEFAULT_CONFIG + all three
config->TERMINAL_DOCKER_SHM_SIZE env bridges: CLI, gateway, config.py map)
- tests: default emit, custom value, opt-out, extra_args precedence,
helper edge cases (sabotage-verified: default/custom tests fail without
the emit)
Salvaged from #56085 (@Stoltemberg), rebased onto current main: sites
main had already converted (credential_pool, auxiliary_client MoA
paths, model_metadata, moa_loop, agent_runtime_helpers) resolve to
main's versions; the remaining ~29 read-only sites across 16 agent/
files swap to the no-deepcopy readonly loader (~135us saved per call).
Full per-site mutation audit performed (every enclosing function read,
escapes traced): 23 SAFE, 5 ESCAPES with read-only consumers, 1 UNSAFE
path (init_agent -> get_compatible_custom_providers -> normalizer
in-place alias writes) fixed by the preceding no-mutate commits, which
make the normalizer copy-safe for ALL callers.
Resolves the PR's conflict with main (2252 commits). Two conflicts, both
"each side added an independent block in the same place" — kept both:
- gateway/run.py — the housekeeping loop. This branch adds the Skill Sync
pulls inside the CURATOR_EVERY branch (12-space indent); main adds a
stale-session auto-archive as a sibling `if` at loop level (8-space).
Different scopes, so the naive union would have mis-nested the archive
block into the curator branch; kept each at its own indent level.
- tools/skill_manager_tool.py — the _edit_skill result dict. This branch
appends the org auto-propose note; main appends
_add_description_prompt_preview(). Independent, order-insensitive.
No behaviour dropped from either side.
Verified: 3552 passed / 0 failed across 63 suites (scope regenerated to
include main's new maybe_auto_archive / _add_description_prompt_preview
consumers) via scripts/run_tests.sh. `hermes sync` and `hermes sync status`
still work against a live token, resolving the production plane default.
The Pyright Optional-parameter warnings in skill_manager_tool.py are
pre-existing on main (`content: str = None` etc.), not introduced here.
Implements the agreed design (2026-07-23): org skills are FIRST-CLASS (bare
names) with three hard companions.
1. TOKEN-GATED RESOLUTION: _org/<org_id>/ mirrors resolve ONLY while marked
active. pull_org_skills (which runs only after the token's org_id+org_role
verified) writes _org/.active_org; discovery (iter_skill_index_files,
_find_skill_dir, snapshot manifest) prunes every other mirror. Leave the
org (verified personal token in maybe_pull_org_skills) => marker cleared
=> org skills stop resolving; offline => marker untouched (grace).
Snapshot manifest includes the marker so org switches invalidate the
prompt snapshot; _SKILLS_SNAPSHOT_VERSION bumped to 2.
2. FAIL-LOUD COLLISIONS: listing pass unified across snapshot/scan paths;
a personal/org name clash flags BOTH entries '[name collision — load via
category path]' — neither side silently wins (personal-wins = silent
divergence from the org set; org-wins = shadowed personal work).
skill_view's existing multi-candidate refusal already rejects the
ambiguous bare name.
3. PROVENANCE: org entries list under an org:<org_id> category with
'[org-shared: by <author>]' tags; skill_view prepends a load-time header
(org, author, as-of + read-only/fork-and-propose guidance) INTO the
content the model consumes, plus an org_provenance result field. Author
comes from the pull-time .org-provenance.json sidecar (HEAD commit author
— token-verified at push by the plane's author_mismatch guard, gg #166).
4. READ-ONLY MIRROR: skill_manage patch/edit/delete/write_file refuse org-
mirror targets with fork-and-propose guidance; org skills are curation-
exempt (is_curation_eligible False — the org HEAD owns them).
Tests: 11 new (tests/agent/test_org_skill_namespace.py) covering gating,
stale-mirror pruning, org-switch flip, snapshot provenance, listing labels,
both-sides collision flags, read-only guard, curation exemption; 448 green
across skills/prompt/sync suites. Live E2E (real modules, temp HERMES_HOME,
mock plane): merge -> pull -> marker+sidecar -> labeled listing -> exactly-2
collision flags -> load-time header -> edit refused -> marker cleared =>
org skills vanish, personal survive.
Surgical reapply of the marker-alignment and dedup-guidance halves of
PR #44166 commits 52341f6ca3 / 3d8a31432d / ae07412e4b onto current main:
- the [SKILL_PRUNED: ...] marker embeds the exact reload call
skill_view(name='<skill>') so the model can act without guessing
- SKILLS_GUIDANCE Skill Safety Rule gains rule 4 (DEDUP): after one
reload, remaining markers for the same skill are historical artifacts
Fixes#32106 (part).
Correction to the previous commit (PR #68402): the claim that api_server
never intercepts MEDIA: tags is inaccurate on current main.
_resolve_media_to_data_urls() (gateway/platforms/api_server.py) DOES
inline image MEDIA: tags (<=5MB, image extensions only) as base64 data
URLs on the four main endpoints (_handle_session_chat,
_handle_session_chat_stream, _handle_chat_completions, _handle_responses).
The real gaps elphamale's PR points at are narrower:
- the /v1/runs output path (_handle_runs) never calls the resolver;
- non-image filetypes are never resolved anywhere (_MEDIA_IMG_EXT is
image-only).
Reword the hint to teach both halves: images via MEDIA: work on the
chat/completions/responses endpoints; non-image files and anything on
the runs endpoint must fall back to plain file paths in the response
text. Update the test to pin the scoped guidance instead of a blanket
prohibition.
Every PLATFORM_HINTS entry for a messaging platform (Telegram, WhatsApp,
Discord, Slack, Signal, WebUI, desktop) teaches the model the MEDIA:/path
convention because an interception mechanism actually resolves it there
(native attachment delivery, or a validated/inlined data URL). The cli
entry, which has no such mechanism, explicitly tells the model NOT to use
it and to state the path in plain text instead.
The api_server entry had neither instruction. Its /v1/runs handler never
routes the final response through any MEDIA: resolver (confirmed against
source: none of the four call sites of the api_server module's media-tag
resolver are inside its runs-endpoint handler), so a MEDIA:/path tag there
renders as inert literal text in the API response — exposing a raw host
filesystem path to the caller with no delivery ever taking place. Nothing
platform-specific told the model not to use a convention it's correctly
taught for several sibling platforms in this same dict, so the general
cross-platform habit could surface here too, unlike cli where an explicit
prohibition already closes the gap.
Mirrors cli's prohibition, adapted for api_server's actual constraint: no
"state the path in plain text" fallback, since a typical API caller has no
filesystem access to the host at all. Points at "a registered file-delivery
tool" generically rather than naming any specific tool, since api_server
toolsets are deployment-defined.
Follow-up to the salvaged #52552 and #53083 commits:
- Rework the Matrix PLATFORM_HINTS entry around what the adapter actually
emits: headings, numbered lists, blockquotes, strikethrough-free markdown
all render (the adapter converts them to sanctioned HTML). Keep the
genuinely valuable guidance: no Markdown tables (Element X / Beeper /
mobile clients don't render HTML tables — cells collapse into one line),
no spoilers/checkboxes/~~strikethrough~~ (not converted by
python-markdown), prefer descriptive link text.
- Regression test: hint must steer models away from tables.
- Fix test_long_response_split_preserves_thread_context to derive its
payload size from the adapter's configurable limit instead of assuming
the old hardcoded 4000.
- Document matrix.max_message_length in the Matrix docs page.
- Contributor mapping for RKelln.
- Replace outdated brief hint with comprehensive, tested rules
- Document exactly what renders on Matrix and what does not
- Add critical linebreak semantics (two trailing spaces = soft break)
- Add link formatting guidance (descriptive text, never bare URLs)
Hermes' computer_use wrapper dropped cua-driver's structured action verdicts,
exposed no delivery_mode, and injected background-only guidance — so the agent
reported unverified no-ops as success and concluded cua-driver 'cannot drive'
Electron/Chromium surfaces (observed live on tldraw offline). Fixes#67052.
Phase A — preserve the result contract:
- ActionResult carries verified/effect/escalation/path/degraded/code/delivery_mode
- CuaDriverBackend._action() reads structuredContent (was data-only); a helper
normalizes it, additive and None-safe on old drivers
- _text_response surfaces the fields additively (ok stays transport-only)
Phase B — bounded, model-reachable foreground:
- delivery_mode (background|foreground) + bring_to_front on the schema, dispatcher,
ABC, and all input methods
- foreground is capability-gated (input.delivery_mode); old drivers get a
structured foreground_unsupported refusal, never a silent background downgrade
- no automatic/hidden foreground retry — the model selects it from the signal
Phase C — guidance + isolation:
- system prompt (prompt_builder) and bundled skills/computer-use/SKILL.md go from
background-ONLY to background-FIRST, teaching the AX→PX→foreground ladder driven
by returned effect/escalation, not predicted from the app being Electron
- foreground approval scoped by (action, delivery_mode): a background approval
never silently authorizes foreground
- approval state keyed per session_id so concurrent gateway runs don't leak unlocks
Tests: tests/tools/test_computer_use_delivery_ladder.py (15) cover confirmed/
unverifiable/suspected_noop/degraded/old-driver verdicts, delivery_mode gating +
foreground_unsupported, and session-scoped foreground approval. Existing 265
computer_use tests still green.
Live E2E (real cua-driver 0.8.3 + tldraw offline on Linux/X11): a background click
returned effect='unverifiable'/path='ax' (no fabricated success), and a foreground
request returned code='foreground_unsupported' — correct on a driver that predates
the input.delivery_mode capability.
The previous commit fixes the canonical agent/skill_utils.parse_frontmatter.
Six more modules reimplement the '---' fence check locally and had the
same bug:
- tools/skill_manager_tool.py _validate_frontmatter — rejected BOM'd
skill_manage create/edit content outright
- tools/skills_hub.py GitHubSource._parse_frontmatter_quick and
OptionalSkillSource._parse_frontmatter — hub browse/install metadata
- hermes_cli/skills_hub.py — local skill install validation
- gateway/run.py — skill slug discovery for disabled-skill hints
- agent/prompt_builder.py _strip_yaml_frontmatter — BOM'd context files
(AGENTS.md) leaked raw frontmatter into the system prompt
- tools/blueprints.py _split_frontmatter — str.lstrip() does not strip
U+FEFF (not whitespace), so the existing lstrip never covered it
Sibling-surface regression tests added.
Bug class also fixed upstream in cline/cline#12218 (found by the weekly
Cline PR scout).
PR #36019 documented the attachment tools in the kanban-worker skill,
but main removed that skill in #50473 and folded its content into the
KANBAN_GUIDANCE prompt block. Land the same guidance there instead so
every dispatcher-spawned worker sees it.
Follow-up on the salvaged #64611 commit: the original guard blocked the
install tree unconditionally, which would have broken the legitimate
'developing Hermes from a source clone' CLI flow (launching hermes inside
the repo and getting its AGENTS.md as project context).
Refined policy:
- resolve_context_cwd(): validates configured paths (missing dir -> None +
warning) but honors an EXPLICIT install-tree cwd verbatim — deliberate
user choice.
- build_context_files_prompt(): blocks only the cwd=None -> os.getcwd()
FALLBACK into the install tree, with a new allow_install_tree_fallback
param. system_prompt.py passes it for platform cli/tui (launch dir is
the user's real shell cwd there); desktop/gateway surfaces keep the
guard (their fallback dir is self-spawned, never user-picked).
- Warning log names the resolved dir and the terminal.cwd remedy.
E2E-verified all five scenarios: desktop fallback blocked, in-tree CLI dev
keeps AGENTS.md, explicit install-tree cwd honored, invalid TERMINAL_CWD
falls to None then blocked, normal workspace loads.
The platform hint in PLATFORM_HINTS['telegram'] always encouraged rich
Markdown constructs (tables, task lists, math, collapsible details) even
when rich_messages: false (the default). This caused the agent to produce
formatting that MarkdownV2 cannot render, especially broken on Telegram Web.
Split the hint into a base hint (MarkdownV2-compatible) and a
TELEGRAM_RICH_MESSAGES_HINT extension. The extension is conditionally
appended in system_prompt.py only when
platforms.telegram.extra.rich_messages is true.
Fixes#57122
Fixes#3356
Build the skills snapshot manifest in one directory walk, avoid importing gateway session context during CLI prompt startup, and reuse direct platform-list matching for snapshot entries.
(cherry picked from commit 1a64c2ed04)
The desktop app's chat panel reuses tui_gateway as its backend, so every chat session was stamped platform="tui". That made the agent read terminal-specific platform guidance while running in the graphical desktop chat surface.
Resolve the misclassification at its source: tui_gateway now picks platform="desktop" when HERMES_DESKTOP=1 and HERMES_DESKTOP_TERMINAL is unset, and keeps platform="tui" for the embedded terminal pane and standalone TUI. Add a PLATFORM_HINTS["desktop"] entry describing the actual chat surface (full GFM markdown, MEDIA: intercept, inline images). Move the embedded-pane clarifier to the platform-hint resolution site so it appends only to the tui hint under HERMES_DESKTOP_TERMINAL=1. Delete the now-dead desktop-hint block from build_environment_hints() that competed with the platform hint.
Standalone TUI sessions produce byte-identical prompts as before; the new desktop hint and clarifier are assembled once per session in the stable tier, so prompt caching is preserved.
_find_hermes_md walks parent directories looking for .hermes.md/HERMES.md,
stopping at the git root. But when there is no git repo (_find_git_root
returns None), the stop guard never fires and the loop walks all the way
to /. On shared systems (CI runners, multi-tenant servers), a .hermes.md
planted at /tmp, /home, or / would be loaded into the system prompt of any
agent session not inside a git repo — a cross-user prompt-injection vector.
Fix: when there is no git root, only check cwd; do not walk parents.
Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
The system-prompt backend probe imported a nonexistent symbol —
`from tools.environments import get_environment` — which always raised
ImportError: cannot import name 'get_environment'. The exception is caught
and only drops the live backend description to a static fallback, so it is
cosmetic, but it broke the live OS/user/cwd probe for every non-local
backend (docker/singularity/modal/daytona/ssh).
The real factory is `_create_environment` in tools.terminal_tool. Build the
environment the same way the live terminal path does (select backend image,
assemble ssh/container config from _get_env_config()), then run the probe.
Note: this does NOT affect tool loading — tool selection runs each tool's
check_fn and never consults this probe. Regression from #52147 (2026-06-25).
Closes#53667 (probe import); the 'cronjob-only' tool-collapse symptom is
not reproducible — tool selection has no probe dependency and memory's
check_fn is unconditionally True.
The 'whatsapp' and 'signal' PLATFORM_HINTS told the agent 'Please do not
use markdown as it does not render' — factually wrong. Both adapters
actively convert markdown to native formatting:
- whatsapp_common.format_message(): **bold**, ~~strike~~, # headers,
links, code blocks -> WhatsApp native syntax
- signal_format.markdown_to_signal(): same conversions via bodyRanges,
plus '- item' / '* item' bullets -> '• ' Unicode bullets
The wrong hint made the agent strip bullets and bold the adapter would
have rendered (#12224). Rewrote both hints to mirror whatsapp_cloud:
markdown is auto-converted, bullet lists work, tables are not supported.
Added a contract test asserting markdown-converting platforms never
forbid markdown in their hint.
deliver=origin (or omitted) from a TUI or classic-CLI session produces a
job with origin=null, because those sessions never populate the
HERMES_SESSION_PLATFORM/CHAT_ID context vars that _origin_from_env reads.
The scheduler then resolves no delivery target and skips delivery — the
job runs and saves output to last_output, but nothing reaches the user
and they only find out by polling cronjob(action='list') (#51568).
This is by design (local sessions have no live-delivery channel), so the
fix surfaces it instead of silently dropping the intent:
- cronjob create now appends an informational notice to its result when
a created job resolves to zero delivery targets and the user did not
explicitly ask for deliver='local'. The check uses the scheduler's own
_resolve_delivery_targets so it accounts for origin, home channels,
'all', and explicit platform targets — no false positives.
- PLATFORM_HINTS gains a 'tui' entry (the TUI had none) and the 'cli'
hint now states that cron jobs from these sessions are local-only and
that deliver must target a gateway-connected platform to notify the
user. This stops the agent promising a delivery that never happens.
No scheduler/delivery behavior change; no new env var; cron isolation
invariant untouched.
Make the computer_use toolset platform-agnostic by driving cua-driver on
macOS, Windows, and Linux. Consumes the 8 cua-driver decoupling surfaces
(capability discovery, structuredContent AX tree, opaque element_token,
click button enum, explicit mimeType, machine-readable manifest,
structured list_windows, structured health_report), each degrading
gracefully on older drivers.
Adds `hermes computer-use doctor` (drives cua-driver health_report with a
per-OS check matrix and an exit 0/1/2 ok/degraded/blocked contract), full
typed wrappers for the previously-uncovered cua-driver tools plus a generic
call_tool escape hatch, per-session agent-cursor lifecycle, platform-aware
system-prompt guidance (host-deterministic, cache-safe), and honors
HERMES_CUA_DRIVER_CMD end-to-end.
Replaces the macOS-only skills/apple/macos-computer-use skill with a
cross-platform skills/computer-use skill, and refreshes the EN + zh-Hans
docs.
Supersedes #44221 (Windows-enablement salvage of #30660).
Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
The kanban-worker and kanban-orchestrator bundled skills existed only to
be force-loaded into dispatcher-spawned workers, gated by
environments:[kanban] so they wouldn't leak into normal CLI listings.
That gating was fragile (the leak that #50443 patched) and the
--skills auto-load was already best-effort — most workers ran without it
because the bundled skill isn't present in profile-scoped skills dirs.
Remove the skills entirely and promote their load-bearing content
(workspace kinds, deliverable artifacts, created-card integrity, profile
discovery) into KANBAN_GUIDANCE, which is already injected into every
kanban worker's system prompt. Net result: every worker reliably gets
the guidance, nothing can leak into a CLI/blank-slate session, and the
gating machinery is gone.
- agent/prompt_builder.py: promote the 4 load-bearing rules into KANBAN_GUIDANCE
- hermes_cli/kanban_db.py: drop --skills kanban-worker auto-injection + _kanban_worker_skill_available probe
- hermes_cli/kanban_swarm.py: drop skills=[kanban-orchestrator] on the root card
- hermes_cli/kanban.py: drop kanban-init skill seeding; fix help text
- delete skills/devops/kanban-{worker,orchestrator}
- docs: delete the two skill pages (EN+zh), fix sidebars/catalog/kanban.md/kanban-worker-lanes.md and the video-orchestrator + codex-lane references
- tests: update spawn-argv expectations; re-bound the guidance-size guard
Supersedes the skill-leak half of #50443 (credit @helix4u for flagging the area).
The universal PARALLEL_TOOL_CALL_GUIDANCE block already lives on main, but it
shipped with two rough edges this change cleans up:
- It duplicated the batching steer for Google models. The
GOOGLE_MODEL_OPERATIONAL_GUIDANCE block still carried its own
"Parallel tool calls" bullet, so Gemini/Gemma received the instruction
twice in one prompt. Drop the redundant bullet — the universal block is now
the single source.
- Its comment claimed "nothing in the open-source system prompt encouraged
batching," which was wrong: the steer existed for Google models only. Reword
to say the gap was that every *other* model got nothing.
- Tighten the test that asserts the steer (precedence-correct), and add an
invariant guarding against re-introducing the Google duplicate.
* Port from cline/cline#11514: encourage parallel tool calls
Add a universal system-prompt guidance block telling the model to batch
independent tool calls (reads, searches, web fetches, read-only commands)
into a single assistant turn instead of one call per turn. The runtime
already executes independent batches concurrently (read-only tools always;
non-overlapping path-scoped file ops); the open-source system prompt had
nothing steering the model to PRODUCE the batch. Fewer round-trips means
less resent context, which compounds over a long conversation.
- prompt_builder.py: new PARALLEL_TOOL_CALL_GUIDANCE block (short, static,
cache-amortised) modeled on TASK_COMPLETION_GUIDANCE.
- system_prompt.py: inject right after the task-completion block, gated by
agent.valid_tool_names + the new toggle.
- agent_init.py: read agent.parallel_tool_call_guidance (default True).
- config.py: add the default under the agent section.
- test_prompt_builder.py: behavior-contract tests (batching steer, dependent
carve-out, length bound) — invariants, not wording snapshots.
Adapted from Cline's TypeScript tool-surface guidance to hermes-agent's
Python prompt-assembly architecture and config-over-env conventions.
* fix(desktop): never persist or restore a named custom provider as bare "custom"
Custom providers vanish from the Desktop/TUI model picker with
"No LLM provider configured" — repeatedly fixed (#44062, #44109, #45578)
and repeatedly regressed (#44022, #47714) because every fix only recovered
the entry identity from a persisted base_url. When a session is
persisted/restored with the resolved provider "custom" and NO base_url, bare
"custom" leaked through verbatim; resolve_runtime_provider("custom") routes to
the OpenRouter default URL with no api_key, so the next turn/resume dies.
Bare "custom" is the resolved billing class shared by every named providers:/
custom_providers: entry — it is not a routable identity. Centralize the
"never let bare custom escape" invariant in one helper,
runtime_provider.canonical_custom_identity(), and apply it at all four leak
sites in tui_gateway/server.py:
- _ensure_session_db_row — the ORIGIN: first DB write seeds the bad row
- _runtime_model_config — live persist
- _stored_session_runtime_overrides — resume restore (heals old rows; drops
unrecoverable bare custom so resume falls back to config default)
- _make_agent — rebuild / per-turn
The helper recovers custom:<name> from the endpoint URL when present, else
from config.model.provider (the durable identity left when no base_url
survived). Regression tests in test_custom_provider_session_persistence.py
lock the no-base_url vector at every site so it cannot regress again.
Context files (AGENTS.md, CLAUDE.md, .hermes.md, .cursorrules, SOUL.md) were
hard-capped at a flat 20K chars before head/tail truncation. Among the agent
harnesses we track, only Codex caps project docs at all (32 KiB); Claude Code,
OpenCode, and Cline load them whole. The flat 20K predates large context
windows and silently truncates real-world AGENTS.md files.
B — dynamic cap: when context_file_max_chars is unset (now the shipped
default), the cap scales with the model's context window
(ctx_tokens * 4 * 0.06, floor 20K, ceiling 500K). Small-context models stay at
the historical 20K; a 200K model gets 48K; large models stop truncating real
docs. An explicit context_file_max_chars still wins. Context length is resolved
once per conversation (stable -> prompt cache untouched).
C — when truncation does happen, the marker now names the concrete file path
and tells the agent to read_file it for the full content.
Validation: 154 targeted tests + full agent/ + hermes_cli/ + test_config
(0 failures); E2E against a real 60K AGENTS.md confirms small windows truncate
with the path-bearing marker, large windows load whole, and the system prompt
is byte-stable across rebuilds.
Follow-up to salvaged PR #41619: replace the module-global
_truncation_warnings list with a contextvars.ContextVar so concurrent
gateway-session prompt builds can't drain or clear each other's pending
warnings (cross-session leak). Adds a context-isolation test.
PROBLEM: Automatic context files such as SOUL.md and AGENTS.md were capped by a hardcoded CONTEXT_FILE_MAX_CHARS value. Amy's local fork had raised that constant from 20K to 25K so a larger SOUL.md would not be silently truncated, but the hardcoded 25K value changed upstream default behavior and made the patch less generally useful.
SOLUTION: Restore the upstream-compatible 20K default, add a context_file_max_chars config setting for users who intentionally keep larger identity/project-context files, keep chat-visible truncation warnings, and document the new setting. Tests cover the default, config override, explicit max_chars precedence, and the warning text.