From 06231e77d7b09ad59d7f9b9e9db0f9c973fb883f Mon Sep 17 00:00:00 2001 From: maxwellgeng Date: Tue, 11 Aug 2026 18:01:14 +0800 Subject: [PATCH 01/11] Merge branch 'dev-020' into dev-fix --- .gitignore | 1 + FORK_NOTES.md | 196 +- FORK_NOTES.zh-CN.md | 258 +- agent/agent_runtime_helpers.py | 1 + agent/coding_context.py | 59 +- agent/tool_executor.py | 1 + apps/desktop/electron/main.ts | 20 +- cron/scheduler.py | 266 +- extract_config.py | 433 +++ gateway/platforms/webhook_filters.py | 47 +- gateway/run.py | 37 +- gateway/slash_commands.py | 23 +- hermes_cli/codex_runtime_plugin_migration.py | 12 + hermes_cli/dashboard_auth/public_paths.py | 4 + hermes_cli/gateway.py | 40 +- hermes_cli/gateway_windows.py | 43 +- hermes_cli/kanban_db.py | 8 +- hermes_cli/main.py | 200 +- hermes_cli/profiles.py | 22 + hermes_cli/relaunch.py | 7 +- hermes_cli/tools_config.py | 17 + hermes_cli/uninstall.py | 8 +- hermes_cli/web_server.py | 45 +- plugins/google_meet/cli.py | 9 + plugins/google_meet/process_manager.py | 11 + plugins/memory/hindsight/__init__.py | 45 +- plugins/memory/mem0/_setup.py | 10 + plugins/platforms/feishu/adapter.py | 219 +- .../google-workspace/scripts/setup.py | 13 + ...est_auxiliary_client_xai_oauth_recovery.py | 15 - tests/agent/test_context_compressor_tools.py | 17 - tests/agent/test_context_tools.py | 56 - tests/agent/test_credential_pool_routing.py | 44 - tests/agent/test_error_classifier.py | 45 - tests/agent/test_minimax_provider.py | 14 - tests/agent/test_platform_hint_desktop.py | 2 +- tests/agent/test_system_prompt.py | 6 +- tests/agent/test_system_reminder.py | 50 - .../test_hermes_tools_mcp_server.py | 13 - tests/cli/test_terminal_interrupt_recovery.py | 29 - tests/cron/test_cron_script.py | 824 +++++ ...st_10710_auto_reset_evicts_cached_agent.py | 51 - .../test_35809_auto_reset_clean_context.py | 64 - tests/gateway/test_feishu.py | 3181 +++++++++++++++-- .../test_internal_worker_subcommands.py | 80 + .../memory/test_hindsight_config_schema.py | 51 - .../memory/test_honcho_config_schema.py | 47 - .../model_providers/test_minimax_profile.py | 31 - .../test_anthropic_truncation_continuation.py | 18 - .../test_compact_reminder_integration.py | 32 - .../test_fallback_credential_isolation.py | 136 - .../run_agent/test_primary_runtime_restore.py | 21 - tests/test_install_no_initial_commit.py | 27 - tests/test_install_sh_symlink_stomp.py | 21 - tests/test_process_loop_event_loop_warning.py | 130 - tests/test_toolsets.py | 21 - tests/test_trajectory_compressor_async.py | 26 - tests/test_xxhash_migration.py | 96 - tests/test_yuanbao_proto.py | 17 - tests/tools/test_bash_fix.py | 114 +- tests/tools/test_browser_hardening.py | 97 - .../tools/test_computer_use_vision_routing.py | 24 - tests/tools/test_file_tools.py | 36 - tests/tools/test_interrupt.py | 50 - tests/tools/test_mcp_stdio_watchdog.py | 21 + tests/tools/test_mcp_tool.py | 95 - tests/tools/test_pr_6656_regressions.py | 35 - tests/tools/test_runtime_compat.py | 123 + tests/tools/test_shell_resolution.py | 49 +- tests/tools/test_signal_media.py | 14 - tests/tools/test_subprocess_stdin_guard.py | 22 - .../test_terminal_dynamic_description.py | 35 +- .../test_terminal_foreground_timeout_cap.py | 17 - tests/tools/test_terminal_post_process.py | 22 - tests/tools/test_todo_tool.py | 579 +++ tests/tools/test_tts_piper.py | 14 - tests/tools/test_video_generation_dispatch.py | 9 - tests/tools/test_watch_patterns.py | 17 - .../tui_gateway/test_slash_worker_sys_path.py | 43 - .../test_slash_worker_utf8_decode.py | 7 +- tools/agent_swarm.py | 13 +- tools/browser_tool.py | 28 +- tools/code_execution_tool.py | 14 +- tools/environments/bash_fix.py | 146 +- tools/environments/local.py | 40 +- tools/lazy_deps.py | 14 + tools/mcp_tool.py | 9 + tools/memory_tool.py | 45 +- tools/process_registry.py | 16 +- tools/runtime_compat.py | 122 + tools/skills_tool.py | 6 +- tools/terminal_tool.py | 42 +- tools/todo_tool.py | 483 ++- tools/tts_tool.py | 99 +- tools/vision_tools.py | 15 +- tools/web_tools.py | 8 +- tui_gateway/host_supervisor.py | 3 +- tui_gateway/server.py | 63 +- 98 files changed, 7437 insertions(+), 2372 deletions(-) create mode 100644 extract_config.py create mode 100644 tests/hermes_cli/test_internal_worker_subcommands.py delete mode 100644 tests/plugins/memory/test_hindsight_config_schema.py delete mode 100644 tests/test_process_loop_event_loop_warning.py delete mode 100644 tests/test_xxhash_migration.py create mode 100644 tests/tools/test_runtime_compat.py create mode 100644 tools/runtime_compat.py diff --git a/.gitignore b/.gitignore index 283c8a9164dbe..a1d69d25ec1bf 100644 --- a/.gitignore +++ b/.gitignore @@ -192,6 +192,7 @@ infographic/ infographics/ infograficos/ infografico/ +build/ native/fts5_cjk/*.so # Runtime marker written by hermes update when a lazy dependency refresh is # interrupted; consumed by launch-time recovery. Never commit it (was tracked diff --git a/FORK_NOTES.md b/FORK_NOTES.md index d1bb69f32e656..839e83913075b 100644 --- a/FORK_NOTES.md +++ b/FORK_NOTES.md @@ -29,8 +29,7 @@ This document explains the fork-specific changes on `main` that diverge from ups | **P-013** | `model_tools.py`, `tests/run_agent/test_repair_tool_arg_keys.py` | Adds automatic tool argument key repair (`repair_tool_arg_keys`) with alias tables, per-tool overrides, fuzzy fallback, nested object/array recursion, and an optional callback hook; integrated into `handle_function_call` before type coercion | LLMs often misname arguments (e.g. "file"→"path", "cmd"→"command"); this makes tool dispatch resilient to common drift without weakening JSON Schemas | Should be upstreamed | | **P-014** | `.github/workflows/release-runtime.yml`, `tools/mcp_tool.py`, `hermes_cli/config.py`, `docs/RUNTIME_RELEASES.md`, `tests/tools/test_mcp_tool.py` | Bundles the native MCP client SDK into the frozen runtime (install entry later folded into the `cn-desktop` extra — see P-015 — plus `--collect-submodules/--copy-metadata mcp` and a CI assert on `mcp-*.dist-info`), and makes `discover_mcp_tools()` warn once when `mcp_servers` is configured but the SDK is absent instead of silently no-op'ing at debug | Issue #16: the desktop runtime shipped without the `mcp` extra, so `_MCP_AVAILABLE=False` and configured `mcp_servers` registered no tools with no INFO-level log. The packaging fix is fork-specific; the diagnostic + known-root-key are generic | Packaging change is CN-specific; the `mcp_tool.py` warning and `mcp_servers` known-root-key should be upstreamed | | **P-015** | `pyproject.toml`, `.github/workflows/release-runtime.yml`, `docs/RUNTIME_RELEASES.md`, `uv.lock` | Adds a `cn-desktop` aggregate extra that pre-bakes every backend the frozen runtime exposes (`web`, `anthropic`, `mcp`, `feishu`, `dingtalk`, `wecom`, plus 微信's `aiohttp`/`qrcode`/`cryptography`). The release workflow installs `.[cn-desktop]`, collects the IM SDK submodules + metadata, runs a build-env import smoke test, and asserts each backend's `dist-info` in the frozen output | Desktop report: the 飞书/钉钉/企微/微信 adapters silently degraded to "unavailable" because their SDKs (`lark-oapi`, `dingtalk-stream`, …) were never bundled and the frozen build can't lazy-install. Same root cause as P-014, generalized to all desktop backends | Packaging is CN-specific; not upstreamed (upstream doesn't build these artifacts) | -| **P-016** | `tools/terminal_tool.py`, `tools/environments/local.py`, `tools/environments/process_pwsh.py`, `tools/environments/base.py`, `model_tools.py`, `tests/tools/test_terminal_dynamic_description.py` | PowerShell native execution: on Windows, uses `pwsh.exe` (PS7) as the primary local shell with `powershell.exe` (PS5.1) fallback, plus full lifecycle support (`_run_pwsh`, `_wrap_command_pwsh`, `init_session`, cwd tracking). Removes Git Bash auto-install. Adds runtime-adaptive terminal tool description that replaces Linux/bash command references with PowerShell cmdlets when the active shell is PowerShell; adds shell-fingerprint to tool-definitions cache key. Adds `pwsh_transform` warning propagation so the LLM is notified when its PS7 syntax was down-leveled to PS5.1 | Agent on Windows was hardcoded to Git Bash; PowerShell has better Windows-native path handling and avoids the POSIX-translation overhead. Git for Windows auto-install has been removed — the agent uses PowerShell on Windows. The static `TERMINAL_TOOL_DESCRIPTION` contained Linux-only command references that are misleading under PowerShell | Should be upstreamed | -| **P-019** | `tools/environments/local.py`, `tools/terminal_tool.py`, `agent/prompt_builder.py`, `cli.py`, `apps/desktop/electron/main.cjs`, `scripts/install.ps1`, `hermes_cli/uninstall.py`, `cron/scheduler.py`, `tools/environments/base.py`, `tools/file_operations.py`, `tools/browser_tool.py`, `tests/tools/test_shell_resolution.py`, `tests/tools/test_terminal_dynamic_description.py`, `tests/tools/test_windows_native_support.py`, `tests/tools/test_local_env_windows_msys.py`, `website/docs/user-guide/windows-native.md`, `website/docs/reference/environment-variables.md`, `website/docs/developer-guide/contributing.md`, `FORK_NOTES.md`, `FORK_NOTES.zh-CN.md`, `hermes_bootstrap.py`, `tools/environments/windows_env.py`, `scripts/check-windows-footguns.py`, tests, `scripts/verify_windows_utf8.py` | Complete Git-Bash-to-PowerShell migration: removes all Git Bash discovery (7-strategy `_find_bash`), WSL launcher filtering, and `HERMES_GIT_BASH_PATH` env var support. On Windows, **Windows PowerShell 5.1** (`powershell.exe`, ships with every Windows 10/11 system) is now the **only** supported shell — no `pwsh.exe` (PS7) probing, no download, no install. `HERMES_SHELL_TYPE=bash` raises RuntimeError on Windows. Renames: `_find_pwsh_simple` → `_find_powershell`, `_run_pwsh` → `_run_powershell`, `_wrap_command_pwsh` → `_wrap_command_powershell`, `_normalize_git_bash_path` → `_normalize_msys_path`. `pwsh_transform` is now **always-on** (not conditional on PS5.1). Replaces `findGitBash()` with `findPowerShell()` in desktop Electron. Removes `Install-Git`/`Set-GitBashEnvVar`/`Stage-Git` from `install.ps1`. Removes `HERMES_GIT_BASH_PATH` from uninstaller. Updates cron scheduler to refuse `.sh`/`.bash` on Windows. Updates prompt builder to instruct PowerShell 5.1 syntax. Cleans up Git Bash references in comments, docs, and tests. Also adds PowerShell UTF-8 encoding hardening via `ps_with_utf8()`, console CP_UTF8 bootstrap, and `encoding='utf-8'` only on PowerShell subprocesses. | `powershell.exe` (5.1) ships with every Windows 10/11 — zero install, zero download. Starts faster than Git Bash, handles Windows paths natively, avoids POSIX-translation overhead. Removes ~400 lines of dead code (7-strategy bash discovery, WSL launcher filter, PortableGit auto-install). The agent now has a single, predictable, always-available shell on Windows. P-016's `pwsh.exe` (PS7) probing was unnecessary complexity — 5.1 is universal. | Supersedes P-016; should be upstreamed | +| **P-016 / P-019 / P-XXX** | `tools/environments/local.py`, `tools/environments/process_pwsh.py`, `tools/environments/windows_env.py`, `tools/terminal_tool.py`, `agent/prompt_builder.py`, `model_tools.py`, `cli.py`, `apps/desktop/electron/main.cjs`, `scripts/install.ps1`, `hermes_cli/uninstall.py`, `cron/scheduler.py`, `tools/environments/base.py`, `tools/file_operations.py`, `tools/browser_tool.py`, `hermes_bootstrap.py`, `scripts/check-windows-footguns.py`, `scripts/verify_windows_utf8.py`, tests, website docs, `FORK_NOTES*.md` | **Windows shell saga — Git Bash → PowerShell.** P-016 (superseded) added PowerShell native execution: on Windows, `pwsh.exe` (PS7) primary with `powershell.exe` (PS5.1) fallback, full lifecycle (`_run_pwsh`, `_wrap_command_pwsh`, `init_session`, cwd tracking), a runtime-adaptive terminal description that replaces Linux/bash command references with PowerShell cmdlets, a shell-fingerprint in the tool-definitions cache key, `pwsh_transform` warning propagation, and removal of Git Bash auto-install. P-019 completed the migration: removed all Git Bash discovery (7-strategy `_find_bash`, WSL launcher filter, `HERMES_GIT_BASH_PATH`); **Windows PowerShell 5.1** (`powershell.exe`, ships with every Windows 10/11) is now the only Windows shell — no `pwsh.exe` probing, no download, no install; `HERMES_SHELL_TYPE=bash` raises `RuntimeError` on Windows; renames `_find_pwsh_simple`→`_find_powershell`, `_run_pwsh`→`_run_powershell`, `_wrap_command_pwsh`→`_wrap_command_powershell`, `_normalize_git_bash_path`→`_normalize_msys_path`; `pwsh_transform` always-on; desktop `findGitBash()`→`findPowerShell()`; removed `Install-Git`/`Set-GitBashEnvVar`/`Stage-Git`; cron refuses `.sh`/`.bash` on Windows; PowerShell UTF-8 hardening (`ps_with_utf8()`, console CP_UTF8 bootstrap, `encoding='utf-8'` only on PowerShell subprocesses). P-XXX (un-numbered follow-up) restored **pwsh-first detection**: `_find_pwsh()` multi-step (PATH, ProgramFiles, Registry, LocalAppData); `_resolve_shell()` prefers pwsh over powershell.exe; `pwsh_transform` skipped when running pwsh natively; all dispatch points accept both `"powershell"` and `"pwsh"`; pwsh-aware dynamic terminal description and `_WINDOWS_PWSH_SHELL_HINT`. | On Windows the agent was hardcoded to Git Bash; PowerShell starts faster, handles Windows paths natively, avoids POSIX-translation overhead, and ships with every Windows 10/11 (zero install/download). P-019 removed ~400 lines of dead code and gave the agent a single, predictable, always-available Windows shell — P-016's `pwsh.exe` probing was unnecessary complexity (5.1 is universal); P-XXX then let users with `pwsh` installed skip the PS7→PS5.1 down-level transform entirely. | Should be upstreamed (P-019 supersedes P-016; P-XXX is its follow-up) | | **P-017** | `agent/tool_dedup.py`, `agent/agent_init.py`, `agent/conversation_loop.py`, `agent/tool_executor.py` | Adds `ToolDedupTracker` that detects consecutive identical tool calls across API iterations and injects escalating reminders (``) at repeat counts 3, 5, and 8 to break infinite loops | Agent on complex tasks can enter infinite loops calling the same tool with the same arguments repeatedly — the existing same-turn dedup (`_deduplicate_tool_calls`) doesn't catch this cross-iteration pattern | Internal — addresses a behavioral robustness gap; the mechanism is generic but integration points are fork-specific | | **P-018** | `agent/agent_init.py`, `tests/run_agent/test_init_fallback_on_exhausted_pool.py` | Adds `_api_key_required` helper and empty-key guards before OpenAI / Anthropic SDK client construction. Raises `RuntimeError: no API key (param empty, env vars unset)` instead of letting a low-level SDK auth exception bubble up | Empty key (param empty, env vars unset) previously triggered confusing low-level SDK exceptions that looked like panics, especially in TUI/gateway background threads where stack traces are not surfaced to the user | Should be upstreamed | | **P-020** | `tools/environments/windows_env.py` (new), `tools/environments/local.py`, `hermes_cli/claw.py`, `hermes_cli/managed_uv.py`, `hermes_cli/gateway.py`, `hermes_cli/dep_ensure.py`, `hermes_cli/clipboard.py`, `skills/creative/comfyui/scripts/hardware_check.py` | Adds `refresh_env_from_registry()` that refreshes `os.environ["PATH"]` and `os.environ["PATHEXT"]` from the Windows Registry (HKLM + HKCU) before every PowerShell subprocess invocation, so tools installed since process start (WinGet, MSI, etc.) are discoverable. Mirrors the pattern from `kimi-cli/src/kimi_cli/utils/environment.py`. No-op on non-Windows. | Without this, the agent cannot discover binaries installed (e.g. via WinGet) after its process started — `shutil.which` and `subprocess.Popen` only see the PATH that was captured at process creation. This is especially painful when the agent installs its own deps (node, uv, ...) during a session. | Should be upstreamed | @@ -49,14 +48,13 @@ This document explains the fork-specific changes on `main` that diverge from ups | **P-033b** | `tools/file_operations.py`, `tests/tools/test_file_ops_windows_inprocess.py`, `tests/tools/test_file_tools_live.py`, `tests/tools/test_file_operations.py` | Hardens `write_file` with backend-agnostic post-write verification: after `_atomic_write` reports success, the file is re-read via `_prim_read_all` and compared to the intended content (BOM stripped, line endings normalized). `_local_atomic_write` now also verifies the file exists and its bytes match before returning. This closes the remaining silent-success window where the writer exits 0 but the bytes never persist. | P-033 fixed the PowerShell/POSIX mismatch but `write_file` still trusted the writer's exit code. Any remaining edge case (backend FS quirk, race, truncated pipe) could still report success while the file was missing or unchanged. | Should be upstreamed with P-033. | | **P-034** | `gateway/status.py`, `tests/gateway/test_gateway_command_line_matcher.py` | The gateway-process recognizer (`_gateway_command_subcommand`) now treats a command line whose argv[0] basename starts with `hermes-agent-cn-runtime` (the frozen desktop PyInstaller binary, e.g. `hermes-agent-cn-runtime-win32-x64.exe`) as a hermes CLI entrypoint. Added ONLY to `has_gateway_entry` (so the real `gateway` subcommand is still parsed) — NOT to the gateway-dedicated-entrypoint scan (which returns `run` unconditionally and would misread a frozen `gateway status`/`stop`/`restart`). | The desktop runs the frozen runtime binary as ` gateway run --replace`, but `_gateway_command_subcommand` only knew `hermes_cli.main` / `hermes`/`hermes-gateway` basenames. So `get_running_pid()` treated a LIVE desktop gateway as "not a gateway" → deleted its live `gateway.pid`/`gateway.lock`, defeating `--replace`, the duplicate-instance guard, AND the scoped WeChat-token lock's staleness check → multiple gateways raced the same iLink session → periodic `[Weixin] Session expired` + "未连接" panel (#42). The Rust side already recognized the binary via a `"gateway run"` substring, so Core/Desktop disagreed on what a gateway was. | The frozen-binary recognition is CN-desktop-specific, but framed as generic frozen-binary support it could be upstreamed. Related: P-014/P-015 (frozen runtime), P-016/P-019. | | **P-036** | `tui_gateway/server.py`, `tests/gateway/test_provider_models_rpc.py` (new) | Adds a `provider.models` RPC that returns a provider's **full** `/models` id list (probe only samples 5) and **tolerates an empty api_key** (local servers need none). Refactors the URL-candidate fetch shared with `provider.probe` into `_fetch_provider_model_ids` so both go through one code path; `provider.probe`'s response is byte-for-byte unchanged. | Desktop report: a self-hosted Ollama on the **LAN** (`http://192.168.31.11:11434/v1`) tested fine (测试连接 goes through `provider.probe` on the backend) but the model picker's 刷新 failed with `external_request only allows https URLs; http is only allowed for local URLs` — the desktop fetched the LAN endpoint directly through its SSRF-guarded `external_request` proxy, which blocks http to non-loopback private IPs. Listing models from the backend (no such guard, same as the probe) makes LAN/self-hosted providers refreshable and also sidesteps the browser CORS that blocked the web shell. | Maybe upstream (sibling to P-011's `provider.probe`) | -| **P-035** | `.github/workflows/release-runtime.yml`, `tests/test_runtime_release_workflow.py` | The runtime-release build-env gate ("Verify platform backends importable") now imports the 飞书/钉钉/企微 adapters from their post-sync plugin locations (`plugins.platforms.feishu.adapter`, `plugins.platforms.dingtalk.adapter`, `plugins.platforms.wecom.adapter` + `plugins.platforms.wecom.callback_adapter`) instead of the removed `gateway.platforms.{feishu,dingtalk,wecom_callback}` modules; weixin (微信个人号, CN-only) still imports from `gateway.platforms.weixin`. Adds a pytest regression test that loads each migrated adapter from `plugins/platforms/` and asserts the gate no longer references the removed modules. | The upstream sync (PR #57, commit `560010547`) migrated the IM adapters from `gateway/platforms/*.py` to bundled plugins (`plugins/platforms//`), but the CN-only release gate still did `import gateway.platforms.feishu`, so every platform's `release-runtime.yml` build failed with `ModuleNotFoundError: No module named 'gateway.platforms.feishu'` *before* PyInstaller — this is what broke `runtime-v0.17.0-cn.3`. The gate runs only on `runtime-v*` tags, so regular CI never exercised it; the new pytest test moves the check into every CI run. | CN-fork-only release tooling — not for upstream. Related: P-014/P-015 (frozen-runtime bundling), P-028/P-029/P-032 (runtime release workflow). | +| **P-035b** | `.github/workflows/release-runtime.yml`, `tests/test_runtime_release_workflow.py` | The runtime-release build-env gate ("Verify platform backends importable") now imports the 飞书/钉钉/企微 adapters from their post-sync plugin locations (`plugins.platforms.feishu.adapter`, `plugins.platforms.dingtalk.adapter`, `plugins.platforms.wecom.adapter` + `plugins.platforms.wecom.callback_adapter`) instead of the removed `gateway.platforms.{feishu,dingtalk,wecom_callback}` modules; weixin (微信个人号, CN-only) still imports from `gateway.platforms.weixin`. Adds a pytest regression test that loads each migrated adapter from `plugins/platforms/` and asserts the gate no longer references the removed modules. | The upstream sync (PR #57, commit `560010547`) migrated the IM adapters from `gateway/platforms/*.py` to bundled plugins (`plugins/platforms//`), but the CN-only release gate still did `import gateway.platforms.feishu`, so every platform's `release-runtime.yml` build failed with `ModuleNotFoundError: No module named 'gateway.platforms.feishu'` *before* PyInstaller — this is what broke `runtime-v0.17.0-cn.3`. The gate runs only on `runtime-v*` tags, so regular CI never exercised it; the new pytest test moves the check into every CI run. | CN-fork-only release tooling — not for upstream. Related: P-014/P-015 (frozen-runtime bundling), P-028/P-029/P-032 (runtime release workflow). | | **P-037** | `tools/environments/local.py`, `tools/file_operations.py`, `tests/tools/test_local_pwsh_warnings.py`, `tests/tools/test_file_ops_p037.py` (new) | Three Windows in-process I/O correctness follow-ups to P-016/P-019/P-030/P-033. **(1)** Moves the `pwsh_transform` PS7→PS5.1 down-level call from `_run_powershell` (which received the **assembled** wrapper) to `_wrap_command_powershell`, applying it to the **raw** user command before it is embedded as a single-quoted `Invoke-Expression ''` literal. **(2)** Routes `patch_replace`'s read, its post-write verify re-read, and `_check_lint`'s disk read through `_prim_read_all` instead of a raw `cat … 2>/dev/null` `_exec`. **(3)** Adds `_decode_file_bytes` + module-level `_INPROC_FALLBACK_ENCODINGS` (= `("mbcs",)` on Windows) and uses it in the whole-file in-process reads (`_prim_read_all`, `_prim_read_page`): try UTF-8, then the system ANSI code page, then lossy replacement. | **(1)** `pwsh_transform`'s region mask skips single-quoted string contents, so transforming the wrapper never touched the user's command — the load-bearing PS7→PS5.1 bridge was a silent no-op on the real exec path and `Invoke-Expression` raised a ParserError on `&&`/`||`/`??`/ternary under 5.1. The unit tests passed only because they call `pwsh_transform` directly. **(2)** Under PowerShell 5.1 (P-016/P-019: the only Windows shell) there is no POSIX `cat` and `2>/dev/null` redirects to a literal `\dev\null` file, so `patch_replace` was likely broken on the very platform P-030/P-033 target. **(3)** The replaced shell read decoded via the system code page; hard-coding UTF-8 turned every non-ASCII byte of a GBK/cp936 file (the common case on Chinese Windows, this fork's audience) into U+FFFD, and a read→patch→write round-trip then persisted that corruption (silent data loss). | All three are generic cross-platform correctness fixes and should be upstreamed; the urgency is CN-fork-specific (P-016/P-019 made PowerShell 5.1 the only Windows shell). Related: P-016/P-019 (PowerShell-only), P-030/P-033 (in-process I/O). | | **P-038** | `agent/lsp/client.py`, `agent/lsp/install.py`, `gateway/run.py`, `gateway/platforms/qqbot/adapter.py`, `gateway/platforms/whatsapp_cloud.py`, `hermes_cli/claw.py`, `hermes_cli/clipboard.py`, `plugins/platforms/telegram/adapter.py`, `plugins/teams_pipeline/pipeline.py`, `tools/file_operations.py`, related tests | Threads the existing `hermes_cli/_subprocess_compat` Windows creation flags into the remaining helper-subprocess spawn sites: LSP servers spawn with `windows_detach_flags_without_breakaway()`; ffmpeg/ffprobe conversions (qqbot, WhatsApp Cloud, Teams pipeline, gateway audio-duration probe), the gateway kanban `exec_cmd` shell, `claw` process scans, clipboard PowerShell helpers and the LSP `npm install` (now routed through `resolve_node_command`) run with `windows_hide_flags()`. Also translates POSIX `/dev/null` redirects to PowerShell `*>$null`/`2>$null`/`>$null` in `ShellFileOperations._exec` on Windows, and marks three POSIX-only tests `skipif(win32)`. | On Windows each of these spawn sites either flashed a visible console window or joined the parent's console/job (so parent teardown killed them) — `creationflags` was previously applied only on the main terminal-tool path. The `/dev/null` redirects created a literal `\dev\null` file (or failed) under PowerShell 5.1 — same bug class as P-037's `cat` shell-out. | Overlaps upstream's Windows console-flash hardening wave (e.g. #52340); reconcile at the next sync — prefer upstream where they collide, keep the sites upstream still misses. Related: P-016/P-019 (PowerShell-only Windows shell), P-033/P-037. | | **P-039** | `agent/auxiliary_client.py`, `hermes_cli/config.py` (auxiliary docs), `hermes_cli/main.py`, `tests/agent/test_auxiliary_client.py`, `tests/agent/test_auxiliary_main_first.py` | Auxiliary "auto" resolution never probes OpenRouter or Nous Portal implicitly. Text chain: main provider → local/custom endpoint → direct API-key providers → None (`_get_provider_chain` drops the openrouter/nous rungs). Vision auto falls back to native Anthropic only (`_VISION_AUTO_PROVIDER_ORDER = ("anthropic",)`), with `_VISION_EXPLICIT_PROVIDER_ORDER` covering explicit requests and main-provider strict backends. OpenRouter/Nous still work when they ARE the main provider or an explicit `auxiliary..provider`. | CN users typically cannot reach openrouter.ai / Nous Portal; implicit fallback probing added dead network waits to every compression/title-gen/vision call and made aux tasks fail slow instead of falling back fast to reachable providers. | CN-specific default, won't upstream. Landed 2026-06-07 (`bc40674ac`) without a P-number; registered retroactively during the v0.18.0 sync after the merge nearly dropped it. | | **P-040** | `hermes_cli/web_server.py`, `tests/hermes_cli/test_web_server_platforms_offload.py` (new) | `/api/messaging/platforms` builds its catalog via `run_in_executor` (outside the profile scope, which stays await-free), and lifespan fires `_warm_platform_registry` into a worker thread at startup (sibling of `_warm_gateway_module`). | `_messaging_platform_catalog()` triggers platform-plugin discovery, which imports every bundled IM adapter on first call — discord.py alone takes 10s+ cold — INLINE in the async handler. The desktop calls this endpoint during its first boot paint, so the event loop wedged and every boot API call queued behind it: users saw a blank/"连接中" dashboard for 15s+ after each runtime update (fresh process), and the Playwright E2E suite failed its 15s composer assertion against a fresh v0.18.0 backend. | Should be upstreamed (upstream desktop/dashboard hit the same wedge; matches their own cold-start offload idiom #54448/#54523). | | **P-041** | `agent/agent_init.py`, `agent/conversation_loop.py`, `tui_gateway/server.py`, `apps/desktop/src/app/session/hooks/use-message-stream/gateway-event.ts`, `apps/desktop/src/lib/chat-messages.ts`, `hermes_cli/config.py`, `tests/run_agent/test_tool_call_streaming_convergence.py` (new), `tests/tui_gateway/test_tool_call_committed_event.py` (new), `apps/desktop/src/lib/chat-messages.test.ts` | Fixes a Windows Desktop stuck-turn where a `write_file` tool call with no preceding text is followed by a `terminal` tool call. Adds an explicit `assistant.tool_calls_committed` event, per-session event tracing, a turn inactivity watchdog, hardens back-to-back tool-part matching, and swallows spurious `None` stream deltas. | The desktop stream state machine started from `message.start` and expected either text deltas or a final `message.complete`. Tool-call-only assistant messages provided neither, so back-to-back tool calls could leave the UI stuck on "running terminal command" with no recovery boundary. | Should be upstreamed | -| **P-XXX** | `tools/environments/local.py`, `tools/terminal_tool.py`, `agent/prompt_builder.py`, `tools/environments/base.py`, `tools/environments/windows_env.py`, `tests/tools/test_shell_resolution.py`, `tests/tools/test_terminal_dynamic_description.py`, `tests/tools/test_local_pwsh_warnings.py` | **Detect `pwsh` (PowerShell 7) first, fallback to PowerShell 5.1.** Adds `_find_pwsh()` multi-step detection (PATH, ProgramFiles, Registry, LocalAppData). `_resolve_shell()` now prefers pwsh over powershell.exe. `_wrap_command_powershell()` skips `pwsh_transform` when running pwsh natively. All dispatch points (`init_session`, `_run_bash`, `_wrap_command`, `_update_cwd`, `_extract_cwd_from_output`) now accept both `"powershell"` and `"pwsh"` shell types. `_detect_shell_for_description()` probes for pwsh and returns `"pwsh"` when found. `_build_dynamic_terminal_description()` has a pwsh variant. `prompt_builder.py` uses `_WINDOWS_PWSH_SHELL_HINT` when pwsh is available (no PS5.1 limitation warnings). | P-016/P-019 hardcoded `powershell.exe` (PS5.1) and always ran `pwsh_transform` to down-level PS7 syntax. Users with `pwsh` installed got unnecessary transform overhead and warnings for native PS7 features. When pwsh is available, use it directly and skip the down-level transform entirely. | Should be upstreamed (follow-up to P-016/P-019) | -| **P-042** | `tools/environments/windows_env.py`, `tools/environments/powershell_session.py` (new), `tools/environments/local.py`, `hermes_cli/config.py`, `tests/tools/test_windows_env.py`, `tests/tools/test_powershell_session.py` (new), `tests/tools/test_local_pwsh_session.py` (new), `tools/file_operations.py`, `tests/tools/test_windows_perf_optimizations.py` (new), `tests/performance/test_windows_perf.py` | **Subprocess-spawn + write-path overhead (perf hotspot #6).** Windows terminal-spawn + in-process-write optimizations. **(1) Registry-refresh cache:** `refresh_env_from_registry()` (P-020), previously re-read HKLM+HKCU `Path`/`PATHEXT` before *every* PowerShell spawn, now caches on the two Environment keys' last-write signature (`QueryInfoKey`) — it skips the value read + `%expand%` + merge when nothing changed and re-reads the instant an install bumps a key's mtime, so no staleness window (unlike a blind TTL). Adds `force=` + `_reset_registry_env_cache()`. **(2) Reusable PowerShell session (opt-in):** new `PowerShellSession` feeds many commands to one long-lived `powershell/pwsh -Command -` over stdin (base64-wrapped + marker-terminated, `try/catch` so the marker always fires), so warm commands run in ~1-5ms instead of the ~80-100ms Windows spawn. `LocalEnvironment.execute()` uses it when `terminal.powershell_session_reuse` (bridged by `HERMES_PWSH_SESSION_REUSE`) is on, resetting `$LASTEXITCODE` per command for exact spawn-parity exit codes and refreshing `$env:PATH` per command for P-020 parity; commands needing stdin, and any failure, fall back to the unchanged spawn path. Default OFF (a session carries shell state between commands). **(3) cmd.exe fast path (opt-in, `terminal.cmd_fast_path`):** trivial metacharacter-free builtins route through a one-shot `cmd.exe /c` (~10-20ms) instead of `powershell.exe`; strict eligibility keeps behaviour identical to PowerShell, considered only after — and superseded by — session reuse. **(4) CRC-32 write verification:** `_local_atomic_write` re-reads the temp and checks a streamed CRC-32 + size before the atomic rename, aborting a corrupt/short write before it clobbers the good original (gated by `HERMES_WRITE_VERIFY_CRC`, default on). **(5) FILE_ATTRIBUTE_TEMPORARY hint (opt-in):** the atomic-write scratch temp can be tagged temporary and cleared before the rename. | Each Windows PowerShell spawn pays ~31-100ms+ of process-creation + DLL-load + interpreter-init on the terminal hot path (root-cause-analysis.md hotspot #6); the P-020 registry refresh added ~5-15ms per call with no cache. Caching the refresh and reusing the interpreter remove both on the warm path. | The registry-refresh cache is a generic Windows fix and should be upstreamed; the session reuse is Windows-shell-specific (built on P-016/P-019's PowerShell-only path) but the pattern could be generalized. Related: P-016/P-019 (PowerShell-only Windows shell), P-020 (registry PATH refresh), P-XXX (pwsh detection). | +| **P-042** | `tools/environments/windows_env.py`, `tools/environments/powershell_session.py` (new), `tools/environments/local.py`, `hermes_cli/config.py`, `tests/tools/test_windows_env.py`, `tests/tools/test_powershell_session.py` (new), `tests/tools/test_local_pwsh_session.py` (new), `tools/file_operations.py`, `tests/tools/test_windows_perf_optimizations.py` (new), `tests/performance/test_windows_perf.py` | **Subprocess-spawn + write-path overhead (perf hotspot #6).** Windows terminal-spawn + in-process-write optimizations. **(1) Registry-refresh cache:** `refresh_env_from_registry()` (P-020), previously re-read HKLM+HKCU `Path`/`PATHEXT` before *every* PowerShell spawn, now caches on the two Environment keys' last-write signature (`QueryInfoKey`) — it skips the value read + `%expand%` + merge when nothing changed and re-reads the instant an install bumps a key's mtime, so no staleness window (unlike a blind TTL). Adds `force=` + `_reset_registry_env_cache()`. **(2) Reusable PowerShell session (opt-in):** new `PowerShellSession` feeds many commands to one long-lived `powershell/pwsh -Command -` over stdin (base64-wrapped + marker-terminated, `try/catch` so the marker always fires), so warm commands run in ~1-5ms instead of the ~80-100ms Windows spawn. `LocalEnvironment.execute()` uses it when `terminal.powershell_session_reuse` (bridged by `HERMES_PWSH_SESSION_REUSE`) is on, resetting `$LASTEXITCODE` per command for exact spawn-parity exit codes and refreshing `$env:PATH` per command for P-020 parity; commands needing stdin, and any failure, fall back to the unchanged spawn path. Default OFF (a session carries shell state between commands). **(3) cmd.exe fast path (opt-in, `terminal.cmd_fast_path`):** trivial metacharacter-free builtins route through a one-shot `cmd.exe /c` (~10-20ms) instead of `powershell.exe`; strict eligibility keeps behaviour identical to PowerShell, considered only after — and superseded by — session reuse. **(4) CRC-32 write verification:** `_local_atomic_write` re-reads the temp and checks a streamed CRC-32 + size before the atomic rename, aborting a corrupt/short write before it clobbers the good original (gated by `HERMES_WRITE_VERIFY_CRC`, default on). **(5) FILE_ATTRIBUTE_TEMPORARY hint (opt-in):** the atomic-write scratch temp can be tagged temporary and cleared before the rename. | Each Windows PowerShell spawn pays ~31-100ms+ of process-creation + DLL-load + interpreter-init on the terminal hot path (root-cause-analysis.md hotspot #6); the P-020 registry refresh added ~5-15ms per call with no cache. Caching the refresh and reusing the interpreter remove both on the warm path. | The registry-refresh cache is a generic Windows fix and should be upstreamed; the session reuse is Windows-shell-specific (built on P-016/P-019's PowerShell-only path) but the pattern could be generalized. Related: P-016/P-019 (PowerShell-only Windows shell; pwsh detection), P-020 (registry PATH refresh). | | **P-043** | `model_tools.py`, `tools/registry.py`, `run_agent.py`, `cli.py`, `tests/performance/test_tool_dispatch.py`, `tests/tools/test_registry_schema_json.py` (new) | **First-dispatch latency (perf hotspots #8/#9).** Moves the ~4,486ms cold first-dispatch tax off the user-visible hot path. Adds `warm_dispatch_path()` — an idempotent, thread-safe, fire-and-forget primitive that completes deferred discovery, builds + caches the schema catalog for a toolset selection, and pre-serializes each tool's schema. Wired into `AIAgent.warmup()`/`awarmup()` and the CLI banner-idle warmup (now routed through the shared primitive). Adds `registry.get_schema_json()` — a lazily-computed, per-entry JSON cache (no import-time cost, invalidated on re-register). | The first tool dispatch / first API request triggered full tool discovery (module imports + `check_fn` probes + schema assembly) synchronously — ~4,486ms cold vs ~2ms warm, so the first tool call felt like a hang (root-cause-analysis.md hotspots #8/#9). | The warmup primitive + lazy schema-JSON cache are generic and should be upstreamed; the cold-start magnitude is Windows/py3.14-specific. Related: P-040 (cold-start offload), P-042 (subprocess-spawn overhead). | | **P-044** | `platform_utils.py` (new), `hermes_cli/config.py`, `hermes_cli/dep_ensure.py`, `tools/code_execution_tool.py`, `tools/environments/powershell_session.py`, `tools/environments/local.py`, `tools/process_registry.py`, `plugins/platforms/whatsapp/adapter.py`, `agent/prompt_builder.py`, `agent/ssl_guard.py`, `tools/environments/windows_env.py`, `hermes_cli/doctor.py`, `scripts/precompile.py` (new), `pyproject.toml`, `tests/tools/test_wmi_ssl_windows_overhead.py` (new), `tests/agent/test_ssl_ca_guard.py` | **WMI + SSL + import overhead at agent-init (.plans/15, hotspots `_wmi.exec_query` 2.91% / `_ssl.set_default_verify_paths` 8.19%).** **(1) WMI:** on Python 3.12+ `platform.system()`/`platform.release()` build `platform.uname()`, which issues a Windows `_wmi.exec_query` (`win32_ver` + `_get_machine_win32`, ~45ms). Dozens of modules ran `_IS_WINDOWS = platform.system() == "Windows"` at *module scope*, so that WMI probe was paid twice during the import cascade, and `prompt_builder`'s `platform.release()` paid it again while building the system prompt on every init. New WMI-free `platform_utils` (`is_windows()` off the `sys.platform` constant, `windows_release()` off `sys.getwindowsversion()`) replaces the module-level flags + the host line; the import cascade and prompt build now issue **0** WMI queries (verified). **(2) SSL:** `verify_ca_bundle()` (run on every `AIAgent` construction, agent_init.py) rebuilt a throwaway `ssl.create_default_context()` (~225ms on Windows) each time; it now memoises the successful verdict on a cheap fingerprint of the CA env vars + certifi bundle (path/size/mtime), so an unchanged CA config validates once per process (~0.05ms on repeat) while any change re-validates and re-raises. **(3) Imports:** `scripts/precompile.py` (`compileall`) warms the `.pyc` cache off the hot path (run-from-source / CI / post-update) so first import doesn't pay `builtins.compile` + `_io.open_code` + a Defender scan of freshly written `.pyc`. **(4) Defender hint:** `suggest_defender_exclusion()` surfaces a HERMES_HOME exclusion tip through `hermes doctor` (informational; changes nothing). | On Windows/Python 3.12+ the innocuous `platform.system()`/`release()` idiom silently talks to the WMI service (~40-90ms/cold call), and the SSL guard re-loaded the CA store on every agent construction — a gateway spawning many agents/subagents re-paid both. The plan's literal `import wmi` / `pywin32` premise did not apply (this tree has no `wmi` package usage); the real WMI source is the stdlib `platform` module, so the fix removes the WMI trigger rather than lazily importing a nonexistent dependency. | `platform_utils` and the ssl_guard memoisation are provider/OS-generic and should be upstreamed (correct everywhere; the WMI *cost* is py3.12+-on-Windows-specific). Related: P-042 (Windows subprocess-spawn overhead), P-043 (first-dispatch latency). | | **P-045** | `import_accelerator.py` (new), `hermes_bootstrap.py`, `run_agent.py`, `scripts/precompile.py`, `pyproject.toml`, `agent/message_utils.py`, `agent/agent_runtime_helpers.py`, `tools/registry.py`, `tests/test_import_accelerator.py` (new), `tests/test_precompile.py` (new), `tests/agent/test_message_utils.py` | **Flame-graph import-system optimizations (.plans/16, import system ~71% of cold agent-init).** **(1) First-party import accelerator:** a `sys.meta_path` finder (`import_accelerator`) resolves Hermes's own *curated* top-level modules/packages (the set mirrors pyproject `py-modules` + `packages.find`) with a single dict lookup, skipping the per-entry `sys.path` directory scan (`nt.stat` / `nt._path_exists`). Package-over-module precedence (so the repo-root `agent.py` harness can never shadow the real `agent/` package), `.pyc`-preserving `spec_from_file_location`, O(1) fall-through for every other name, opt-out `HERMES_DISABLE_IMPORT_ACCELERATOR`. Installed from `hermes_bootstrap` (the first import of every entry point) and idempotently from `run_agent`. The map is validated once at build time and NOT re-stat'd per resolve, so it never regresses a warm tree (measured neutral-to-faster). **(2) Precompile idempotency:** `scripts/precompile.py` gains `precompile_if_needed` (stamp-guarded on a source+interpreter fingerprint), `precompile_in_background` (daemon thread), `[tool.hermes.precompile]` target reading, and `--if-needed` / `--force`; an opt-in `HERMES_PRECOMPILE_ON_START` background warm (default OFF, no-op under pytest/frozen) front-loads `builtins.compile` (~10.82% of cold init) off the hot path for the run-from-source layout. **(3) Per-request micro-opts:** `sanitize_api_messages` (runs before every LLM call) folds its two per-tool_call type dispatches into one `get_tool_call_function_and_id`; `registry.get_definitions` snapshots only the *requested* entries under a brief lock instead of materializing a map of the whole ~250-tool registry. | The import system is ~71% of cold agent-init. The plan's literal `type()`-vs-`isinstance` swap was disproven by benchmark (identical cost; the combined form is slower), and its "344k isinstance calls" are third-party *import-time* class construction (pydantic/SDK), mitigated by NOT importing them eagerly (lazy proxies + lazy tool index + this accelerator) rather than by rewriting our own isinstance calls. So the fix targets the real levers: skip redundant path scanning, precompile bytecode, and cut genuinely-redundant per-request dispatch. | The import accelerator, precompile idempotency, and the sanitizer/registry micro-opts are OS-generic and should be upstreamed; the cold-start *magnitude* is Windows/run-from-source-specific. Related: P-043 (first-dispatch latency), P-044 (WMI/SSL/import overhead). | @@ -67,29 +65,65 @@ This document explains the fork-specific changes on `main` that diverge from ups | **P-049** | `tools/terminal_post_process.py` (new), `tools/terminal_command_rewrite.py` (new), `tools/rtk_provision.py` (new), `tools/terminal_tool.py`, `hermes_constants.py`, `hermes_cli/dep_ensure.py`, `scripts/install.ps1`, `scripts/install.sh`, `scripts/install_coreutils.py`, `tests/tools/test_terminal_post_process.py` (new), `tools/file_operations.py`, `tools/tirith_security.py`, `hermes_cli/commands.py`, `tests/tools/test_file_operations.py`, `tests/tools/test_search_error_guard.py`, `tests/tools/test_search_hidden_dirs.py`, `tests/tools/test_tirith_security.py` | **Terminal output post-processing pipeline + rtk (reasoning toolkit) integration.** (1) New `terminal_post_process.py`: multi-stage pipeline — ANSI stripping + `\r\n`→`\n` normalization, deduplication of repeated output lines (single-line + multi-line block mode, configurable threshold), line-based head/tail truncation with fold marker, oversized output export to session file, and YAML-like metadata block assembly. (2) New `rtk_provision.py`: runtime detection + path resolution for the `rtk` binary (mirrors `_find_rg()` pattern), searching managed tools dir → legacy `$HERMES_HOME/bin` → PATH, with `functools.lru_cache`. (3) New `terminal_command_rewrite.py`: shell-command-aware rewriting that prepends `rtk` to known high-output commands (`git`, `cargo`, `npm`, `ls`, `grep`, `cat`, `python`, `docker`, PowerShell cmdlets, etc.), correctly splitting shell segments at `;`/`&&`/`||`/`|` while respecting quotes and subshells. (4) `terminal_tool.py` now accepts `token_kill` (default True) and `max_lines` parameters; before execution it rewrites commands through rtk when available, and after execution runs the full post-processing pipeline (replacing the old inline ANSI-strip + char-based truncation). (5) `hermes_constants.py`: new `get_managed_tools_dir()` returns `/tools` (with legacy `/bin` fallback for existing installs). (6) `dep_ensure.py`: adds `rtk` to `_DEP_CHECKS`/`_DEP_DESCRIPTIONS`; refactors `_find_rg()` and coreutils check to use `get_managed_tools_dir()` with legacy fallback. (7) `file_operations.py`/`commands.py`: use `_find_rg()` from dep_ensure instead of raw `shutil.which("rg")` so the managed copy is preferred. (8) `tirith_security.py`: migrates auto-install target from legacy `$HERMES_HOME/bin/tirith` to `get_managed_tools_dir()`, with backward-compat PATH fallback. (9) `scripts/install.ps1`/`install.sh`: add rtk binary download and `hermes doctor` check. (10) `scripts/install_coreutils.py`: uses `get_managed_tools_dir()` for managed tools path. | The old terminal output handling was a single inline ANSI-strip + hard-coded char-based truncation at 40%/60% split, with no deduplication, no line-based truncation, and no way for the model to control output limits. Commands like `git log`, `cargo test`, `docker ps`, or `npm install` could produce thousands of lines of repetitive output — the model paid for every repeated line. rtk is an external CLI that natively collapses repeated output lines before they reach the agent, and the post-processing pipeline provides a second pass (dedup + line truncation) for cases where rtk is absent or disabled. The new `max_lines` parameter lets the model request a specific number of lines (head + tail with fold marker), which is more intuitive than the old byte-based truncation. The `get_managed_tools_dir()` consolidation moves external binaries to `/tools/` (from the generic `bin/`) so they don't pollute PATH and are easier to manage. | Should be upstreamed (generic terminal output quality-of-life improvement; the managed-tools-dir pattern is a generic maintenance improvement). | -| **P-050** | `tools/environments/local.py`, `agent/prompt_builder.py`, `hermes_cli/config.py`, `hermes_cli/gateway.py`, `tools/environments/base.py`, `scripts/keystroke_diagnostic.py`, `apps/desktop/electron/main.ts`, `apps/desktop/electron/windows-hermes-resolution.test.ts`, `tests/tools/test_shell_resolution.py`, `tests/tools/test_modal_sandbox_fixes.py`, `tests/agent/test_image_routing.py`, `tests/skills/test_unbroker_skill.py`, `tests/skills/test_openclaw_migration.py`, `tests/hermes_cli/test_update_stale_dashboard.py`, `tools/terminal_tool.py`, README files, website docs, `skills/autonomous-ai-agents/hermes-agent/SKILL.md`, `FORK_NOTES.md` | **Re-enable `HERMES_SHELL_TYPE=bash` on Windows as an optional explicit shell (requires pre-installed Git Bash, no auto-download).** Phase 2.2: `_resolve_shell()` now finds pre-installed bash via `_find_bash_posix()` instead of raising `RuntimeError`. Phase 2.1: `_WINDOWS_BASH_SHELL_HINT` and the `bash` dispatch branch in `prompt_builder.py` preserved; `_WINDOWS_POWERSHELL_SHELL_HINT` updated to mention `pwsh_transform`'s automatic down-leveling. Both PowerShell hints expanded from 5 to 14 rules (Verb-Noun cmdlets, .NET pipeline, comparison/logical operators, string quoting, splatting, `$LASTEXITCODE`, backtick-avoidance). Phase 1: `findGitBash()` in Electron simplified (removed PortableGit auto-download candidates); preflight now conditionally checks bash (when `shell:bash` configured) or PowerShell (default). Phase 3: tests updated — `test_windows_bash_found_returns_bash` and `test_windows_bash_not_found_raises_helpful_error` replace the old `test_windows_bash_raises_runtime_error`. Phase 4: all READMEs and website docs updated to describe PowerShell as the default shell with Git Bash as an optional opt-in. **Cross-platform test fixes:** `TestCwdHandling` code fix (check raw `docker_cwd_source` before `os.path.abspath`), `TestExtractImageRefs` regex extended for Windows drive-letter paths + `os.path.normpath` for mixed separators, platform-portable path assertions across 3 test files, flaky Windows tests marked `xfail(strict=False)`. — P-019 made PowerShell 5.1 the only supported shell on Windows and prohibited `HERMES_SHELL_TYPE=bash`. This fork's users may still have Git for Windows installed for VCS operations, and some workflows legitimately need POSIX shell syntax. Re-allowing bash as an explicit opt-in (no auto-download) restores flexibility without re-introducing the auto-install complexity or the PortableGit download. | Should be upstreamed (user choice; no auto-download risk) | - -| **P-052** | `tools/environments/local.py`, `tests/tools/test_local_git_bash_port.py` (new) | **kimi `bash_tool` win32 parity port.** (1) `_wrap_command` gates `fix_bash_command` explicitly on `sys.platform == "win32"` (POSIX hosts: byte-for-byte no-op) and records `_bash_fix_warnings` only when the fixer changed the command. (2) Ports the git.exe discovery chain (`_where_git_executables` / `_git_bash_candidate_from_git_path` / `_git_exec_path` / `_git_install_root_from_exec_path` / `_git_bash_candidates_from_exec_path`) wired into `_find_bash` as the LAST candidate source (after env override → managed portable Git → known locations → PATH). (3) Ports `_is_git_bash_install` (drive-anchored `/cmd/git.exe` marker) + `_with_msystem_neutralized` (`export MSYSTEM=; `), applied per command in `_wrap_command`'s win32 branch so children (xmake/meson) see an empty MSYSTEM instead of `MINGW64`; real MSYS2 installs untouched. (4) Ports `_encode_startup_script` (base64+gzip one-liner) — ready for a future interactive `bash -i` bootstrap. (5) Ports macOS candidates (`_bash_candidates_macos` / `_bash_candidates_system` / `_git_bash_for_macos`) with a darwin preference branch in `_find_bash_posix` (Linux ordering unchanged). (6) `_run_bash` bash branch now spawns `self._shell_path` (resolved Git Bash) instead of re-finding via `_find_bash_posix()`, so init discovery and execution agree. | P-050 re-enabled `HERMES_SHELL_TYPE=bash` on Windows; on that path children of one-shot `bash -c` misdetected the platform (`MSYSTEM=MINGW64`), Git Bash discovery missed per-user/choco/scoop/side-by-side installs, the bash-fix ran unconditionally on POSIX hosts, and `_run_bash` could spawn a different bash than the one `_resolve_shell` selected. | The MSYSTEM neutralization + git.exe discovery + marker check + macOS candidates are kimi's design and generic — should be upstreamed; the win32 gate + shell-path consistency are correctness fixes. | +| **P-050 / P-052 / P-054** | `tools/environments/local.py`, `tools/terminal_tool.py`, `agent/prompt_builder.py`, `hermes_cli/config.py`, `hermes_cli/gateway.py`, `tools/environments/base.py`, `scripts/keystroke_diagnostic.py`, `apps/desktop/electron/main.ts`, `apps/desktop/electron/windows-hermes-resolution.test.ts`, `tests/tools/test_shell_resolution.py`, `tests/tools/test_terminal_dynamic_description.py`, `tests/tools/test_local_git_bash_port.py` (new), README files, website docs, `skills/autonomous-ai-agents/hermes-agent/SKILL.md`, `FORK_NOTES*.md` | **Git Bash re-enabled as an explicit Windows opt-in (pre-installed only, no auto-download), then hardened.** P-050 re-allowed `HERMES_SHELL_TYPE=bash` on Windows: `_resolve_shell()` finds pre-installed bash via `_find_bash_posix()` instead of raising; `_WINDOWS_BASH_SHELL_HINT` and the bash dispatch branch restored; both PowerShell hints expanded 5→14 rules (Verb-Noun cmdlets, .NET pipeline, operators, splatting, `$LASTEXITCODE`, backtick-avoidance); desktop `findGitBash()` simplified (no PortableGit auto-download) and preflight conditionally checks bash or PowerShell; READMEs/docs updated (PowerShell default, Git Bash optional opt-in). P-052 ported kimi's `bash_tool` win32 parity: win32-gated `fix_bash_command` (POSIX hosts byte-for-byte no-op), git.exe discovery chain (`where.exe git` / `git --exec-path`) as the LAST `_find_bash` candidate source, `_is_git_bash_install` marker + `_with_msystem_neutralized` (`export MSYSTEM=; `) so children see empty MSYSTEM instead of `MINGW64`, `_encode_startup_script` (base64+gzip, ready for future interactive `bash -i`), macOS bash candidates, and `_run_bash` spawning the resolved `self._shell_path`. P-054 made a missing/broken Git Bash **fall back** to the PowerShell chain (pwsh → powershell.exe) with a warning instead of failing startup: `_resolve_shell()`'s bash branch no longer lets `_find_bash()`'s `RuntimeError` propagate; `_detect_shell_for_description()` reports `"bash"` only when a usable Git Bash was found; desktop preflight (`ensureRuntime`) warns instead of throwing. | P-019 made PowerShell 5.1 the only Windows shell and banned `HERMES_SHELL_TYPE=bash`, but fork users may still have Git for Windows for VCS and some workflows legitimately need POSIX syntax; re-allowing bash as an explicit opt-in (no auto-download) restores flexibility without re-introducing auto-install complexity. P-052 fixed the MSYSTEM `MINGW64` platform misdetection and missed per-user/choco/scoop/side-by-side Git Bash installs on that path. P-054 ensured a stale `shell: bash` config or an uninstalled/policy-blocked Git degrades gracefully to the always-present PowerShell instead of bricking the entire terminal tool. | Should be upstreamed (user choice, no auto-download risk; graceful degradation is generic) | +| **P-051** | repo-wide — 269 text-mode `subprocess.run/Popen/call/check_call/check_output` call sites in shipped code (`agent/coding_context.py`, `agent/system_prompt.py`, `hermes_cli/web_server.py`, `tools/tts_tool.py`, `tools/transcription_tools.py`, `hermes_cli/_subprocess_compat.py`, …), `tests/test_subprocess_text_pipe_decoding.py` (new) | **Pins explicit decoding on every text-mode subprocess pipe — the GBK `_readerthread` UnicodeDecodeError class eliminated.** Children that write UTF-8 by contract (git, gh, rg, python/pip/uv, node/npm, docker, ssh/scp, systemctl, tmux, cosign, codex/claude CLIs, hermes itself, …) get `encoding="utf-8", errors="replace"`; Windows-native tools emitting the OEM/ANSI codepage (`tasklist`/`taskkill`/`netstat`/`where`) get `errors="replace"` only; the three `**kwargs`-dict splat sites (`_fs_git_branch`, `_run_command_tts`, `_run_command_stt`) get the same pins inside their dicts; a latent `NameError` (missing `import subprocess`) in `_subprocess_compat.py`'s drop-in wrappers is fixed. | On zh-CN Windows (cp936, py3.14 without UTF-8 mode) `subprocess.run(..., text=True)` wraps child pipes with `locale.getpreferredencoding(False)`; a single GBK-illegal byte (e.g. `0xAE` inside 实) kills CPython's daemon `_readerthread`, the caller gets `stdout is None` with no exception, and the session-start workspace snapshot silently vanished from the system prompt. | Should be upstreamed — the per-site pins and the new AST-invariant regression guard are OS-agnostic. Related: c210b9621 (slash-worker pipe pins), P-042, P-019 | +| **P-053** | `tests/` — 170 files (8 deleted whole, 162 edited) | **Test-suite hygiene sweep: removed ad-hoc / magic-mock / anti-LLM-hallucination tests.** Triaged 1,203 candidate test files (63 strong-signal + 39 medium-signal chunks) via sub-agent review against the repo's own change-detector ban (AGENTS.md "Don't write change-detector tests"): deleted 8 whole files (self-referential replica-of-production tests, third-party-lib-only tests, pure schema/enum literal-pin files) and **546 individual tests across 162 files** — change-detector literal pins (model lists, tool counts, schema shapes, version/constant freezes), magic-mock call-signature pins, mock-self tautologies, `inspect.getsource`/AST source-shape pins, inline re-implementations of production logic, and coverage padding. ~10,467 lines removed; 1,033 files kept (behavioral / regression / invariant / E2E tests). | The suite had accumulated tests whose only purpose was to freeze current values or code shapes so an LLM editing the codebase couldn't silently change them. They break on any intentional change, add maintenance noise, and never test behavior — the opposite of the repo's "Behavior contracts over snapshots" rule. | Test-quality cleanup, not a behavioral patch — the DELETE criteria match AGENTS.md's own guidance and could inform upstream test hygiene | +| **P-055** | `tools/runtime_compat.py` (new), `gateway/platforms/webhook_filters.py`, `gateway/run.py`, `gateway/slash_commands.py`, `hermes_cli/main.py`, `hermes_cli/gateway.py`, `hermes_cli/gateway_windows.py`, `hermes_cli/kanban_db.py`, `hermes_cli/relaunch.py`, `hermes_cli/web_server.py`, `hermes_cli/uninstall.py`, `hermes_cli/profiles.py`, `hermes_cli/tools_config.py`, `hermes_cli/codex_runtime_plugin_migration.py`, `tui_gateway/server.py`, `tui_gateway/host_supervisor.py`, `tools/todo_tool.py`, `tools/tts_tool.py`, `tools/mcp_tool.py`, `tools/code_execution_tool.py`, `tools/lazy_deps.py`, `plugins/google_meet/*`, `plugins/memory/{hindsight,mem0}/*`, `skills/productivity/google-workspace/scripts/setup.py`, `tests/` | **Route every "spawn python as interpreter" site through the frozen-runtime chokepoint.** The CN portable desktop runtime is a PyInstaller-frozen exe with no standalone `python.exe` — inside it `sys.executable` IS the Hermes CLI binary, so `[sys.executable, script.py]` / `[sys.executable, "-m", mod]` / `[sys.executable, "-c", code]` runs `hermes ` and dies with argparse "invalid choice". New `tools/runtime_compat.py` centralizes `is_frozen_runtime()`, `hermes_cli_argv()` (drops the `-m hermes_cli.main` prefix under frozen), and `run_python_script_in_process()` (in-process `runpy` runner with timeout/stdin capture, generalizing the cron fix). Every production spawn site now: (1) re-invokes the CLI via `hermes_cli_argv` (kanban dispatcher, relaunch, dashboard reexec, web-server detached actions, uninstall, gateway run/restart argv builders, `gateway_windows` cmd/vbs/argv builders); (2) runs trusted scripts in-process when frozen (webhook route scripts, todo verification, NeuTTS synth, Piper voice download); (3) skips or clearly refuses when a real python is required (MCP stdio watchdog wrapper, execute_code sandbox, Google Meet bot, Codex migration, uv/pip lazy installs, `hermes update`); (4) dispatches TUI slash-worker / compute-host / gateway restart-watch / update-gateway-helper through new hidden CLI subcommands (`__slash-worker`, `__compute-host`, `__gateway-restart-watch`, `__update-gateway-helper`). | Same root cause as the cron fix (`#whitecat_inspect`): the frozen desktop runtime has no standalone python, so every subprocess site that assumed `sys.executable` was an interpreter broke at the desktop. Fixing cron alone left ~30 sibling call sites broken. | Packaging is CN-specific (upstream doesn't ship a frozen exe); the `hermes_cli_argv`/in-process-runner pattern is generic and could be upstreamed as a compat helper | +| **P-056** | `agent/prompt_builder.py`, `tests/agent/test_prompt_builder.py`, `tests/agent/test_system_prompt.py`, `tests/agent/test_platform_hint_desktop.py` | **Compresses and simplifies core system prompt guidance in `prompt_builder.py`.** `HERMES_AGENT_INTRODUCTION`, `HERMES_AGENT_HELP_GUIDANCE`, `MEMORY_GUIDANCE`, `SESSION_SEARCH_GUIDANCE`, `SKILLS_GUIDANCE`, `KANBAN_GUIDANCE`, `TOOL_USE_ENFORCEMENT_GUIDANCE`, `TASK_COMPLETION_GUIDANCE`, `PARALLEL_TOOL_CALL_GUIDANCE`, `OPENAI_MODEL_EXECUTION_GUIDANCE`, `GOOGLE_MODEL_OPERATIONAL_GUIDANCE`, `STEER_CHANNEL_NOTE`, `computer_use_guidance`, `PLATFORM_HINTS`, and the Windows PowerShell/pwsh shell hints are all rewritten to be concise and directive while preserving the original behavioral contracts. Removes verbose XML-tag scaffolding, long examples, and redundant explanations. Factors a shared `_MEDIA_DELIVERY_HINT` across messaging platforms. Tests are updated from literal phrase checks to behavioral invariants (headings, key concepts) so they do not break when prompt wording is refined. | Long, verbose system prompts consume tokens and can dilute the model's attention; shorter, directive instructions improve signal-to-noise ratio while the same rules (act don't plan, verify, batch independent calls, use tools for facts, no fabrication, platform-specific media delivery, PowerShell syntax) remain enforced. | Should be upstreamed (generic prompt-quality / token-efficiency improvement) | +| **P-057** | `tools/delegate_tool.py`, `tools/session_search_tool.py`, `tools/code_execution_tool.py`, `tools/terminal_tool.py`, `tools/skill_manager_tool.py`, `tools/memory_tool.py`, `tools/todo_tool.py`, `tools/clarify_tool.py`, `tools/browser_tool.py`, `tools/file_tools.py`, `tools/process_registry.py`, `tools/vision_tools.py`, `tools/tts_tool.py`, `tools/skills_tool.py`, `tools/agent_swarm.py`, `tools/web_tools.py`, `agent/coding_context.py`, `agent/prompt_builder.py` | **Second pass: compress tool-schema descriptions and remaining system-prompt blocks to save tokens.** Tool descriptions (the largest per-API-call cost — every tool ships on every call) rewritten to be concise-but-comprehensive: default 28-tool core set drops from 47,535 to ~29,964 schema chars (37% cut; descriptions 25,237→13,330, params 22,298→16,634). Top targets: delegate_task 4002→1458, session_search 3547→1246, execute_code 2454→1559, terminal 2398→1364, skill_manage 1800→1001, memory 1476→884, todo 1299→760, clarify 1185→745; browser/file/vision/process/tts/skills/web tools −40% each. Every behavior-critical fact preserved: calling shapes, dynamic limits (delegate concurrency/spawn-depth render from config), required params, when-NOT-to-use routing, PowerShell/bash command mappings. System-prompt blocks: `CODING_AGENT_GUIDANCE` 2626→1387, `KANBAN_GUIDANCE` 5481→2798, `computer_use_guidance` 815→581, `PLATFORM_HINTS["yuanbao"]` 1072→667. No schema structure, handler, function name, or test file changed; dynamic-schema mechanics (delegate_task/execute_code/terminal) intact. Verified: targeted suites green (856 passed), full `tests/tools` 3,672 passed (4 pre-existing MSYS pathconv env failures also fail on pristine baseline), ruff clean. | Continuation of P-056: after the guidance constants, tool descriptions were the next token lever — every model-tool schema is resent on every API call for the session's lifetime, so 37% fewer chars directly cuts cost and improves signal-to-noise. | Should be upstreamed (generic token-efficiency improvement; no CN-specific behavior) | > **P-001** (provider dict-vs-list mismatch in `tui_gateway/server.py`) — **dropped from this fork**. Upstream has since fixed it; the line `user_provs = cfg.get("providers")` in `_apply_model_switch` already does the right thing. --- -### P-052: kimi bash_tool win32 parity — bash-fix gating, MSYSTEM neutralization, git.exe discovery +### P-050 / P-052 / P-054: Git Bash re-enabled as an explicit Windows opt-in, then hardened + +**Symptom / need.** P-019 made Windows PowerShell 5.1 the only supported Windows shell and prohibited `HERMES_SHELL_TYPE=bash`. But fork users may still have Git for Windows installed for VCS operations, and some workflows legitimately need POSIX shell syntax. Re-allowing bash as an explicit opt-in (pre-installed only, no auto-download) restores flexibility without re-introducing the auto-install complexity or the PortableGit download. However, the P-019-era hard `RuntimeError` when Git Bash is missing (or cannot start — the Mandatory-ASLR failure class) meant a stale `terminal.shell: bash` config, an uninstalled Git for Windows, or a policy-blocked Git Bash bricked the entire terminal tool — even though PowerShell 5.1 ships with every Windows system and `auto` mode already uses the pwsh → powershell.exe chain. Additionally, on the re-enabled bash path, children of one-shot `bash -c` misdetected the platform (`MSYSTEM=MINGW64`), Git Bash discovery missed per-user/choco/scoop/side-by-side installs, `fix_bash_command` ran unconditionally on POSIX hosts, and `_run_bash` could spawn a different bash than `_resolve_shell` selected. + +**What P-050 did** (re-enable `HERMES_SHELL_TYPE=bash` as an explicit opt-in): + +1. **`tools/environments/local.py`** — `_resolve_shell()` finds pre-installed bash via `_find_bash_posix()` instead of raising `RuntimeError`. +2. **`agent/prompt_builder.py`** — `_WINDOWS_BASH_SHELL_HINT` and the `bash` dispatch branch preserved; `_WINDOWS_POWERSHELL_SHELL_HINT` updated to mention `pwsh_transform`'s automatic down-leveling; both hints expanded from 5 to 14 rules (Verb-Noun cmdlets, .NET pipeline, comparison/logical operators, string quoting, splatting, `$LASTEXITCODE`, backtick-avoidance). +3. **`apps/desktop/electron/main.ts`** — `findGitBash()` simplified (removed PortableGit auto-download candidates); preflight conditionally checks bash (when `shell:bash` configured) or PowerShell (default). +4. **Tests** — `test_windows_bash_found_returns_bash` and `test_windows_bash_not_found_raises_helpful_error` replace the old `test_windows_bash_raises_runtime_error`. +5. **Docs** — READMEs and website docs describe PowerShell as the default shell with Git Bash as an optional opt-in. Cross-platform test fixes: `TestCwdHandling` raw `docker_cwd_source` check, `TestExtractImageRefs` regex extended for Windows drive letters + `os.path.normpath`, platform-portable path assertions, flaky Windows tests marked `xfail(strict=False)`. + +**What P-052 did** (kimi `bash_tool` win32 parity port, all in `tools/environments/local.py`): + +1. **win32-only gating at the call site** — `_wrap_command` checks `sys.platform == "win32"` before `fix_bash_command` (POSIX hosts: byte-for-byte no-op); `_bash_fix_warnings` recorded only when the fixer actually changed the command. +2. **git.exe discovery chain** — `_where_git_executables` (`where.exe git`), `_git_bash_candidate_from_git_path` (`/../bin/bash.exe`), `_git_exec_path` (timeout-guarded `git --exec-path`), `_git_install_root_from_exec_path` (walk up from `mingw*/libexec/git-core`), `_git_bash_candidates_from_exec_path`; wired into `_find_bash` as the **last** candidate source (after env override → managed portable Git → known locations → PATH); the `where.exe`/`git` subprocesses use `windows_hide_flags()`. +3. **MSYSTEM neutralization** — `_is_git_bash_install` (drive-anchored `/cmd/git.exe` marker; `bin/bash.exe` and `usr/bin/bash.exe` layouts; real MSYS2 installs never match) + `_MSYSTEM_NEUTRALIZE_PREFIX = "export MSYSTEM=; "` + `_with_msystem_neutralized`, applied in `_wrap_command`'s win32 branch after bash-fix so children see an empty `MSYSTEM` even when the snapshot exports `MINGW64`. +4. **`_encode_startup_script`** — base64+gzip self-decoding one-liner (stdlib `base64`/`gzip`; identical to kimi's `pybase64`). No production caller yet — it is one third of the interactive `bash -i` bootstrap set, ready for a future interactive Git Bash mode. +5. **macOS bash candidates** — `_bash_candidates_macos` (`/opt/homebrew`, `/usr/local`, `/opt/local`), `_bash_candidates_system`, `_git_bash_for_macos`; `_find_bash_posix` gains a darwin branch preferring newer Homebrew/MacPorts bash over system bash 3.2. Linux ordering unchanged. +6. **Consistency fix** — `_run_bash`'s bash branch spawns `self._shell_path` (the Git Bash resolved at environment init, now including the git.exe chain) instead of re-finding via `_find_bash_posix()`, so `init_session` and `execute` agree on one binary. + +**What P-054 did** (missing/broken Git Bash → graceful PowerShell fallback): + +1. **`tools/environments/local.py` — `_resolve_shell()`.** The `bash` branch wraps `_find_bash()` in `try/except RuntimeError` (Git Bash not found, or the ASLR-remediation class) and treats a resolved-but-probe-failed bash as unavailable via a `_bash_starts()` cache re-check. When bash is unavailable it logs a warning carrying the underlying reason and returns the same `pwsh` → `powershell.exe` chain `auto` uses. `_find_bash()` itself is unchanged (still raises for callers that need the hard error, e.g. `_git_bash_bin_dirs`' try/except path). +2. **`tools/terminal_tool.py` — `_detect_shell_for_description()`.** Bash mode reports `"bash"` only when `_IS_WINDOWS` is true AND `_find_bash()` returned a path that `_bash_starts()` accepts; otherwise `"powershell"` (the shell the terminal actually falls back to). The probe is gated on the real OS flag so tests that mock `platform.system()` never trigger an actual bash search. The stale P-019 comment is gone. +3. **`apps/desktop/electron/main.ts` — preflight.** When `HERMES_SHELL_TYPE=bash` and `findGitBash()` fails, `ensureRuntime` logs `console.warn` and proceeds to the PowerShell-availability check (mirroring the core fallback) instead of throwing. +4. **Tests.** `test_shell_resolution.py`: `test_windows_bash_found_returns_bash` also mocks `_bash_starts=True`; the old `test_windows_bash_not_found_raises_helpful_error` is replaced by `test_windows_bash_missing_falls_back_to_pwsh`, `test_windows_bash_missing_falls_back_to_powershell`, and `test_windows_bash_broken_falls_back_to_powershell`. `test_terminal_dynamic_description.py`: new `test_detect_windows_explicit_bash_returns_bash_when_found` and renamed `test_detect_windows_explicit_bash_returns_powershell_when_bash_unavailable`. + +**What was deliberately NOT changed.** `_find_bash()` keeps raising `RuntimeError` for its direct callers (its "Git Bash is not found" and ASLR-remediation contracts are still useful there). The `auto` default on Windows is untouched (already PowerShell-first). POSIX behavior untouched. No auto-download of Git is added — the fallback only switches to the always-present PowerShell chain. -**Symptom / need.** `HERMES_SHELL_TYPE=bash` (P-050) restored Git Bash as an optional Windows shell, but the kimi-agent bash-tool comparison (Section B of the kimi-vs-Hermes win32 analysis) found three gaps: (1) `fix_bash_command` ran from `_wrap_command` without an explicit win32 gate at the call site — POSIX hosts paid scanner overhead for a byte-for-byte no-op; (2) Git Bash's `bin/bash.exe` launcher unconditionally injects `MSYSTEM=MINGW64` and the MSYS2 runtime re-injects it into children when absent, so xmake/meson/cross-toolchains spawned from a one-shot `bash -c` misdetected the platform as `MINGW64` instead of `windows`; (3) Git Bash discovery never consulted `where.exe git` / `git --exec-path`, so unusual-but-valid installs (per-user, chocolatey/scoop, side-by-side) were missed. +**Tested.** `tests/tools/test_local_git_bash_port.py` (new, 44 tests: encode round-trip + payload safety; `_is_git_bash_install` layouts/marker/drive-anchor; `_with_msystem_neutralized` win32+Git Bash / non-Git-Bash / non-win32 / None; discovery-chain pure functions + subprocess mocking; `_find_bash` wiring; macOS darwin preference; `_wrap_command` MSYSTEM wiring; `_run_bash` spawns the resolved shell path) and `tests/tools/test_shell_resolution.py` + `tests/tools/test_terminal_dynamic_description.py` (47 passed), plus the related suites `test_local_env_windows_msys.py`, `test_find_shell.py`, `test_bash_fix.py`, `test_local_shell_init.py`, `test_process_registry.py`. Full `tests/tools` suite: 8687 passed, 330 skipped (platform-specific skips only). `ruff check` clean on the edited Python files. -**What was implemented** (all in `tools/environments/local.py`): +**Should we upstream?** Yes — graceful degradation when an explicitly-opted-in shell is missing is generic; PowerShell ships with every Windows system so the fallback is always safe. The MSYSTEM neutralization, git.exe discovery chain, marker check and macOS candidates are kimi's design and generic; the win32 gate and `_run_bash` shell-path consistency are correctness fixes. No behavior change on POSIX or when Git Bash is healthy. Not ported by design: `_bash_runs` (superseded by Hermes' stronger `_bash_starts`) and the Windows shell default (deliberate fork divergence). -1. **win32-only gating at the call site** — `_wrap_command` checks `sys.platform == "win32"` before `fix_bash_command` (mirroring the fixer's internal guard), keeping the win32-only contract visible and skipping all fixer overhead on POSIX hosts; `_bash_fix_warnings` is recorded only when the fixer actually changed the command (consumed once by `BaseEnvironment.execute()` alongside `pwsh_warnings`). -2. **git.exe discovery chain** — `_where_git_executables` (`where.exe git`), `_git_bash_candidate_from_git_path` (`/../bin/bash.exe`), `_git_exec_path` (timeout-guarded `git --exec-path`), `_git_install_root_from_exec_path` (walk up from `mingw*/libexec/git-core`), `_git_bash_candidates_from_exec_path`; wired into `_find_bash` as the **last** candidate source so the managed portable Git and standard install locations keep priority, and a plain `bash` on PATH wins over shelling out to `where.exe`/`git`. The `where.exe`/`git` subprocesses use `windows_hide_flags()` (no console flash). -3. **MSYSTEM neutralization** — `_is_git_bash_install` (drive-anchored `/cmd/git.exe` marker; both `bin/bash.exe` and `usr/bin/bash.exe` layouts; real MSYS2 installs never match) + `_MSYSTEM_NEUTRALIZE_PREFIX = "export MSYSTEM=; "` + `_with_msystem_neutralized`, applied in `_wrap_command`'s win32 branch after bash-fix. The prefix runs inside the eval AFTER the snapshot source, so children of the user command see an empty `MSYSTEM` even when the snapshot exports `MINGW64`. -4. **`_encode_startup_script`** — base64+gzip self-decoding one-liner (stdlib `base64`/`gzip`; identical output to kimi's `pybase64`). No production caller yet — it is one third of the interactive `bash -i` bootstrap set (with `bash_compatibility_prelude`), ready for a future interactive Git Bash mode. -5. **macOS bash candidates** — `_bash_candidates_macos` (`/opt/homebrew`, `/usr/local`, `/opt/local`), `_bash_candidates_system`, `_git_bash_for_macos` (official Git installer bash); `_find_bash_posix` gains a darwin branch preferring newer Homebrew/MacPorts bash over the aging system bash 3.2. Linux ordering unchanged. -6. **Consistency fix** — `_run_bash`'s bash branch now spawns `self._shell_path` (the Git Bash resolved at environment init, which now includes the git.exe chain) instead of re-finding via `_find_bash_posix()`, so `init_session` and `execute` agree on one binary. -**Tested.** `tests/tools/test_local_git_bash_port.py` (new, 44 tests): encode round-trip + payload safety; `_is_git_bash_install` both layouts / missing marker / drive-anchor (never the drive-relative `C:...` form); `_with_msystem_neutralized` win32+Git Bash prefix / non-Git-Bash unchanged / non-win32 unchanged / None path; the discovery chain pure functions + subprocess mocking (success / nonzero / OSError / timeout); `_find_bash` wiring via the where.exe branch and the exec-path branch + no-candidates error; macOS darwin preference + git-installer fallback + Linux order; `_wrap_command` MSYSTEM wiring (win32 embeds prefix, non-win32 skips, pwsh dispatch skips, real-marker integration both ways); `_run_bash` spawns the resolved shell path (login + non-login). Full `tests/tools` suite: 8687 passed, 330 skipped (platform-specific skips only). +### P-053: Test-suite hygiene sweep — remove ad-hoc / magic-mock / anti-LLM-hallucination tests -**Upstreamable?** Yes — the MSYSTEM neutralization, git.exe discovery chain, marker check and macOS candidates are kimi's design and generic; the win32 gate and `_run_bash` shell-path consistency are correctness fixes. Not ported by design: `_bash_runs` (superseded by Hermes' stronger `_bash_starts`) and the Windows shell default (deliberate fork divergence, P-016/P-019/P-050). +**Symptom / need.** The test suite had accumulated a large class of tests whose only purpose was to freeze current values or code shapes so that an LLM editing the codebase couldn't silently change them ("anti-hallucination" guardrails): exact model/command/tool lists (`assert models == ["gpt-4.1-mini", ...]`), enumeration counts (`len(tools) == 42`), schema-shape pins (`"query" in schema["parameters"]["properties"]`), version/constant-literal freezes, `inspect.getsource`/AST source-shape pins, mock-self tautologies (`assert x == MagicMock()`-style), inline re-implementations of production logic that never call the real code, and bulk coverage-padding smoke tests. AGENTS.md explicitly bans these ("Don't write change-detector tests" / "Behavior contracts over snapshots — not freeze a current value (model lists, config version literals, enumeration counts)"). They break on any intentional change, never verify behavior, and add noise to every sweep. + +**What was implemented** (all in `tests/`): + +1. **Signal scan** — a per-file classifier scored all 2,257 test files on change-detector signals (schema-shape pins, literal-list pins, version pins, `len()==N` pins, `assert_called_once_with` payload pins, isinstance/hasattr/is-not-None-only checks). 1,203 files with any signal were split into 102 chunks (63 strong + 39 medium). +2. **Sub-agent triage** — each chunk was reviewed against a written rubric (`TEST_TRIAGE_RUBRIC.md`) with a "when in doubt → KEEP" bias. 1,033 files were kept (behavioral / regression / invariant / E2E / performance tests, incl. wire-contract tests and issue-numbered regression repros). +3. **Deletions** — **8 whole files** (self-referential replica-of-production tests like `test_step_callback_compat.py`, third-party-lib-only tests like `test_xxhash_migration.py` / `test_process_loop_event_loop_warning.py`, pure schema/enum literal-pin files like `test_hindsight_config_schema.py`) and **546 individual tests across 162 files** (~10,467 lines). +4. **Class-aware AST deletion** — `delete_tests2.py` removes whole classes only when ALL of their methods are targeted (and the class has > 0 methods), supports whole-class targets, and falls back to unqualified-name matching for misspelled class prefixes; blank-line runs are collapsed and each edited file is re-parsed before saving. + +**Regression guard.** The full suite still collects (44,538 tests) and every edited file was re-run: **8,809 passed, 398 skipped, 0 failures** across all 162 edited files; a cross-directory 32-file sample also passed (862 passed, 0 failed). Zero cross-file references to deleted modules; zero dangling call references to deleted test names. + +**Should we upstream?** The deletion *criteria* match AGENTS.md's own guidance and could inform upstream test hygiene, but the sweep itself is a fork-maintenance action, not a behavioral patch. `deletion_plan.json` (audit trail of every removed test + reason) and `TEST_TRIAGE_RUBRIC.md` (the rubric) are retained at repo root for review. ### P-049: Terminal output post-processing pipeline + rtk (reasoning toolkit) integration @@ -561,27 +595,57 @@ opening an upstream PR. **Side effects**: The frozen runtime grows by the IM SDKs and their transitive deps (notably the pure-Python `alibabacloud_*` chain). All are pure-Python with cross-platform wheels/sdists — unlike `matrix`'s `python-olm`, which needs a C toolchain and is intentionally still excluded. No change to source installs. **Should we upstream?** No — upstream doesn't build these PyInstaller artifacts. The `cn-desktop` extra and packaging are CN-runtime-specific. -### P-016: PowerShell native execution + runtime-adaptive terminal description +### P-016 / P-019 / P-XXX: PowerShell native execution — Git-Bash→PowerShell migration + pwsh detection -> **Updated by P-019**: P-019 completes the migration by removing all remaining Git Bash discovery logic and targeting **only Windows PowerShell 5.1** (`powershell.exe`). See P-019 below for details. +> P-019 completes the migration P-016 started by removing all remaining Git Bash discovery logic and targeting **only Windows PowerShell 5.1** (`powershell.exe`); P-XXX (landed un-numbered) later restored pwsh-first detection on top. The three are described together below. -**Symptom**: On Windows, the agent was hardcoded to always use Git Bash. PowerShell is faster to start (`-NoProfile`), handles Windows paths natively (no `/c/foo` translation). Additionally, the terminal tool's static `TERMINAL_TOOL_DESCRIPTION` referenced Linux/bash commands that don't exist on native PowerShell. +**Symptom / need.** On Windows, the agent was hardcoded to always use Git Bash. PowerShell starts faster (`-NoProfile`), handles Windows paths natively (no `/c/foo` translation). The terminal tool's static `TERMINAL_TOOL_DESCRIPTION` referenced Linux/bash commands that don't exist on native PowerShell. Upstream's `LocalEnvironment` is bash-only, so there was no PowerShell path at all. -**Root cause**: Upstream's `LocalEnvironment` is bash-only. The terminal tool description is a hardcoded static string assuming a Linux environment. - -**What the patch does**: +**What P-016 did** (superseded by P-019): 1. **`tools/environments/local.py`** — Adds `_resolve_shell()`: on Windows, detects `pwsh.exe` (PS7) first, falls back to `powershell.exe` (PS5.1) or Git Bash. Adds `_run_pwsh()`, `_wrap_command_pwsh()`, overrides `init_session()`, `_run_bash()`, `_wrap_command()`. Respects `HERMES_SHELL_TYPE` and `HERMES_PWSH_PATH`. - 2. **`tools/terminal_tool.py`** — Dynamic description: `_detect_shell_for_description()` + `_build_dynamic_terminal_description()` replace Linux/bash command references with PowerShell cmdlets. - 3. **`model_tools.py`** — Adds `_shell_fp` to `get_tool_definitions()` cache key. +4. **`tools/environments/process_pwsh.py`** — `pwsh_transform()` down-levels PS7+ syntax (`?:`, `??`, `&&`, `||`, `?.`, `?[`) to PS5.1-compatible `if/else`, with warning propagation so the LLM is notified when its PS7 syntax was down-leveled. -4. **`tools/environments/process_pwsh.py`** — `pwsh_transform()` down-levels PS7+ syntax (`?:`, `??`, `&&`, `||`, `?.`, `?[`) to PS5.1-compatible `if/else`, with warning propagation. +**What P-019 did** (completes the migration; Windows PowerShell 5.1 only): -**Side effects**: On Windows, terminal commands now execute in PowerShell. Git Bash auto-install removed, but Python-level bash fallback (`_find_bash()`) remained as a 7-strategy discovery chain. +1. **`tools/environments/local.py`** — Core shell resolution: removes `_find_bash()` (~130 lines, 7 strategies + WSL launcher filter + auto-install with dead `_install_git` import), replaced by a minimal `_find_bash_posix()` for non-Windows only. Renames `_find_pwsh_simple` → `_find_powershell()` (just `shutil.which("powershell.exe") or "powershell.exe"` — no `pwsh.exe` probing). `_resolve_shell()` on Windows always returns `("powershell", path)`; `HERMES_SHELL_TYPE=bash` raises `RuntimeError`; `HERMES_PWSH_PATH` support removed. Renames `_run_pwsh` → `_run_powershell`, `_wrap_command_pwsh` → `_wrap_command_powershell`. **`pwsh_transform` is now always-on** (removes the `if os.path.basename(...).startswith("powershell")` guard). Gates MSYS normalization in `_update_cwd`/`_extract_cwd_from_output` behind `self._shell_type == "bash"`. +2. **`tools/terminal_tool.py`** — Removes the "Windows Git Bash" description branch (dead code); `_detect_shell_for_description()` always returns `"powershell"` on Windows. +3. **`agent/prompt_builder.py`** — Replaces `_WINDOWS_BASH_SHELL_HINT` with `_WINDOWS_POWERSHELL_SHELL_HINT` (PS5.1 syntax: `;` not `&&`, `$env:VAR`, no `?:`/`??`/`?.`). +4. **`cli.py`** — Renames `_normalize_git_bash_path` → `_normalize_msys_path`. +5. **`apps/desktop/electron/main.cjs`** — Replaces `findGitBash()` (~40 lines) with `findPowerShell()` (~15 lines); preflight verifies `powershell.exe`. +6. **`scripts/install.ps1`** — Removes `Install-Git` bash discovery + `Set-GitBashEnvVar` (~210 lines), simplifies `Stage-Git`, adds a defensive `powershell.exe` check, drops all `HERMES_GIT_BASH_PATH` references. +7. **`hermes_cli/uninstall.py`** — Removes `HERMES_GIT_BASH_PATH` from env var cleanup. +8. **`cron/scheduler.py`** — Updates the `.sh`/`.bash` error message (no longer mentions Git for Windows). +9. **Comment cleanup** — `tools/environments/base.py`, `tools/file_operations.py`, `tools/browser_tool.py`: "Git Bash" → "PowerShell" or generic "shell". +10. **Tests** — `test_shell_resolution.py` (rewritten), `test_terminal_dynamic_description.py` (bash-on-Windows test removed), `test_windows_native_support.py`, `test_local_env_windows_msys.py` updated. +11. **Docs** — `windows-native.md`, `environment-variables.md`, `contributing.md` rewritten for PowerShell 5.1. +12. **PowerShell UTF-8 encoding hardening** — `ps_with_utf8()` in `tools/environments/windows_env.py` prepends `[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; $OutputEncoding=[System.Text.Encoding]::UTF8;`; called from `local.py` after `pwsh_transform()`; `encoding="utf-8", errors="replace"` kept only on PowerShell subprocess callers (`hermes_cli/claw.py`, `clipboard.py`, `gateway.py`, `managed_uv.py`); `hermes_bootstrap.py` sets the Windows console code page to CP_UTF8 (65001) with `HERMES_DISABLE_WINDOWS_UTF8=1` escape hatch; `encoding="utf-8"` reverted on all non-PowerShell subprocesses; new tests (`test_clipboard.py::TestClipboardPowershellEncoding`, `test_local_pwsh_warnings.py::TestRunPowershellUtf8Encoding` / `TestPwshTransformAndUtf8Compose`, `test_windows_encoding.py`, `scripts/verify_windows_utf8.py`). + +**What P-XXX did** (pwsh-first follow-up, un-numbered): + +1. **`tools/environments/local.py`** — Adds `_find_pwsh()` multi-step detection (PATH, ProgramFiles, Registry, LocalAppData). `_resolve_shell()` now prefers pwsh over powershell.exe. `_wrap_command_powershell()` skips `pwsh_transform` when running pwsh natively. All dispatch points (`init_session`, `_run_bash`, `_wrap_command`, `_update_cwd`, `_extract_cwd_from_output`) accept both `"powershell"` and `"pwsh"` shell types. +2. **`tools/terminal_tool.py`** — `_detect_shell_for_description()` probes for pwsh and returns `"pwsh"` when found; `_build_dynamic_terminal_description()` has a pwsh variant. +3. **`agent/prompt_builder.py`** — Uses `_WINDOWS_PWSH_SHELL_HINT` when pwsh is available (no PS5.1 limitation warnings). + +**Why we need it**: +- `powershell.exe` (5.1) ships with every Windows 10/11 — zero install, zero download. +- Starts faster than Git Bash, handles Windows paths natively, avoids POSIX-translation overhead. +- P-019 removes ~400 lines of dead code (7-strategy bash discovery, WSL launcher filter, PortableGit auto-install, `HERMES_GIT_BASH_PATH` env var, `HERMES_PWSH_PATH` env var); the agent gets a single, predictable, always-available shell on Windows. P-016's `pwsh.exe` (PS7) probing was unnecessary complexity — 5.1 is universal. +- P-XXX: users with `pwsh` installed previously paid unnecessary transform overhead and PS5.1-limitation warnings for native PS7 features; when pwsh is available, use it directly and skip the down-level transform entirely. -**Should we upstream?** Yes — superseded by P-019 which completes the migration. +**Side effects**: +- `HERMES_SHELL_TYPE=bash` raised a clear `RuntimeError` on Windows (until P-050/P-052/P-054 re-opened it as an explicit opt-in — see the combined P-050/P-052/P-054 entry). +- `HERMES_PWSH_PATH` and `HERMES_GIT_BASH_PATH` env vars are no longer honored. +- All commands go through `pwsh_transform` unconditionally (unless pwsh runs natively, P-XXX). +- PowerShell commands now reliably round-trip non-ASCII output (CJK, emoji, accented characters); non-PowerShell subprocesses remain on the system locale, which is the intended conservative scope. + +**Should we upstream?** Yes. P-019 completes the migration P-016 started and makes Hermes a zero-dependency Windows citizen; P-XXX is a generic follow-up. + +**Sync note (2026-06-27, `chore/sync-upstream-20260627`)**: upstream periodically re-introduces Git Bash machinery on `main`. This sync's `upstream/main` restored the 7-strategy `_find_bash()` in `tools/environments/local.py`, the static terminal description in `tools/terminal_tool.py`, and Git-Bash wording in `website/docs/developer-guide/contributing.md`. The sync **re-asserted P-016/P-019** — kept the fork's PowerShell-only path and grafted only upstream's *independent* fixes on top: `_find_shell()`'s `$SHELL`-preference for POSIX background spawning (#42203, adapted to call the fork's `_find_bash_posix()`); `start_new_session`; install-dir PATH reachability; and in `apps/desktop/electron/main.cjs` the no-console-python helpers (`getNoConsoleVenvPython`/`toNoConsolePython`/`applyWindowsNoConsoleSpawnHints`/`unwrapWindowsVenvHermesCommand`) combined with the fork's async backend resolution (the probes are `async`, so the fork's `await` is required). Future syncs should expect the same Git-Bash drift and resolve the same way. + +**`cli.py` decomposition cleanup (2026-06-27)**: a prior sync had left a large block of methods "restored from upstream CLI decomposition" inline in `HermesCLI`. Upstream now provides those methods via `CLICommandsMixin` / `CLIAgentSetupMixin` (which `HermesCLI` inherits), so this sync dropped the redundant inline copies in favour of upstream's structure (per MAINTAINING.md "if upstream added an equivalent feature, remove the local fork implementation"). The only genuine fork change re-applied to `cli.py` was the P-019 rename; the unused `_new_session_id` helper (0 callers) was dropped. --- @@ -639,74 +703,6 @@ opening an upstream PR. --- -### P-019: Complete Git-Bash-to-PowerShell migration (Windows PowerShell 5.1 only) - -**Symptom**: P-016 added PowerShell support but left the codebase in a hybrid state: `pwsh.exe` (PS7) was probed first, with `powershell.exe` (PS5.1) as fallback, and the 7-strategy `_find_bash()` Git Bash discovery chain (env override → PortableGit → git.exe derivation → registry → PATH → common paths → auto-install) was still present. `HERMES_GIT_BASH_PATH` env var, `HERMES_PWSH_PATH` env var, and `_install_git` import (non-existent module) were all dead or dead-end code. - -**Root cause**: P-016 focused on adding PowerShell as the primary shell but didn't fully remove the Git Bash machinery. The `pwsh.exe` (PS7) requirement was unnecessary — Windows PowerShell 5.1 (`powershell.exe`) ships with every Windows 10/11 system and is always available. - -**What the patch does**: - -1. **`tools/environments/local.py`** — Core shell resolution (Phase 1): - - Removes `_find_bash()` (~130 lines, 7 strategies + WSL launcher filter + auto-install with dead `_install_git` import). Replaces with minimal `_find_bash_posix()` for non-Windows only. - - Removes `_is_windows_wsl_launcher()` helper (no longer needed). - - Renames `_find_pwsh_simple` → `_find_powershell()`: just `shutil.which("powershell.exe") or "powershell.exe"` — no `pwsh.exe` probing. - - Rewrites `_resolve_shell()`: on Windows always returns `("powershell", path)`. `HERMES_SHELL_TYPE=bash` raises `RuntimeError` on Windows. Removes `HERMES_PWSH_PATH` support. - - Renames `_run_pwsh` → `_run_powershell`, `_wrap_command_pwsh` → `_wrap_command_powershell`. - - **`pwsh_transform` is now always-on** — unconditionally applied to every command (removes the `if os.path.basename(...).startswith("powershell")` guard). - - Updates all `"pwsh"` → `"powershell"` references in `init_session`, `_run_bash`, `_wrap_command`. - - Gates MSYS normalization in `_update_cwd`/`_extract_cwd_from_output` behind `self._shell_type == "bash"`. - - Updates comments throughout: `_make_run_env`, `get_temp_dir`, `_msys_to_windows_path`, `_resolve_safe_cwd`. - -2. **`tools/terminal_tool.py`** — Removes "Windows Git Bash" description branch (dead code). Simplifies `_detect_shell_for_description()`: always returns `"powershell"` on Windows. - -3. **`agent/prompt_builder.py`** — Replaces `_WINDOWS_BASH_SHELL_HINT` with `_WINDOWS_POWERSHELL_SHELL_HINT` instructing the agent to use PS5.1 syntax (`;` not `&&`, `$env:VAR`, no `?:`/`??`/`?.`). - -4. **`cli.py`** — Renames `_normalize_git_bash_path` → `_normalize_msys_path`. - -5. **`apps/desktop/electron/main.cjs`** — Replaces `findGitBash()` (~40 lines) with `findPowerShell()` (~15 lines). Updates preflight check to verify `powershell.exe`. - -6. **`scripts/install.ps1`** — Removes `Install-Git` bash discovery + `Set-GitBashEnvVar` (~210 lines). Simplifies `Stage-Git`. Adds defensive `powershell.exe` check. Removes all `HERMES_GIT_BASH_PATH` references. - -7. **`hermes_cli/uninstall.py`** — Removes `HERMES_GIT_BASH_PATH` from env var cleanup. - -8. **`cron/scheduler.py`** — Updates `.sh`/`.bash` error message: no longer mentions Git for Windows. - -9. **Comments cleanup**: `tools/environments/base.py`, `tools/file_operations.py`, `tools/browser_tool.py` — "Git Bash" → "PowerShell" or generic "shell". - -10. **Tests**: `test_shell_resolution.py` (rewritten for new functions), `test_terminal_dynamic_description.py` (removed bash-on-Windows test, updated assertions), `test_windows_native_support.py` (renamed `_normalize_git_bash_path` references, updated cron message expectations), `test_local_env_windows_msys.py` (updated docstrings). - -11. **Docs**: `windows-native.md` (rewrote "How Hermes runs shell commands" section, removed `HERMES_GIT_BASH_PATH` from env var table and installer steps), `environment-variables.md` (replaced `HERMES_GIT_BASH_PATH` with `HERMES_SHELL_TYPE`), `contributing.md` ("Git Bash" → "Windows PowerShell 5.1"). - -12. **PowerShell UTF-8 encoding hardening** — so PowerShell subprocess output is decoded as UTF-8 on Windows: - - Adds `ps_with_utf8()` helper in `tools/environments/windows_env.py` that prepends `[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; $OutputEncoding=[System.Text.Encoding]::UTF8;` to PowerShell commands. Idempotent, no-op on non-Windows. - - Calls `ps_with_utf8()` in `tools/environments/local.py` after `pwsh_transform()`. - - Keeps `encoding="utf-8", errors="replace"` only on PowerShell subprocess callers: `hermes_cli/claw.py`, `hermes_cli/clipboard.py`, `hermes_cli/gateway.py`, `hermes_cli/managed_uv.py`. - - `hermes_bootstrap.py` sets the Windows console code page to CP_UTF8 (65001) and adds `HERMES_DISABLE_WINDOWS_UTF8=1` escape hatch. - - Reverts `encoding="utf-8"` additions on all non-PowerShell subprocesses (tasklist, ssh, docker, ffmpeg, singularity, ripgrep, termux, comfyui auto-fix, git helpers in `scripts/check-windows-footguns.py`, and various tests). - - Adds tests: `tests/tools/test_clipboard.py::TestClipboardPowershellEncoding`, `tests/tools/test_local_pwsh_warnings.py::TestRunPowershellUtf8Encoding` / `TestPwshTransformAndUtf8Compose`, `tests/tools/test_windows_encoding.py`, and `scripts/verify_windows_utf8.py`. - -**Why we need it**: -- `powershell.exe` (5.1) ships with every Windows 10/11 — zero install, zero download. -- Starts faster than Git Bash, handles Windows paths natively, avoids POSIX-translation overhead. -- Removes ~400 lines of dead code (7-strategy bash discovery, WSL launcher filter, PortableGit auto-install, `HERMES_GIT_BASH_PATH` env var, `HERMES_PWSH_PATH` env var). -- Agent now has a single, predictable, always-available shell on Windows. -- P-016's `pwsh.exe` (PS7) probing was unnecessary complexity — 5.1 is universal. - -**Side effects**: -- `HERMES_SHELL_TYPE=bash` now raises a clear `RuntimeError` on Windows. -- `HERMES_PWSH_PATH` and `HERMES_GIT_BASH_PATH` env vars are no longer honored. -- All commands go through `pwsh_transform` unconditionally — PS7+ syntax is always down-leveled. -- PowerShell commands now reliably round-trip non-ASCII output (CJK, emoji, accented characters). Non-PowerShell subprocesses remain on the system locale, which is the intended conservative scope. - -**Should we upstream?** Yes. This completes the migration P-016 started and makes Hermes a zero-dependency Windows citizen. - -**Sync note (2026-06-27, `chore/sync-upstream-20260627`)**: upstream periodically re-introduces Git Bash machinery on `main`. This sync's `upstream/main` restored the 7-strategy `_find_bash()` in `tools/environments/local.py`, the static terminal description in `tools/terminal_tool.py`, and Git-Bash wording in `website/docs/developer-guide/contributing.md`. The sync **re-asserted P-016/P-019** — kept the fork's PowerShell-only path and grafted only upstream's *independent* fixes on top: `_find_shell()`'s `$SHELL`-preference for POSIX background spawning (#42203, adapted to call the fork's `_find_bash_posix()`); `start_new_session`; install-dir PATH reachability; and in `apps/desktop/electron/main.cjs` the no-console-python helpers (`getNoConsoleVenvPython`/`toNoConsolePython`/`applyWindowsNoConsoleSpawnHints`/`unwrapWindowsVenvHermesCommand`) combined with the fork's async backend resolution (the probes are `async`, so the fork's `await` is required). Future syncs should expect the same Git-Bash drift and resolve the same way. - -**`cli.py` decomposition cleanup (2026-06-27)**: a prior sync had left a large block of methods "restored from upstream CLI decomposition" inline in `HermesCLI`. Upstream now provides those methods via `CLICommandsMixin` / `CLIAgentSetupMixin` (which `HermesCLI` inherits), so this sync dropped the redundant inline copies in favour of upstream's structure (per MAINTAINING.md "if upstream added an equivalent feature, remove the local fork implementation"). The only genuine fork change re-applied to `cli.py` was the P-019 rename; the unused `_new_session_id` helper (0 callers) was dropped. - ---- - ### P-024: Empty-content message filtering in `sanitize_api_messages` > Renumbered from a duplicate **P-022** (the streaming stale-stream detector above already owns P-022, and its `cn/P-022-provider-stream-hang` branch + `[CN-fork] P-022` commits back that number; this empty-content patch had no P-022 commits of its own, so it moved to the next free number). @@ -875,7 +871,7 @@ Windows platform now requires PowerShell 7 (`pwsh`) or Windows PowerShell (syste **Note**: `gateway/status.py` was not previously a fork-divergent file, so this is a new behavioral divergence. The mismatch only collapses N frozen desktop runtimes (they share `HERMES_GATEWAY_RUNTIME_DIR`/`LOCK_DIR`); a legacy launchd-venv CLI gateway uses the default lock dir and lives in a separate pid/lock namespace, so a user running BOTH a venv install and the desktop can still double-poll — out of scope for this recognizer fix. **Should we upstream?** The recognition logic is generic frozen-binary support and could be upstreamed; the binary name is CN-desktop-specific. Related: P-014/P-015 (frozen runtime), P-016/P-019. -### P-035: runtime-release gate imports IM adapters from their post-sync plugin location +### P-035b: runtime-release gate imports IM adapters from their post-sync plugin location **Symptom.** `runtime-v0.17.0-cn.3` failed on all four platforms in `release-runtime.yml`, every job dying at the **"Verify platform backends importable (build env)"** step with `ModuleNotFoundError: No module named 'gateway.platforms.feishu'` — before PyInstaller ever ran, so no runtime artifact or GitHub Release was produced. @@ -918,7 +914,7 @@ Windows platform now requires PowerShell 7 (`pwsh`) or Windows PowerShell (syste **Tested.** `tests/tools/test_windows_env.py` (`TestRefreshEnvCache`: 10 calls → 1 read, signature-change re-read, `force` bypass, `None`-signature fail-safe, non-Windows no-op; plus an autouse cache-reset so the existing mocked-registry tests still exercise a real read on Windows). `tests/tools/test_powershell_session.py` (pure-logic `combine_commands`/`PSResult` cross-platform; live: reuse, state persistence, exit codes, cmdlet-only spawn-parity, error/`exit`/timeout recovery, unicode, cwd, batch/combined, context manager; `test_powershell_session_reuse` asserts warm < a fresh spawn; `test_command_batching`). `tests/tools/test_local_pwsh_session.py` (flag resolver via mocking; live execute() basic/reuse/exit-code/cwd/unicode/stdin-fallback/cleanup, and a parametrized session-vs-spawn output+rc parity check). `tests/performance/test_windows_perf.py::test_registry_refresh_cache_effectiveness` asserts the cache collapses 50 warm calls into a single real read. Live tests `skipif` when PowerShell is absent (i.e. off Windows). The default-OFF spawn path is unchanged, so the whole existing terminal suite (`test_terminal_*`, `test_local_pwsh_warnings`, etc.) passes untouched. `tests/tools/test_windows_perf_optimizations.py` covers the plan's four named tests + extras: `test_registry_cache_ttl` (signature cache + max-age TTL + `force`), `test_persistent_powershell_session` (live reuse + state persistence, plus cross-platform pure primitives), `test_cmd_fallback` (classifier + strict-gate contract; resolver gating; live routing proof), `test_crc_verification` (corrupt-CRC and size-mismatch both abort with the original intact, toggle-off skips), and `TestMarkAsTemporary` + `test_mark_temp_files_write_path` (attribute set/clear; the permanent file is never left temporary). Cross-platform tests mock the platform / force the in-process path so they run on the Linux CI slices; live tests `skipif` off Windows. -**Should we upstream?** The registry-refresh cache is a generic Windows correctness/perf fix and should be upstreamed. The PowerShell session reuse is specific to this fork's PowerShell-only Windows shell (P-016/P-019), but the `PowerShellSession` + opt-in-fast-path pattern is self-contained and could be generalised. Related: P-016/P-019 (PowerShell-only Windows shell), P-020 (registry PATH refresh), P-XXX (pwsh-7 detection), P-030/P-033/P-037 (in-process Windows I/O — the other half of cutting spawn frequency). +**Should we upstream?** The registry-refresh cache is a generic Windows correctness/perf fix and should be upstreamed. The PowerShell session reuse is specific to this fork's PowerShell-only Windows shell (P-016/P-019), but the `PowerShellSession` + opt-in-fast-path pattern is self-contained and could be generalised. Related: P-016/P-019 (PowerShell-only Windows shell; pwsh-7 detection), P-020 (registry PATH refresh), P-030/P-033/P-037 (in-process Windows I/O — the other half of cutting spawn frequency). --- diff --git a/FORK_NOTES.zh-CN.md b/FORK_NOTES.zh-CN.md index 6ba3aa144bed6..41e22e3e43764 100644 --- a/FORK_NOTES.zh-CN.md +++ b/FORK_NOTES.zh-CN.md @@ -20,10 +20,10 @@ | **P-001** | `tui_gateway/server.py` | provider 配置 dict/list 不一致修复 | 早期 fork 需要兼容用户配置形态 | 已由上游修复,本 fork 不再携带 | | **P-002** | `hermes_cli/web_server.py` | 增加 `POST /api/upload` 附件上传接口 | desktop / web composer 拖拽上传依赖它 | 未进入上游 | | **P-003** | `hermes_cli/web_server.py` | 去掉 `/api/ws` 的 `_DASHBOARD_EMBEDDED_CHAT_ENABLED` 门禁 | desktop 以 headless dashboard 方式运行,不带 `--tui` 时仍需要 gateway WS | **基本被上游解决** —— v0.16.0(#38591)默认把该标志设为 `True` 并移除了 `--tui`;fork 仍保留 `/api/ws` 上的显式去门禁作为纵深防御 | -| **P-004** | `hermes_cli/web_server.py` | 增加 `GET /api/fs/list` 文件夹浏览接口 | web 工作区选择器需要列目录,避免让用户手输路径 | 未进入上游 | +| **P-004** | `hermes_cli/web_server.py` | 增加 `GET /api/fs/list` 文件夹浏览接口 | web 工作区选择器需要列目录,避免让用户手输路径 | **已与上游收敛** —— 上游后来自己也提供了 `/api/fs/list`(camelCase、无 home 限制);fork 的旧助手已作为死代码删除 | | **P-005** | `hermes_cli/web_server.py` | 增加 `GET /api/mcp-servers` 只读 MCP 列表 | desktop 健康检查需要 MCP 数量,但不能泄露 command/args/env | 可考虑上游 | | **P-006** | `hermes_cli/config.py` | 为 CN provider 注册 `OPTIONAL_ENV_VARS` | 模型设置页需要展示 ARK、QIANFAN、HUNYUAN、SiliconFlow 等密钥项 | CN 专属,通常不向上游提交 | -| **P-007** | `tui_gateway/ws.py` | 捕获并记录 gateway dispatch 异常,返回 JSON-RPC error | 否则前端只看到 WebSocket closed,缺少诊断信息 | 建议上游 | +| ~~**P-007**~~ | `tui_gateway/ws.py` | ~~捕获并记录 gateway dispatch 异常,返回 JSON-RPC error~~ | 否则前端只看到 WebSocket closed,缺少诊断信息 | **已被上游取代** —— 2026-06-04 同步中已删除 | | **P-008** | `hermes_cli/web_server.py`、`hermes_cli/main.py`、档案相关定向测试 | 保留 `GET/PUT /api/profiles/active` 兼容层,并让 Desktop 托管启动信任显式 `HERMES_HOME`,避免命名档案切回默认档案时被旧 sticky 档案重新劫持 | desktop 切换器需要兼容读写 sticky active profile;实时切回默认档案时,旧 `active_profile` 会保留到替换进程就绪,Core 不能把新进程重新导向旧档案 | 上游已提供 GET/POST;其余为 Desktop 兼容与重启语义 | | **P-009** | `hermes_cli/web_server.py`, `tui_gateway/sse.py` | 增加 `/api/v2/events` SSE 和 `/api/v2/rpc` POST transport | ~~desktop 默认使用 EventSource + POST~~ → desktop ≥ 0.4 已改用原生 `/api/ws` WebSocket(与官方桌面端一致),此 transport 只服务旧版外壳 | **已弃用** —— 为 ≤ 0.3.x 旧外壳保留(外壳无自更新而 runtime 热更新),旧外壳 EOL 后移除。不上游。 | | **P-010** | `hermes_cli/config.py` | 注册 `LONGCAT_API_KEY` | CN 模型设置需要 LongCat 密钥入口 | CN 专属,除非上游支持 LongCat | @@ -32,14 +32,14 @@ | **P-013** | `model_tools.py`, `tests/run_agent/test_repair_tool_arg_keys.py` | 在 `handle_function_call` 中增加自动参数键修复:全局别名表、工具级覆盖、模糊匹配、嵌套对象/数组递归修复,以及可选回调通知 | LLM 经常把参数名写错(如 `file`→`path`、`cmd`→`command`),此前会直接报 "unknown parameter";该补丁在不放宽 JSON Schema 的前提下提高工具调用的容错率 | 建议上游 | | **P-014** | `.github/workflows/release-runtime.yml`, `tools/mcp_tool.py`, `hermes_cli/config.py`, `docs/RUNTIME_RELEASES.md`, `tests/tools/test_mcp_tool.py` | 把原生 MCP 客户端 SDK 打进冻结 runtime(安装入口后并入 `cn-desktop` extra,见 P-015;外加 `--collect-submodules/--copy-metadata mcp` + CI 断言 `mcp-*.dist-info` 存在);并让 `discover_mcp_tools()` 在已配置 `mcp_servers` 但 SDK 缺失时输出一次 WARNING,而不是在 debug 级别静默跳过 | issue #16:desktop runtime 打包时缺少 `mcp` extra,导致 `_MCP_AVAILABLE=False`,已配置的 `mcp_servers` 不注册任何工具且 INFO 日志无任何提示。打包改动是 CN 特有,诊断日志与已知根键则是通用改进 | 打包改动 CN 特有;`mcp_tool.py` 告警与 `mcp_servers` 根键建议上游 | | **P-015** | `pyproject.toml`, `.github/workflows/release-runtime.yml`, `docs/RUNTIME_RELEASES.md`, `uv.lock` | 新增 `cn-desktop` 聚合 extra,把冻结 runtime 暴露的所有后端预打包(`web`、`anthropic`、`mcp`、`feishu`、`dingtalk`、`wecom`,以及微信用的 `aiohttp`/`qrcode`/`cryptography`)。发布流程改为安装 `.[cn-desktop]`,收集各 IM SDK 子模块与元数据,新增"构建环境 import 冒烟",并断言每个后端的 `dist-info` 出现在冻结产物中 | 桌面反馈:飞书/钉钉/企微/微信适配器因 SDK(`lark-oapi`、`dingtalk-stream` 等)从未被打包、且冻结环境无法懒安装而静默降级为"不可用"。根因同 P-014,推广到所有桌面后端 | 打包 CN 特有;不上游(上游不构建这些产物) | -| **P-016** | `tools/terminal_tool.py`, `tools/environments/local.py`, `tools/environments/process_pwsh.py`, `tools/environments/base.py`, `model_tools.py`, `tests/tools/test_terminal_dynamic_description.py` | PowerShell 原生执行:Windows 上使用 `pwsh.exe`(PS7)作为主 shell,`powershell.exe`(PS5.1)作为回退,支持完整生命周期管理;删除 Git Bash 自动安装。增加运行时自适应的 terminal 工具描述和 shell 指纹缓存键。增加 pwsh_transform 警告传递 | Windows 上 agent 原本硬编码为 Git Bash;PowerShell 启动更快,路径处理更原生。Git for Windows 自动安装已删除。静态 terminal 描述中的 Linux 命令引用在 PS 下会产生误导 | 被 P-019 取代 | -| **P-019** | `tools/environments/local.py`, `tools/terminal_tool.py`, `agent/prompt_builder.py`, `cli.py`, `apps/desktop/electron/main.cjs`, `scripts/install.ps1`, `hermes_cli/uninstall.py`, `cron/scheduler.py`, `tools/environments/base.py`, `tools/file_operations.py`, `tools/browser_tool.py`, `tests/*`, `website/docs/*`, `FORK_NOTES*.md`、`hermes_bootstrap.py`、`tools/environments/windows_env.py`、`scripts/check-windows-footguns.py`、相关测试、`scripts/verify_windows_utf8.py` | 完成 Git-Bash→PowerShell 迁移:移除全部 Git Bash 发现逻辑(7策略 `_find_bash`)、WSL 启动器过滤和 `HERMES_GIT_BASH_PATH`。Windows 上仅使用 **Windows PowerShell 5.1**(`powershell.exe`,每套 Windows 10/11 自带)——无需 `pwsh.exe`、无需下载、无需安装。`HERMES_SHELL_TYPE=bash` 在 Windows 上抛 RuntimeError。重命名多个函数和变量。`pwsh_transform` 改为始终开启。替换桌面端 `findGitBash` 为 `findPowerShell`。移除安装脚本的 Git Bash 安装逻辑。清理所有 Git Bash 注释、文档和测试;并新增 PowerShell UTF-8 编码加固(`ps_with_utf8()` 辅助函数、控制台 CP_UTF8 引导、仅对 PowerShell 子进程保留 `encoding='utf-8'`)。 | `powershell.exe` (5.1) 每套 Windows 10/11 自带——零安装零下载。比 Git Bash 启动更快,路径处理原生,无需 POSIX 翻译。删除约 400 行死代码(7 策略 bash 发现、WSL 启动器过滤、PortableGit 自动安装)。Agent 在 Windows 上拥有唯一、可预测、始终可用的 shell。P-016 的 `pwsh.exe` 探测是不必要的复杂度——5.1 全覆盖 | 取代 P-016;应上游化 | +| **P-016 / P-019 / P-XXX** | `tools/environments/local.py`、`tools/environments/process_pwsh.py`、`tools/environments/windows_env.py`、`tools/terminal_tool.py`、`agent/prompt_builder.py`、`model_tools.py`、`cli.py`、`apps/desktop/electron/main.cjs`、`scripts/install.ps1`、`hermes_cli/uninstall.py`、`cron/scheduler.py`、`tools/environments/base.py`、`tools/file_operations.py`、`tools/browser_tool.py`、`hermes_bootstrap.py`、`scripts/check-windows-footguns.py`、`scripts/verify_windows_utf8.py`、相关测试、网站文档、`FORK_NOTES*.md` | **Windows shell 演进史——Git Bash → PowerShell。** P-016(已被取代)加入 PowerShell 原生执行:Windows 上 `pwsh.exe`(PS7)为主、`powershell.exe`(PS5.1)回退,完整生命周期(`_run_pwsh`、`_wrap_command_pwsh`、`init_session`、cwd 跟踪),运行时自适应的 terminal 工具描述(把 Linux/bash 命令引用替换为 PowerShell cmdlet),工具定义缓存键加 shell 指纹,`pwsh_transform` 警告传递,并删除 Git Bash 自动安装。P-019 完成迁移:移除全部 Git Bash 发现逻辑(7 策略 `_find_bash`、WSL 启动器过滤、`HERMES_GIT_BASH_PATH`);Windows 上**仅使用 Windows PowerShell 5.1**(`powershell.exe`,每套 Windows 10/11 自带)——无 `pwsh.exe` 探测、无下载、无安装;`HERMES_SHELL_TYPE=bash` 在 Windows 上抛 RuntimeError;重命名 `_find_pwsh_simple`→`_find_powershell`、`_run_pwsh`→`_run_powershell`、`_wrap_command_pwsh`→`_wrap_command_powershell`、`_normalize_git_bash_path`→`_normalize_msys_path`;`pwsh_transform` 始终开启;桌面端 `findGitBash()`→`findPowerShell()`;移除 `Install-Git`/`Set-GitBashEnvVar`/`Stage-Git`;cron 在 Windows 拒绝 `.sh`/`.bash`;PowerShell UTF-8 编码加固(`ps_with_utf8()`、控制台 CP_UTF8 引导、仅 PowerShell 子进程保留 `encoding='utf-8'`)。P-XXX(未编号后续)恢复 **pwsh 优先检测**:`_find_pwsh()` 多步检测(PATH、ProgramFiles、注册表、LocalAppData);`_resolve_shell()` 优先 pwsh;原生 pwsh 时跳过 `pwsh_transform`;所有分发点同时接受 `"powershell"` 与 `"pwsh"`;pwsh 感知的动态终端描述与 `_WINDOWS_PWSH_SHELL_HINT`。 | Windows 上 agent 原本硬编码为 Git Bash;PowerShell 启动更快、路径处理更原生、免 POSIX 翻译,且随每套 Windows 10/11 自带(零安装零下载)。P-019 删除约 400 行死代码,让 agent 在 Windows 上拥有唯一、可预测、始终可用的 shell——P-016 的 `pwsh.exe` 探测是不必要的复杂度(5.1 全覆盖);P-XXX 则让装了 pwsh 的用户完全跳过 PS7→PS5.1 降级转换。 | 应上游化(P-019 取代 P-016;P-XXX 是其后续) | | **P-017** | `agent/tool_dedup.py`, `agent/agent_init.py`, `agent/conversation_loop.py`, `agent/tool_executor.py` | 增加 `ToolDedupTracker`,在跨 API 迭代间检测重复的相同工具调用,并在重复次数达到 3、5、8 次时注入逐级升级的 `` 提示以打破无限循环 | Agent 在处理复杂任务时可能陷入无限循环,反复调用相同工具和参数——现有同轮去重 `_deduplicate_tool_calls` 无法检测跨迭代模式 | 内部机制——解决行为健壮性缺口;机制通用,但集成点与 fork 架构耦合 | | **P-018** | `agent/agent_init.py`, `tests/run_agent/test_init_fallback_on_exhausted_pool.py` | 增加 `_api_key_required` 辅助函数,并在 OpenAI / Anthropic SDK 客户端构造前加入空 key 保护;当 api_key 为空且 provider 需要密钥时,抛出 `RuntimeError: no API key (param empty, env vars unset)` | 此前空 key(参数为空且环境变量未设置)会触发底层 SDK 认证异常,在 TUI/gateway 后台线程中表现为 panic 且无堆栈信息 | 建议上游 | | **P-020** | `tools/environments/windows_env.py`(新建), `tools/environments/local.py`, `hermes_cli/claw.py`, `hermes_cli/managed_uv.py`, `hermes_cli/gateway.py`, `hermes_cli/dep_ensure.py`, `hermes_cli/clipboard.py`, `skills/creative/comfyui/scripts/hardware_check.py` | 新增 `refresh_env_from_registry()` 函数,从 Windows 注册表(HKLM + HKCU)刷新 `os.environ["PATH"]` 和 `os.environ["PATHEXT"]`,在每次 PowerShell 子进程调用前执行,使进程启动后安装的工具(如 WinGet、MSI)可被发现。参考 `kimi-cli/src/kimi_cli/utils/environment.py` 的实现。非 Windows 平台无操作。 | 如果不刷新,agent 无法发现进程启动后安装的二进制文件(例如通过 WinGet 安装的工具)— `shutil.which` 和 `subprocess.Popen` 只能看到进程创建时捕获的 PATH。当 agent 在会话中安装自己的依赖(node、uv 等)时尤其痛苦。 | 建议上游 | | **P-021** | `gateway/run.py`、`cron/scheduler.py`、`cron/jobs.py`、`hermes_time.py` | 四项 cron "静默停摆" 根因修复:(1) `_start_cron_ticker` 初始化包在 try/except 中,防止 daemon 线程静默死亡;(2) 僵尸 `.tick.lock` 自动清理——锁文件 mtime 超过 `lock_stale_seconds`(默认 120s)则删除;(3) `_validate_cron_startup()` 启动前校验 `jobs.json` 可解析性;(4) `_ensure_aware` 按配置时区解释无时区时间戳;修复 `hermes_time.py` 缺失的 `def now()`;每次 tick 调用 `reset_cache()` 使时区配置热生效。 | `jobs.json` 损坏 → ticker 线程崩溃 → daemon 静默死亡。僵尸 `.tick.lock` → 所有后续 tick 永久阻塞。ticker 初始化 `ImportError` → 线程零日志死亡。服务器时区 ≠ 配置时区 → 调度时间静默偏移。 | 建议上游 | | **P-022** | `agent/chat_completion_helpers.py`、`agent/anthropic_adapter.py`、`agent/httpx_clients.py`、`run_agent.py`、`tests/run_agent/test_streaming_stale_timeout.py` | 修复流式 stale-stream 检测器,让静默断掉的服务商连接不可能永久卡死一个回合:(1) 检测器现在中止**活的** transport——`anthropic_messages` 模式下跨线程 `shutdown(SHUT_RDWR)` Anthropic 客户端的 socket(#29507 安全)并重建,而不是只碰 OpenAI 请求客户端(Anthropic 流因此一直挂着);(2) 有界升级:间隔 `HERMES_STREAM_STALE_KILL_GRACE` 的 `HERMES_STREAM_STALE_MAX_KILLS` 次中止后,合成 `TimeoutError` 放弃 daemon worker,而不是重置自己的计时器空转;(3) 卡住期间用**实时** `_emit_status` 上报,不再走回合结束才 flush 的缓冲通道;(4) 给 Anthropic httpx 客户端补 TCP keepalive(与 OpenAI 主客户端对齐),共享 `keepalive_socket_options()`。 | 桌面/网关长会话永久卡死("计时器一直走,任务已经死了"):Anthropic 流静默(半开 socket)后从未被中止,worker 线程阻塞在 `recv()`,检测器重置自己的 `last_chunk_time` 空转,缓冲状态也从不 flush——后端和桌面端都看不到任何错误。 | 建议上游(通用可靠性修复);2026-07 同步:上游自行演化出 close()+重置计时器 的变体,fork 保留 FD-safe socket 击杀 + 有界升级(刻意不重置 last-chunk 时间戳);2026-07-10 v0.18.2 同步:并入上游跨回合 stale 熔断(#58962),熔断计数在 FD-safe 击杀前累加;上游 httpx 连接池主动回收(keepalive_expiry=20s)尚未移植进 `agent/httpx_clients`,待跟进 | | **P-024** | `agent/agent_runtime_helpers.py`、`tests/run_agent/test_agent_guardrails.py`、`tests/run_agent/test_session_meta_filtering.py` | 在 `sanitize_api_messages` 中增加空内容过滤:丢弃 `content` 为 `""` 且无有效载荷的 `assistant`/`user`/`function` 消息;保留仍携带 `tool_calls`、`codex_reasoning_items`、`codex_message_items` 或 `reasoning_content` 的 `assistant` 消息。 | MiMo v2.5 及严格的 OpenAI 兼容网关会拒收空 `content` 消息(HTTP 400 / "text is not set")。长会话(如飞书 3-13h)在上下文压缩/截断后可能留下这类消息。 | 建议上游;2026-07-10 v0.18.2 同步:上游给清洗管线新增三项防御(#58168 损坏参数修复用规范 `call_id||id` 优先级、#58755 空 `tool_calls` 数组规整、#58327 `tool_call_id` 去重),已全部移植进 fork 的单趟融合版 `sanitize_api_messages`(经 `agent.message_utils`,保持不导入 `run_agent` 的性能特性) | +| **P-027** | `cli.py`、`tests/cli/test_cli_save_config_value.py` | `save_config_value()` 只在项目级 `cli-config.yaml` **已存在**时才写入它;否则写入(并创建)**用户**配置——绝不在已安装包/源码树里创建配置文件。 | 旧的 `else project_config_path` 分支在 `HERMES_HOME` 没有 `config.yaml` 时(如测试隔离 home)会创建 `/cli-config.yaml`;并行测试跑批下该文件泄漏给 8 个 worker,污染走项目配置的读取路径(`HERMES_IGNORE_USER_CONFIG` 下的 `load_cli_config`),使 `test_ignore_user_config_flags` 变 flaky。生产环境往包目录写配置同样是错的。 | 建议上游 | | **P-023** | `tui_gateway/server.py` | 网关回合执行器现在会把"漏接"的 `/steer` 作为下一轮用户输入投递。`run_conversation()` 只能把 steer 注入到*后续*的工具结果里;落在最后一个工具批次之后(或纯文本回合)的 steer 会以 `result["pending_steer"]` 返回。`cli.py` 会重新投递它,但网关此前直接丢弃——导致桌面端(运行时输入行为默认 "引导/steer")发出的 steer 静默丢失。仿照已有的 `goal_followup` 链路:在 `finally` 释放 `session["running"]` 后,用 steer 文本发起一次嵌套 `_run_prompt_submit`(受 `running` 保护,真实用户输入优先;优先级高于 goal 续跑)。 | 桌面端反馈(#193):"引导功能不好用……等到任务执行完,我引导的东西也没插入进去"——晚到的 steer 被 `agent.steer()` 接受却从未生效,因为网关忽略了 `pending_steer`。 | 建议上游(通用可靠性修复) | | **P-026** | `hermes_constants.py`、`hermes_bootstrap.py`、`tests/test_managed_runtime_caches.py` | 桌面以托管运行时方式启动时(`HERMES_DESKTOP_MANAGED=1`),`configure_managed_runtime_caches()` 用 `setdefault` 把第三方缓存/临时目录环境变量指向 `/cache` 的子目录:`HF_HOME`、`HUGGINGFACE_HUB_CACHE`、`TORCH_HOME`、`TIKTOKEN_CACHE_DIR`、`MPLCONFIGDIR`、`NLTK_DATA`、`PLAYWRIGHT_BROWSERS_PATH`,以及(仅当三者都未设置时)`TMPDIR/TEMP/TMP`。从 `hermes_bootstrap`(每个入口的第一个 import)调用,确保早于 transformers/tiktoken/playwright 加载。 | Windows 桌面占盘:即使桌面端已把自身运行时树锚定到所选安装盘,这些库仍默认把缓存写进 `~/.cache`(C 盘),导致用户选了 D 盘安装、C 盘照样被撑满。`setdefault` + `HERMES_DESKTOP_MANAGED` 门控让独立 CLI 安装与用户显式覆盖不受影响。 | CN 桌面收敛;环境变量钩子通用,可考虑上游 | | **P-028** | `agent/models_dev.py`、`agent/models_dev_snapshot.json`(新增)、`agent/model_metadata.py`、`hermes_cli/model_cost_guard.py`、`hermes_cli/web_server.py`、`tui_gateway/server.py`、`gateway/slash_commands.py`、`cli.py`、`hermes_cli/auth.py`、`scripts/refresh_models_dev_snapshot.py`(新增)、`pyproject.toml`、`MANIFEST.in`、`.github/workflows/release-runtime.yml` | 让 models.dev 元数据离线优先,使模型保存/切换不再阻塞在网络上。(1) 随包预置 `models_dev_snapshot.json`,并在 `fetch_models_dev` 补上真正的 Stage 0/4 兜底,缓存永不为空。(2) 新增 `allow_network=False` 非阻塞读模式,贯穿 `get_model_capabilities`/`get_model_info`/`lookup_models_dev_context`/`get_model_context_length`/`expensive_model_warning`;所有模型保存/切换热路径(网关 `config.set`、REST `/api/model/set`、`/api/model/info`、`/model` 斜杠命令、CLI 切换、模型保存告警)都走该模式——只读缓存/快照、fail-open。(3) `MODELS_DEV_URL` 与超时可用环境变量覆盖(`HERMES_MODELS_DEV_URL` 国内镜像、`HERMES_MODELS_DEV_TIMEOUT`,默认 15s→3s);`prewarm_models_dev_async` 在 web 启动时后台预热缓存(`HERMES_DISABLE_MODELS_DEV_PREWARM` 可关)。 | 国内访问 `https://models.dev/api.json` 慢/被墙;同步的 15s 超时拉取就卡在 `/models` 页"设为当前模型"/"保存"的关键路径上,而缓存只有请求成功才会写入——于是每次操作都重吃满 15s(桌面端反馈:模型操作要"十几秒")。 | 离线优先快照 + 非阻塞读模式建议上游;国内镜像开关与打包属 CN 专有 | @@ -49,7 +49,7 @@ | **P-032** | `.github/workflows/release-runtime.yml`、`hermes_cli/main.py`、`hermes_cli/colors.py`、`tests/hermes_cli/test_colors_force_color.py`(新增) | 冻结桌面 runtime 载荷内置 Node.js LTS(完整发行版含 npm/npx)与预构建 Ink TUI(`tui/dist/entry.js`):`release-runtime.yml` 先构建 `ui-tui`,把 node 下载并 stage 进 `dist/$NAME/node` 再做 macOS normalize/签名(node 的 Mach-O 由 `sign_macos_runtime_payload.sh` 签名,Ed25519 manifest 签整个 zip 自然覆盖);桌面端经 `HERMES_NODE`/`HERMES_TUI_DIR` 指向内置产物;`colors.py` 支持 `FORCE_COLOR` 保持冻结环境彩色输出。 | 桌面用户机器上没有 Node 时,`hermes --tui` 及一切依赖 Node 的路径不可用;现场安装 Node 慢且易失败。 | CN 桌面发布链路专属,不上游。相关:P-014/P-015、P-028/P-029。 | | **P-033** | `tools/file_operations.py`、`tests/tools/test_file_ops_windows_inprocess.py`(新增) | `ShellFileOperations` 在**本地 Windows** 后端把磁盘 I/O 改为**进程内** Python stdlib,不再 shell-out POSIX `wc`/`sed`/`head`/`mktemp`/`cat`/`ls`:新增 `_prim_stat_size`/`_prim_read_sample`/`_prim_read_all`/`_prim_read_page`/`_prim_count_lines`/`_prim_list_dir`/`_prim_mkdirs` 磁盘原语与 `_local_atomic_write`(临时文件 + `os.replace`),由 `_use_inproc_io()`(`_IS_WINDOWS and _is_local_env()`)门控;非 Windows 本地与所有远程后端走完全不变的 shell 路径。 | P-016/P-019 之后 Windows 唯一 shell 是 PowerShell 5.1,这些 POSIX 工具不存在,文件工具在 Windows 本地后端大面积失效。 | 建议上游(Windows 正确性)。相关:P-030、P-033b、P-037。 | | **P-034** | `gateway/status.py`、`tests/gateway/test_gateway_command_line_matcher.py` | 网关进程识别器 `_gateway_command_subcommand` 把 argv[0] basename 以 `hermes-agent-cn-runtime` 开头的命令行(冻结桌面 PyInstaller 二进制,如 `hermes-agent-cn-runtime-win32-x64.exe`)识别为 hermes CLI 入口;只加进 `has_gateway_entry`(保留真实 `gateway` 子命令解析),不进 dedicated-entrypoint 扫描(后者无条件返回 `run`,会误读冻结版的 `gateway status`/`stop`/`restart`)。 | 桌面端以 ` gateway run --replace` 运行冻结 runtime,旧识别器只认 `hermes_cli.main`/`hermes`/`hermes-gateway`——`get_running_pid()` 把活网关当"非网关",删其 `gateway.pid`/`gateway.lock`,`--replace`、防重实例锁、微信 token 锁的过期判定全部失效,多网关竞争同一 iLink 会话(#42 周期性 Session expired/未连接)。Rust 侧本就按 `"gateway run"` 子串识别,Core/Desktop 对"什么是网关"曾判断不一致。 | 以通用"冻结二进制识别"形式可上游。相关:P-014/P-015、P-016/P-019。 | -| **P-035** | `.github/workflows/release-runtime.yml`、`tests/test_runtime_release_workflow.py` | 运行时发布构建环境门("Verify platform backends importable")改从插件迁移后的位置导入飞书/钉钉/企微适配器(`plugins.platforms.feishu.adapter`、`plugins.platforms.dingtalk.adapter`、`plugins.platforms.wecom.adapter` + `callback_adapter`);weixin(微信个人号,CN 专属)仍从 `gateway.platforms.weixin` 导入。新增 pytest 回归测试在每次 CI 从 `plugins/platforms/` 加载各迁移后适配器,并断言门里不再引用被删模块。 | 上游同步(PR #57,`560010547`)把 IM 适配器从 `gateway/platforms/*.py` 迁到 bundled 插件后,CN 发布门仍 `import gateway.platforms.feishu`,所有平台的 `release-runtime.yml` 在 PyInstaller 之前就 ModuleNotFoundError——`runtime-v0.17.0-cn.3` 因此挂掉;该门只在 `runtime-v*` tag 上运行,常规 CI 从不触达,新测试把检查搬进每次 CI。 | CN 发布工具专属,不上游。相关:P-014/P-015、P-028/P-029/P-032。 | +| **P-035b** | `.github/workflows/release-runtime.yml`、`tests/test_runtime_release_workflow.py` | 运行时发布构建环境门("Verify platform backends importable")改从插件迁移后的位置导入飞书/钉钉/企微适配器(`plugins.platforms.feishu.adapter`、`plugins.platforms.dingtalk.adapter`、`plugins.platforms.wecom.adapter` + `callback_adapter`);weixin(微信个人号,CN 专属)仍从 `gateway.platforms.weixin` 导入。新增 pytest 回归测试在每次 CI 从 `plugins/platforms/` 加载各迁移后适配器,并断言门里不再引用被删模块。 | 上游同步(PR #57,`560010547`)把 IM 适配器从 `gateway/platforms/*.py` 迁到 bundled 插件后,CN 发布门仍 `import gateway.platforms.feishu`,所有平台的 `release-runtime.yml` 在 PyInstaller 之前就 ModuleNotFoundError——`runtime-v0.17.0-cn.3` 因此挂掉;该门只在 `runtime-v*` tag 上运行,常规 CI 从不触达,新测试把检查搬进每次 CI。 | CN 发布工具专属,不上游。相关:P-014/P-015、P-028/P-029/P-032。 | | **P-036** | `tui_gateway/server.py`、`tests/gateway/test_provider_models_rpc.py`(新增) | 新增 `provider.models` RPC:返回服务商**完整** `/models` id 列表(probe 只采样 5 个)且**容忍空 api_key**(本地自建服务不需要密钥);与 `provider.probe` 共用的 URL 候选抓取重构为 `_fetch_provider_model_ids`,probe 的响应逐字节不变。 | 桌面端反馈:局域网自建 Ollama(`http://192.168.31.11:11434/v1`)"测试连接"通过(走后端 probe),但模型选择器"刷新"失败——桌面直连走 SSRF 防护的 `external_request` 代理,拦非环回私网 IP 的 http。改由后端列模型(与 probe 同路径)让 LAN/自建服务商可刷新,也绕开 web 壳的浏览器 CORS。 | 可上游(P-011 `provider.probe` 的姊妹) | | **P-037** | `tools/environments/local.py`、`tools/file_operations.py`、`tests/tools/test_local_pwsh_warnings.py`、`tests/tools/test_file_ops_p037.py`(新增) | 对 P-016/P-019/P-030/P-033 的三个 Windows 进程内 I/O 正确性收尾。**(1)** 把 `pwsh_transform`(PS7→PS5.1 降级)的调用从 `_run_powershell`(拿到的是**拼装后**的 wrapper)移到 `_wrap_command_powershell`,在用户命令被嵌入单引号 `Invoke-Expression ''` 字面量**之前**对**原始**命令做降级。**(2)** 把 `patch_replace` 的读取、写后校验重读、以及 `_check_lint` 的磁盘读改走 `_prim_read_all`,不再直接 `cat … 2>/dev/null` shell-out。**(3)** 新增 `_decode_file_bytes` + 模块级 `_INPROC_FALLBACK_ENCODINGS`(Windows 上 = `("mbcs",)`),用于进程内整文件读取(`_prim_read_all`、`_prim_read_page`):先试 UTF-8,再试系统 ANSI 代码页,最后才有损替换。 | **(1)** `pwsh_transform` 的 region mask 会跳过单引号字符串内容,所以对 wrapper 做降级根本碰不到用户命令——这条 PS7→PS5.1 关键兼容桥在真实执行路径上是**静默 no-op**,PS5.1 的 `Invoke-Expression` 遇到 `&&`/`||`/`??`/三元会直接 ParserError;单元测试因直接调 `pwsh_transform` 才误以为通过。**(2)** 在 PowerShell 5.1(P-016/P-019:Windows 唯一 shell)下没有 POSIX `cat`、`2>/dev/null` 会被当作写入字面文件 `\dev\null`,所以 `patch_replace` 很可能在 P-030/P-033 主打的平台上直接坏掉。**(3)** 被替换的 shell 读取按系统代码页解码;硬编码 UTF-8 会把 GBK/cp936 文件(中文 Windows 常态,本 fork 受众)的每个非 ASCII 字节变成 U+FFFD,而一次 读→patch→写 回环会把这种损坏**持久化**(静默数据丢失)。 | 三条都是通用跨平台正确性修复,应上游;紧迫性是 CN-fork 专有(P-016/P-019 让 PowerShell 5.1 成为 Windows 唯一 shell)。相关:P-016/P-019(PowerShell-only)、P-030/P-033(进程内 I/O)。 | | **P-033b** | `tools/file_operations.py`、`tests/tools/test_file_ops_windows_inprocess.py`、`tests/tools/test_file_tools_live.py`、`tests/tools/test_file_operations.py` | 给 `write_file` 加后端无关的写后验证:`_atomic_write` 报成功后用 `_prim_read_all` 重读并与目标内容比对(剥 BOM、归一化行尾);`_local_atomic_write` 返回前也验证文件存在且字节一致。堵住"写入器 exit 0 但字节未落盘"的静默成功窗口。 | P-033 修了 PowerShell/POSIX 不匹配,但 `write_file` 仍只信写入器退出码;剩余边缘情况(后端 FS 怪癖、竞态、管道截断)仍可能文件缺失/未变却报成功。 | 建议与 P-033 一起上游。 | @@ -57,9 +57,10 @@ | **P-039** | `agent/auxiliary_client.py`、`hermes_cli/config.py`(auxiliary 文案)、`hermes_cli/main.py`、`tests/agent/test_auxiliary_client.py`、`tests/agent/test_auxiliary_main_first.py` | 辅助模型 "auto" 解析**从不隐式探测** OpenRouter 与 Nous Portal。文本链:主 provider → 本地/自定义端点 → 直连 API-key 服务商 → None(`_get_provider_chain` 去掉 openrouter/nous 两级)。视觉 auto 只回退原生 Anthropic(`_VISION_AUTO_PROVIDER_ORDER = ("anthropic",)`),`_VISION_EXPLICIT_PROVIDER_ORDER` 负责显式请求与主 provider 严格后端。用户主 provider 是 OpenRouter/Nous、或显式 `auxiliary..provider` 指定时仍正常可用。 | 国内用户通常连不上 openrouter.ai / Nous Portal;隐式回退探测给每次压缩/标题生成/视觉调用平添死等超时,辅助任务"慢慢失败"而不是快速回退到可达服务商。 | CN 专属默认,不上游。2026-06-07(`bc40674ac`)落地时未编号;v0.18.0 同步中险些被合并丢弃后补登记。 | | **P-040** | `hermes_cli/web_server.py`、`tests/hermes_cli/test_web_server_platforms_offload.py`(新增) | `/api/messaging/platforms` 的目录构建改走 `run_in_executor`(且移出 profile scope,scope 内保持无 await);lifespan 启动时把 `_warm_platform_registry` 发进工作线程预热(与 `_warm_gateway_module` 同款)。 | `_messaging_platform_catalog()` 会触发平台插件发现,首次调用同步 import 所有内置 IM 适配器——仅 discord.py 冷启动就 10s+——而且是在 async handler 里**内联**执行。桌面端首屏就调这个端点,事件循环被卡死、所有启动期 API 排队:每次 runtime 更新后(新进程)用户会看到 15s+ 的白屏/"连接中";Playwright E2E 对全新 v0.18.0 后端也因 15s 断言窗口而失败。 | 建议上游(官方桌面/dashboard 同样中招;与上游自己的冷启动 offload 惯例 #54448/#54523 同款)。 | | **P-041** | `agent/agent_init.py`、`agent/conversation_loop.py`、`tui_gateway/server.py`、`apps/desktop/src/app/session/hooks/use-message-stream/gateway-event.ts`、`apps/desktop/src/lib/chat-messages.ts`、`hermes_cli/config.py`、`tests/run_agent/test_tool_call_streaming_convergence.py`(新增)、`tests/tui_gateway/test_tool_call_committed_event.py`(新增)、`apps/desktop/src/lib/chat-messages.test.ts` | 修复 Windows 桌面端"工具调用后卡死":当 `write_file` 工具调用前无正文、随后紧跟 `terminal` 工具调用时,UI 可能卡在"正在运行终端命令"无法恢复。新增显式的 `assistant.tool_calls_committed` 事件、会话级事件追踪、回合不活动看门狗、强化相邻工具调用匹配,并吞掉多余的 `None` 流式增量。 | 桌面端流状态机从 `message.start` 启动,期望收到文本增量或最终的 `message.complete`。纯工具调用的助手消息两者都不提供,导致连续工具调用时 UI 在 `tool.start` 边界上失去清晰状态切换。 | 建议上游 | -| **P-XXX** | `tools/environments/local.py`、`tools/terminal_tool.py`、`agent/prompt_builder.py`、`tools/environments/base.py`、`tools/environments/windows_env.py`、`tests/tools/test_shell_resolution.py`、`tests/tools/test_terminal_dynamic_description.py`、`tests/tools/test_local_pwsh_warnings.py` | **优先探测 `pwsh`(PowerShell 7),回退到 PowerShell 5.1。** 新增 `_find_pwsh()` 多步检测(PATH、ProgramFiles、注册表、LocalAppData)。`_resolve_shell()` 现在优先选择 pwsh 而非 powershell.exe。`_wrap_command_powershell()` 在使用 pwsh 原生执行时跳过 `pwsh_transform`。所有调度点(`init_session`、`_run_bash`、`_wrap_command`、`_update_cwd`、`_extract_cwd_from_output`)同时接受 `"powershell"` 和 `"pwsh"` 类型。`_detect_shell_for_description()` 探测 pwsh 并在找到时返回 `"pwsh"`。`_build_dynamic_terminal_description()` 包含 pwsh 变体。`prompt_builder.py` 在可用时使用 `_WINDOWS_PWSH_SHELL_HINT`(不含 PS5.1 限制警告)。 | P-016/P-019 硬编码为 `powershell.exe`(PS5.1)并始终运行 `pwsh_transform` 降级 PS7 语法。安装了 pwsh 的用户不必要地承受了转换开销和原生 PS7 特性的警告。当 pwsh 可用时,应直接使用它并完全跳过降级转换。 | 建议上游(P-016/P-019 的后续) | -| **P-042** | `tools/environments/windows_env.py`、`tools/environments/powershell_session.py`(新增)、`tools/environments/local.py`、`hermes_cli/config.py`、`tests/tools/test_windows_env.py`、`tests/tools/test_powershell_session.py`(新增)、`tests/tools/test_local_pwsh_session.py`(新增)、`tools/file_operations.py`、`tests/tools/test_windows_perf_optimizations.py`(新增)、`tests/performance/test_windows_perf.py` | **子进程 spawn + 写路径开销(性能热点 #6)。** Windows 终端 spawn + 进程内写路径优化。**(1) 注册表刷新缓存:** `refresh_env_from_registry()`(P-020)此前在**每次** PowerShell spawn 前都重读 HKLM+HKCU 的 `Path`/`PATHEXT`,现在按两个 Environment 键的最后写入时间签名(`QueryInfoKey`)缓存——签名未变时跳过读取+`%展开%`+合并,一旦安装工具改动键的 mtime 立刻重读,因此对新装工具**没有陈旧窗口**(不像固定 TTL)。新增 `force=` 与 `_reset_registry_env_cache()`。**(2) 复用 PowerShell 会话(可选开启):** 新增 `PowerShellSession`,通过 stdin 向同一个常驻 `powershell/pwsh -Command -` 喂多条命令(base64 包裹 + marker 终止,`try/catch` 保证 marker 必然发出),暖命令 ~1-5ms,而非一次 spawn 的 ~80-100ms。`LocalEnvironment.execute()` 在 `terminal.powershell_session_reuse`(由内部 `HERMES_PWSH_SESSION_REUSE` 环境变量桥接)开启时走会话,逐命令重置 `$LASTEXITCODE` 以与 spawn 完全一致的退出码、逐命令刷新 `$env:PATH` 以保持 P-020 语义;需要 stdin 的命令及任何失败都回退到不变的 spawn 路径。默认**关闭**(会话会在命令间保留 shell 状态)。**(3) cmd.exe 快路径(可选开启,`terminal.cmd_fast_path`):** 一小撮无元字符的平凡内建(`dir`/`echo`/`type`/`copy`/`move`/`del`/`mkdir`/`rmdir`/`whoami`/`ver`…)改走一次性 `cmd.exe /c`(~10-20ms)而非 `powershell.exe`(~80-100ms)。`is_simple_command()` 粗分类,`_cmd_fast_path_eligible()` 严格执行门(额外拒绝任何 shell 元字符与改 cwd/env 的内建),保证路由后与 PowerShell 逐字节一致;输出按 UTF-8→系统 ANSI 代码页解码(兼容中文),退出码直取进程。默认**关闭**且只在会话路径未接手时才考虑——会话复用更快(暖 ~1-5ms)且保留 PowerShell 语义,cmd.exe 只是无状态 spawn 选项;不合格命令一律回退不变的 spawn 路径。**(4) CRC-32 写后校验:** `_local_atomic_write`(P-033)在原子 `os.replace` 前重读刚写的临时文件,用流式 CRC-32 + 字节长度与预期比对,不一致则中止且原文件不动——抓得到"大小相同但内容损坏"(同长度位翻转,调用方仅 stat 大小的 P-033b 看不到)。廉价(临时文件在页缓存,比两个 4 字节摘要而非再规范化整份内容做 `==`)。由 `_WRITE_VERIFY_CRC`(`HERMES_WRITE_VERIFY_CRC`,默认开)门控;承重的调用方 `_prim_stat_size` 校验保留,是加强而非替换。**(5) FILE_ATTRIBUTE_TEMPORARY 提示(可选开启):** `set_file_temporary()`/`mark_as_temporary()`(windows_env.py)给临时文件打 temporary 属性;由 `_MARK_TEMP_FILES`(`HERMES_MARK_TEMP_FILES`,默认关)接入 `_local_atomic_write`:创建后打标、**改名前清标**,落地的永久文件绝不残留 temporary。非 Windows 无操作。 | 每次 Windows PowerShell spawn 在终端热路径上要付 ~31-100ms+ 的进程创建 + DLL 加载 + 解释器初始化(root-cause-analysis.md 热点 #6);P-020 的注册表刷新又给每次调用加了 ~5-15ms 且无缓存。缓存刷新 + 复用解释器可在暖路径上去掉这两项。 | 注册表刷新缓存是通用 Windows 修复,应上游;会话复用是 Windows-shell 专有(建立在 P-016/P-019 的 PowerShell-only 之上),但模式可推广。相关:P-016/P-019(Windows 唯一 shell)、P-020(注册表 PATH 刷新)、P-XXX(pwsh 检测)。 | +| **P-042** | `tools/environments/windows_env.py`、`tools/environments/powershell_session.py`(新增)、`tools/environments/local.py`、`hermes_cli/config.py`、`tests/tools/test_windows_env.py`、`tests/tools/test_powershell_session.py`(新增)、`tests/tools/test_local_pwsh_session.py`(新增)、`tools/file_operations.py`、`tests/tools/test_windows_perf_optimizations.py`(新增)、`tests/performance/test_windows_perf.py` | **子进程 spawn + 写路径开销(性能热点 #6)。** Windows 终端 spawn + 进程内写路径优化。**(1) 注册表刷新缓存:** `refresh_env_from_registry()`(P-020)此前在**每次** PowerShell spawn 前都重读 HKLM+HKCU 的 `Path`/`PATHEXT`,现在按两个 Environment 键的最后写入时间签名(`QueryInfoKey`)缓存——签名未变时跳过读取+`%展开%`+合并,一旦安装工具改动键的 mtime 立刻重读,因此对新装工具**没有陈旧窗口**(不像固定 TTL)。新增 `force=` 与 `_reset_registry_env_cache()`。**(2) 复用 PowerShell 会话(可选开启):** 新增 `PowerShellSession`,通过 stdin 向同一个常驻 `powershell/pwsh -Command -` 喂多条命令(base64 包裹 + marker 终止,`try/catch` 保证 marker 必然发出),暖命令 ~1-5ms,而非一次 spawn 的 ~80-100ms。`LocalEnvironment.execute()` 在 `terminal.powershell_session_reuse`(由内部 `HERMES_PWSH_SESSION_REUSE` 环境变量桥接)开启时走会话,逐命令重置 `$LASTEXITCODE` 以与 spawn 完全一致的退出码、逐命令刷新 `$env:PATH` 以保持 P-020 语义;需要 stdin 的命令及任何失败都回退到不变的 spawn 路径。默认**关闭**(会话会在命令间保留 shell 状态)。**(3) cmd.exe 快路径(可选开启,`terminal.cmd_fast_path`):** 一小撮无元字符的平凡内建(`dir`/`echo`/`type`/`copy`/`move`/`del`/`mkdir`/`rmdir`/`whoami`/`ver`…)改走一次性 `cmd.exe /c`(~10-20ms)而非 `powershell.exe`(~80-100ms)。`is_simple_command()` 粗分类,`_cmd_fast_path_eligible()` 严格执行门(额外拒绝任何 shell 元字符与改 cwd/env 的内建),保证路由后与 PowerShell 逐字节一致;输出按 UTF-8→系统 ANSI 代码页解码(兼容中文),退出码直取进程。默认**关闭**且只在会话路径未接手时才考虑——会话复用更快(暖 ~1-5ms)且保留 PowerShell 语义,cmd.exe 只是无状态 spawn 选项;不合格命令一律回退不变的 spawn 路径。**(4) CRC-32 写后校验:** `_local_atomic_write`(P-033)在原子 `os.replace` 前重读刚写的临时文件,用流式 CRC-32 + 字节长度与预期比对,不一致则中止且原文件不动——抓得到"大小相同但内容损坏"(同长度位翻转,调用方仅 stat 大小的 P-033b 看不到)。廉价(临时文件在页缓存,比两个 4 字节摘要而非再规范化整份内容做 `==`)。由 `_WRITE_VERIFY_CRC`(`HERMES_WRITE_VERIFY_CRC`,默认开)门控;承重的调用方 `_prim_stat_size` 校验保留,是加强而非替换。**(5) FILE_ATTRIBUTE_TEMPORARY 提示(可选开启):** `set_file_temporary()`/`mark_as_temporary()`(windows_env.py)给临时文件打 temporary 属性;由 `_MARK_TEMP_FILES`(`HERMES_MARK_TEMP_FILES`,默认关)接入 `_local_atomic_write`:创建后打标、**改名前清标**,落地的永久文件绝不残留 temporary。非 Windows 无操作。 | 每次 Windows PowerShell spawn 在终端热路径上要付 ~31-100ms+ 的进程创建 + DLL 加载 + 解释器初始化(root-cause-analysis.md 热点 #6);P-020 的注册表刷新又给每次调用加了 ~5-15ms 且无缓存。缓存刷新 + 复用解释器可在暖路径上去掉这两项。 | 注册表刷新缓存是通用 Windows 修复,应上游;会话复用是 Windows-shell 专有(建立在 P-016/P-019 的 PowerShell-only 之上),但模式可推广。相关:P-016/P-019(Windows 唯一 shell;含 pwsh 检测)、P-020(注册表 PATH 刷新)。 | | **P-043** | `model_tools.py`、`tools/registry.py`、`run_agent.py`、`cli.py`、`tests/performance/test_tool_dispatch.py`、`tests/tools/test_registry_schema_json.py`(新增) | **首次分发延迟(性能热点 #8/#9)。** 把 ~4,486ms 的冷启动首次分发开销挪出用户可见热路径。新增 `warm_dispatch_path()`——一个幂等、线程安全、发射即忘的预热原语:完成延迟发现、为某个工具集选择构建并缓存 schema 目录、并预序列化每个工具的 schema。接入 `AIAgent.warmup()`/`awarmup()` 以及 CLI 横幅空闲期预热(现统一走该原语)。新增 `registry.get_schema_json()`——按条目惰性计算的 JSON 缓存(无导入期成本,重新注册即失效)。 | 首次工具分发/首次 API 请求会同步触发完整工具发现(模块导入 + `check_fn` 探测 + schema 组装)——冷 ~4,486ms vs 暖 ~2ms,导致第一次工具调用像卡死(root-cause-analysis.md 热点 #8/#9)。 | 预热原语 + 惰性 schema-JSON 缓存是通用的,应上游;冷启动量级为 Windows/py3.14 专有。相关:P-040(冷启动卸载)、P-042(子进程 spawn 开销)。 | +| **P-044** | `platform_utils.py`(新增)、`hermes_cli/config.py`、`hermes_cli/dep_ensure.py`、`tools/code_execution_tool.py`、`tools/environments/powershell_session.py`、`tools/environments/local.py`、`tools/process_registry.py`、`plugins/platforms/whatsapp/adapter.py`、`agent/prompt_builder.py`、`agent/ssl_guard.py`、`tools/environments/windows_env.py`、`hermes_cli/doctor.py`、`scripts/precompile.py`(新增)、`pyproject.toml`、`tests/tools/test_wmi_ssl_windows_overhead.py`(新增)、`tests/agent/test_ssl_ca_guard.py` | **agent 初始化期的 WMI + SSL + import 开销(.plans/15,热点 `_wmi.exec_query` 2.91% / `_ssl.set_default_verify_paths` 8.19%)。** **(1) WMI:** Python 3.12+ 上 `platform.system()`/`platform.release()` 会构造 `platform.uname()`,后者在 Windows 上发起 `_wmi.exec_query`(`win32_ver` + `_get_machine_win32`,~45ms)。多个模块在**模块作用域**执行 `_IS_WINDOWS = platform.system() == "Windows"`,导入级联里 WMI 探测被付了两次,`prompt_builder` 的 `platform.release()` 每次初始化构建 system prompt 时又付一次。新的无 WMI `platform_utils`(`is_windows()` 走 `sys.platform` 常量、`windows_release()` 走 `sys.getwindowsversion()`)替换模块级标志与主机行;导入级联与 prompt 构建现在 **0** 次 WMI 查询(已验证)。**(2) SSL:** `verify_ca_bundle()`(每次 `AIAgent` 构造都跑,agent_init.py)每次重建一次性 `ssl.create_default_context()`(Windows 上 ~225ms);现在按 CA 环境变量 + certifi bundle(path/size/mtime)的廉价指纹缓存成功结论,未变时每个进程只校验一次(重复 ~0.05ms),任何变更都会重新校验并重新抛出。**(3) import:** `scripts/precompile.py`(`compileall`)把 `.pyc` 预热移出热路径(源码运行/CI/更新后),首次 import 不再付 `builtins.compile` + `_io.open_code` + Defender 扫描新写的 `.pyc`。**(4) Defender 提示:** `suggest_defender_exclusion()` 经 `hermes doctor` 给出 HERMES_HOME 排除建议(仅提示,不改任何设置)。 | Windows/Python 3.12+ 上无害的 `platform.system()`/`release()` 惯用法会悄悄访问 WMI 服务(每次冷调 ~40-90ms),而 SSL guard 每次构造 agent 都重载 CA 存储——网关批量起 agent/subagent 时两者都被重复支付。计划的字面 `import wmi` / `pywin32` 前提不成立(本树没有 `wmi` 包);真正的 WMI 来源是 stdlib `platform` 模块,因此修复是移除 WMI 触发点而非懒加载不存在的依赖。 | `platform_utils` 与 ssl_guard 记忆化是 provider/OS 通用改进,应上游(处处正确;WMI *成本*是 py3.12+ Windows 专属)。相关:P-042(Windows 子进程 spawn 开销)、P-043(首次分发延迟)。 | +| **P-045** | `import_accelerator.py`(新增)、`hermes_bootstrap.py`、`run_agent.py`、`scripts/precompile.py`、`pyproject.toml`、`agent/message_utils.py`、`agent/agent_runtime_helpers.py`、`tools/registry.py`、`tests/test_import_accelerator.py`(新增)、`tests/test_precompile.py`(新增)、`tests/agent/test_message_utils.py` | **Flame-graph import 系统优化(.plans/16,import 系统约占冷启动 agent-init 的 71%)。** **(1) 一方 import 加速器:** `sys.meta_path` finder(`import_accelerator`)用一次 dict 查找解析 Hermes 自身*精选*的顶层模块/包(集合镜像 pyproject `py-modules` + `packages.find`),跳过逐条 `sys.path` 目录扫描(`nt.stat` / `nt._path_exists`)。包优先于模块(仓库根 `agent.py` harness 永不遮蔽真正的 `agent/` 包)、`.pyc` 保留的 `spec_from_file_location`、其他名字 O(1) 透传、`HERMES_DISABLE_IMPORT_ACCELERATOR` 可关。从 `hermes_bootstrap`(每个入口的第一个 import)安装,并从 `run_agent` 幂等安装。映射构建期校验一次,不做逐 resolve 的 re-stat,因此绝不拖慢暖树(实测中性到更快)。**(2) precompile 幂等:** `scripts/precompile.py` 新增 `precompile_if_needed`(按源码+解释器指纹盖章)、`precompile_in_background`(daemon 线程)、`[tool.hermes.precompile]` 目标读取与 `--if-needed`/`--force`;可选的 `HERMES_PRECOMPILE_ON_START` 后台预热(默认关,pytest/冻结下 no-op)把 `builtins.compile`(冷启动 ~10.82%)移出源码运行布局的热路径。**(3) 每请求微优化:** `sanitize_api_messages`(每次 LLM 调用前都跑)把每个 tool_call 的两次类型分发合并为一次 `get_tool_call_function_and_id`;`registry.get_definitions` 只在短暂锁内快照*请求的*条目,不再每次物化整个 ~250 工具注册表。 | import 系统约占冷启动 agent-init 的 71%。计划字面的 `type()` 换 `isinstance` 被基准否定(成本相同;组合形式更慢),其"34.4 万次 isinstance 调用"是第三方**import 期**类构造(pydantic/SDK),对策是**不急切导入**(懒代理 + 懒工具索引 + 本加速器),而非改写我们自己的 isinstance 调用。因此修复打在真正的杠杆上:跳过冗余路径扫描、预编译字节码、砍掉真正冗余的每请求分发。 | import 加速器、precompile 幂等与 sanitizer/registry 微优化 OS 通用,应上游;冷启动*量级*是 Windows/源码运行专属。相关:P-043(首次分发延迟)、P-044(WMI/SSL/import 开销)。 | | **P-046** | `tui_gateway/server.py`、`tests/gateway/test_provider_models_rpc.py` | `provider.probe` / `provider.models` 新增可选 `api_mode` 参数:`"anthropic_messages"` 时共享的 `_fetch_provider_model_ids` 切换到 Anthropic 协议——URL 候选镜像 SDK 自动追加 `/v1/messages` 的规则(裸域名 → `{base}/v1/models`,嵌套路径回退 host 根),鉴权改用 `x-api-key` + `anthropic-version`(对齐 `hermes_cli.models.probe_api_models`)而非 `Authorization: Bearer`;未知/缺省 `api_mode` 行为逐字节不变。 | CN 桌面端供应商目录新增了一批 Anthropic 协议的 Claude Code 中转站(PackyCode、AICodeMirror 等)与 MiniMax `/anthropic` 端点;它们的「测试连接」走 OpenAI 风格探测(Bearer + `/models` 启发式),严格的 Anthropic 网关会拒绝——有效密钥被误报为鉴权失败。 | 或可上游(P-011/P-036 的姊妹) | | **P-047** | `tui_gateway/cli_delegation.py`(新增)、`tui_gateway/server.py`、`skills/autonomous-ai-agents/claude-code/SKILL.md`、`skills/autonomous-ai-agents/codex/SKILL.md`、`tests/tui_gateway/test_cli_delegation_classifier.py`(新增)、`tests/tui_gateway/test_cli_delegation_events.py`(新增)、`tests/tui_gateway/test_cli_delegation_stream.py`(新增) | 新增三个 gateway 事件——`delegation.cli.started` / `delegation.cli.output` / `delegation.cli.completed`——识别经 `terminal` 工具委派给外部编码代理 CLI 的调用(Claude Code `claude -p …` / Codex `codex exec …`)。纯函数命令分类器逐层剥壳 `cd X &&`、env 前缀、`timeout`、`bash -lc`、管道,排除 tmux/ssh 与运维调用(`--version`、`claude mcp`、`codex login`),提取 mode/prompt/workdir/flags。`delegation_id == tool_call_id`,客户端据此把既有工具卡「升级」而非双渲染。后台委派在既有 `process_registry.on_output` sink 里加第二个消费者,watcher 线程 500ms 合并冲刷实时输出(ANSI 剥离、`redact_sensitive_text(force=True)`、单次 4KB / 每委派 256KB 上限),进程退出映射为 `completed/failed/killed/lost`,并解析 Claude `--output-format json\|stream-json` / Codex `--json` 结果(`session_id`、`num_turns`、`total_cost_usd`)。技能升版(claude-code 2.3.0、codex 1.1.0)引导委派走 `background=true` + 结构化输出。显式非目标:tmux 交互式委派不跟踪;`process submit` 输入不回显。 | CN 桌面端要可视化 hermes 对 Claude Code / Codex 的多 Agent 委派("我的代理的子代理现在在干什么")。技能把所有委派都汇入 `terminal` 工具且没有任何结构化标记,UI 只能对 80 字符截断预览做模式匹配——前台调用完成前更是零输出。gateway 是唯一能统一分类、关联并推流的收口点。 | CN 桌面端驱动,但事件本身与客户端无关;待上游桌面端有委派视图后或可上游。分类器 fixture 与 Hermes-CN-Desktop `web/src/lib/cli-delegation.test.ts` 字面镜像——改一处必改两处。 | @@ -68,9 +69,62 @@ `→` ` 归一化、重复行去重(单行+多行块模式、可配阈值)、基于行数的头/尾截断加折叠标记、超长输出导出到会话文件、YAML 风格元数据块组装。(2) 新增 `rtk_provision.py`:`rtk` 二进制的运行时检测与路径解析(仿 `_find_rg()` 模式),依次查找 managed tools 目录 → 旧版 `$HERMES_HOME/bin` → PATH,带 `functools.lru_cache`。(3) 新增 `terminal_command_rewrite.py`:Shell 命令感知的重写,为已知的高输出命令(`git`、`cargo`、`npm`、`ls`、`grep`、`cat`、`python`、`docker`、PowerShell cmdlet 等)前置 `rtk`,正确解析 `;`/`&&`/`||`/`|` 分割,尊重引号和 subshell。(4) `terminal_tool.py` 新增 `token_kill`(默认 True)和 `max_lines` 参数;执行前可重写命令通过 rtk,执行后走完整后处理管线(替换了旧的内联 ANSI 剥离 + 字符级截断)。(5) `hermes_constants.py`:新增 `get_managed_tools_dir()` 返回 `/tools`(兼容旧版 `/bin` 兜底)。(6) `dep_ensure.py`:将 `rtk` 加入 `_DEP_CHECKS`/`_DEP_DESCRIPTIONS`;重构 `_find_rg()` 和 coreutils 检查使用 `get_managed_tools_dir()` 加旧版兜底。(7) `file_operations.py`/`commands.py`:使用 `_find_rg()` 而非裸 `shutil.which("rg")`,优先使用托管副本。(8) `tirith_security.py`:将自动安装目标从旧版 `$HERMES_HOME/bin/tirith` 迁移到 `get_managed_tools_dir()`,保留向后兼容的 PATH 兜底。(9) `scripts/install.ps1`/`install.sh`:增加 rtk 下载和 `hermes doctor` 检查。(10) `scripts/install_coreutils.py`:使用 `get_managed_tools_dir()` 获取管理工具路径。 | 旧的终端输出处理仅仅是一段 inline ANSI 剥离 + 硬编码的 40%/60% 字符级截断,没有去重、没有行级截断、模型也无法控制输出行数。像 `git log`、`cargo test`、`docker ps`、`npm install` 这样的命令可能产生成千上万行重复输出——模型为每一行重复内容买单。rtk 是一个外部 CLI,能在数据到达 agent 之前原生折叠重复行;后处理管线在 rtk 不可用或禁用时提供第二道防线(去重 + 行截断)。新的 `max_lines` 参数让模型可以指定行数(头+尾加折叠标记),比旧的字节级截断更直观。`get_managed_tools_dir()` 的整合把外部二进制移到了 `/tools/`(从通用的 `bin/` 迁出),避免污染 PATH 且更易管理。 | 建议上游(通用终端输出品质改进;去重/截断/导出管线是纯 Python 无外部依赖;managed-tools-dir 模式是通用维护改进)。 | -| **P-050** | `tools/environments/local.py`、`agent/prompt_builder.py`、`hermes_cli/config.py`、`hermes_cli/gateway.py`、`tools/environments/base.py`、`scripts/keystroke_diagnostic.py`、`apps/desktop/electron/main.ts`、`apps/desktop/electron/windows-hermes-resolution.test.ts`、`tests/tools/test_shell_resolution.py`、README 文件、网站文档、`skills/autonomous-ai-agents/hermes-agent/SKILL.md`、`FORK_NOTES.md` | **允许 `HERMES_SHELL_TYPE=bash` 作为可选的显式 Windows shell(需预装 Git Bash,不自助下载)。** Phase 2.2:`_resolve_shell()` 现在通过 `_find_bash_posix()` 查找预装 bash 而非直接抛 `RuntimeError`。Phase 2.1:保留 `_WINDOWS_BASH_SHELL_HINT` 与 `bash` 分发分支;`_WINDOWS_POWERSHELL_SHELL_HINT` 更新为提示用户可自由使用 PS7+ 语法(`pwsh_transform` 会自动降级)。Phase 1:`findGitBash()` 简化(移除 PortableGit 自助下载候选);预检改为按配置 shell 有条件检查 bash(`shell:bash`)或 PowerShell(默认)。Phase 3:测试更新——`test_windows_bash_found_returns_bash` 和 `test_windows_bash_not_found_raises_helpful_error` 替代了旧的 `test_windows_bash_raises_runtime_error`。Phase 4:所有 README 和网站文档更新为将 PowerShell 描述为默认 shell,Git Bash 作为可选的显式 opt-in。 | P-019 使 PowerShell 5.1 成为 Windows 上唯一受支持的 shell,并禁止了 `HERMES_SHELL_TYPE=bash`。本 fork 的用户可能仍为 VCS 操作安装了 Git for Windows,且某些工作流合法需要 POSIX shell 语法。重新允许 bash 作为显式 opt-in(不自动下载)恢复了灵活性,同时不会重新引入自动安装的复杂性或 PortableGit 下载。 | 建议上游(用户选择权;无自动下载风险) | +| **P-050 / P-052 / P-054** | `tools/environments/local.py`、`tools/terminal_tool.py`、`agent/prompt_builder.py`、`hermes_cli/config.py`、`hermes_cli/gateway.py`、`tools/environments/base.py`、`scripts/keystroke_diagnostic.py`、`apps/desktop/electron/main.ts`、`apps/desktop/electron/windows-hermes-resolution.test.ts`、`tests/tools/test_shell_resolution.py`、`tests/tools/test_terminal_dynamic_description.py`、`tests/tools/test_local_git_bash_port.py`(新增)、README 文件、网站文档、`skills/autonomous-ai-agents/hermes-agent/SKILL.md`、`FORK_NOTES*.md` | **重新开放 `HERMES_SHELL_TYPE=bash` 作为可选的显式 Windows shell(仅限预装 Git Bash、不自动下载),并逐步加固。** P-050 重新允许 Windows 上的 `HERMES_SHELL_TYPE=bash`:`_resolve_shell()` 用 `_find_bash_posix()` 查找预装 bash 而非抛错;恢复 `_WINDOWS_BASH_SHELL_HINT` 与 bash 分发分支;两个 PowerShell 提示从 5 条扩到 14 条(Verb-Noun cmdlet、.NET 管道、比较/逻辑运算符、字符串引号、splatting、`$LASTEXITCODE`、反引号规避);桌面端 `findGitBash()` 简化(移除 PortableGit 自动下载候选),预检按配置条件检查 bash(`shell:bash`)或 PowerShell(默认);README/文档更新(PowerShell 默认,Git Bash 可选 opt-in)。P-052 移植 kimi `bash_tool` 的 win32 对等实现:win32 门控 `fix_bash_command`(POSIX 主机逐字节空操作)、git.exe 发现链(`where.exe git` / `git --exec-path`)作为 `_find_bash` 的**最后**候选源、`_is_git_bash_install` 标记 + `_with_msystem_neutralized`(`export MSYSTEM=; `)让子进程看到空 MSYSTEM 而非 `MINGW64`、`_encode_startup_script`(base64+gzip,为将来交互式 `bash -i` 备好)、macOS bash 候选、以及 `_run_bash` 启动解析出的 `self._shell_path`。P-054 让 Git Bash 缺失/损坏时**回退**到 PowerShell 链路(pwsh → powershell.exe)并告警,不再启动失败:`_resolve_shell()` 的 bash 分支不再让 `_find_bash()` 的 `RuntimeError` 外传;`_detect_shell_for_description()` 仅在真正找到可用 Git Bash 时报告 `"bash"`;桌面预检(`ensureRuntime`)改为告警而非抛错。 | P-019 让 PowerShell 5.1 成为 Windows 唯一 shell 并禁止 `HERMES_SHELL_TYPE=bash`,但本 fork 用户可能仍为 VCS 操作安装 Git for Windows,且部分工作流合法需要 POSIX 语法;以显式 opt-in(不自动下载)重新开放 bash 可在不重引自动安装复杂度的前提下恢复灵活性。P-052 修复了该路径上的 MSYSTEM `MINGW64` 平台误判与漏掉 per-user/choco/scoop/并排安装的问题。P-054 确保过期的 `shell: bash` 配置或未安装/被策略禁用的 Git 优雅降级到始终可用的 PowerShell,而不是卡死整个 terminal 工具。 | 建议上游(用户选择权、无自动下载风险;优雅降级是通用能力) | +| **P-051** | 全仓库——shipped 代码中 269 处文本模式 `subprocess.run/Popen/call/check_call/check_output` 调用点(`agent/coding_context.py`、`agent/system_prompt.py`、`hermes_cli/web_server.py`、`tools/tts_tool.py`、`tools/transcription_tools.py`、`hermes_cli/_subprocess_compat.py`、…)、`tests/test_subprocess_text_pipe_decoding.py`(新增) | **给每一条文本模式子进程管道钉死显式解码——消灭 GBK `_readerthread` UnicodeDecodeError 这一类 bug。** 按约定输出 UTF-8 的子进程(git、gh、rg、python/pip/uv、node/npm、docker、ssh/scp、systemctl、tmux、cosign、codex/claude CLI、hermes 自身、…)钉 `encoding="utf-8", errors="replace"`;输出 OEM/ANSI 代码页的 Windows 原生工具(`tasklist`/`taskkill`/`netstat`/`where`)只钉 `errors="replace"`;三处 `**kwargs` dict 展开点(`_fs_git_branch`、`_run_command_tts`、`_run_command_stt`)在 dict 内补同样钉法;`_subprocess_compat.py` 的 drop-in wrapper 缺 `import subprocess` 的潜在 `NameError` 一并修复。 | zh-CN Windows(cp936、py3.14 无 UTF-8 模式)上 `subprocess.run(..., text=True)` 用 `locale.getpreferredencoding(False)` 包子进程管道;一个 GBK 非法字节(如实 = `E5 AE 9E` 的中间字节 `0xAE`)就会杀死 CPython 的 daemon `_readerthread`,调用方拿到 `stdout is None` 且无任何异常——会话开始的 workspace 快照因此从 system prompt 里静默消失。 | 应上游——逐点钉法与新的 AST 不变式回归测试 OS 无关。相关:c210b9621(slash worker 管道钉法)、P-042、P-019。 | +| **P-053** | `tests/` —— 170 个文件(8 个整文件删除、162 个文件编辑) | **测试套件卫生清理:移除 ad-hoc / magic-mock / 防 LLM 幻觉测试。** 基于仓库自身对 change-detector 测试的禁令(AGENTS.md「Don't write change-detector tests」),通过子代理审查对 1,203 个候选测试文件(63 个强信号 + 39 个中等信号分块)进行三分类:删除 8 个整文件(复制生产逻辑的自我指涉测试、只测第三方库的测试、纯 schema/enum 字面量钉死文件)以及 **162 个文件中的 546 个测试**——包括 change-detector 字面量钉死(模型列表、工具数量、schema 形状、版本/常量冻结)、magic-mock 调用签名钉死、mock 自指同义反复、`inspect.getsource`/AST 源码形状钉死、内联重写生产逻辑而从不调用真实代码的测试、以及批量凑覆盖率的冒烟测试。共删除约 10,467 行;保留 1,033 个文件(行为 / 回归 / 不变量 / E2E 测试)。 | 测试套件积累了一类测试,其唯一目的是冻结当前值或代码形状,防止编辑代码库的 LLM 静默改动它们。这类测试在任何有意改动时都会失败、徒增维护噪音、且从不验证行为——与仓库「Behavior contracts over snapshots」的原则背道而驰。 | 测试质量清理,非行为补丁——删除标准与 AGENTS.md 自身指引一致,可为上游测试卫生提供参考 | +| **P-055** | `tools/runtime_compat.py`(新增)、`gateway/platforms/webhook_filters.py`、`gateway/run.py`、`gateway/slash_commands.py`、`hermes_cli/main.py`、`hermes_cli/gateway.py`、`hermes_cli/gateway_windows.py`、`hermes_cli/kanban_db.py`、`hermes_cli/relaunch.py`、`hermes_cli/web_server.py`、`hermes_cli/uninstall.py`、`hermes_cli/profiles.py`、`hermes_cli/tools_config.py`、`hermes_cli/codex_runtime_plugin_migration.py`、`tui_gateway/server.py`、`tui_gateway/host_supervisor.py`、`tools/todo_tool.py`、`tools/tts_tool.py`、`tools/mcp_tool.py`、`tools/code_execution_tool.py`、`tools/lazy_deps.py`、`plugins/google_meet/*`、`plugins/memory/{hindsight,mem0}/*`、`skills/productivity/google-workspace/scripts/setup.py`、`tests/` | **把每一个"把 python 当解释器 spawn"的调用点收口到冻结运行时咽喉。** CN 便携桌面 runtime 是 PyInstaller 冻结的 exe,没有独立的 `python.exe`——内部 `sys.executable` 就是 Hermes CLI 本体,`[sys.executable, script.py]` / `[sys.executable, "-m", mod]` / `[sys.executable, "-c", code]` 会变成 `hermes ` 并以 argparse "invalid choice" 死掉。新 `tools/runtime_compat.py` 集中提供 `is_frozen_runtime()`、`hermes_cli_argv()`(冻结下丢掉 `-m hermes_cli.main` 前缀)与 `run_python_script_in_process()`(进程内 `runpy` 运行器,带超时/stdin 捕获,泛化 cron 修复)。每个生产 spawn 点现在:(1) 经 `hermes_cli_argv` 重新调用 CLI(kanban dispatcher、relaunch、dashboard reexec、web-server 分离动作、uninstall、gateway run/restart argv 构造、`gateway_windows` cmd/vbs/argv 构造);(2) 冻结时进程内运行可信脚本(webhook 路由脚本、todo 校验、NeuTTS 合成、Piper 语音下载);(3) 需要真 python 时跳过或明确拒绝(MCP stdio watchdog wrapper、execute_code 沙箱、Google Meet bot、Codex 迁移、uv/pip 懒安装、`hermes update`);(4) TUI slash-worker / compute-host / gateway restart-watch / update-gateway-helper 改走新的隐藏 CLI 子命令(`__slash-worker`、`__compute-host`、`__gateway-restart-watch`、`__update-gateway-helper`)。 | 与 cron 修复同根因(`#whitecat_inspect`):冻结桌面 runtime 没有独立 python,所有假设 `sys.executable` 是解释器的子进程点在桌面端全挂。只修 cron 会留下 ~30 个同类调用点继续坏。 | 打包 CN 专属(上游不发冻结 exe);`hermes_cli_argv`/进程内运行器模式通用,可作为 compat helper 上游。 | +| **P-056** | `agent/prompt_builder.py`、`tests/agent/test_prompt_builder.py`、`tests/agent/test_system_prompt.py`、`tests/agent/test_platform_hint_desktop.py` | **压缩并精简 `prompt_builder.py` 中的核心 system prompt 指引。** `HERMES_AGENT_INTRODUCTION`、`HERMES_AGENT_HELP_GUIDANCE`、`MEMORY_GUIDANCE`、`SESSION_SEARCH_GUIDANCE`、`SKILLS_GUIDANCE`、`KANBAN_GUIDANCE`、`TOOL_USE_ENFORCEMENT_GUIDANCE`、`TASK_COMPLETION_GUIDANCE`、`PARALLEL_TOOL_CALL_GUIDANCE`、`OPENAI_MODEL_EXECUTION_GUIDANCE`、`GOOGLE_MODEL_OPERATIONAL_GUIDANCE`、`STEER_CHANNEL_NOTE`、`computer_use_guidance`、`PLATFORM_HINTS` 以及 Windows PowerShell/pwsh shell hint 全部重写为简洁、指令式表述,同时保留原有行为契约。移除冗长的 XML 标签骨架、长篇示例和重复解释;消息平台共用 `_MEDIA_DELIVERY_HINT`。测试从字面短语检查改为行为不变量断言(标题、核心概念),避免微调措辞即失败。 | 过长的 system prompt 消耗 token 并稀释模型注意力;更短的指令在保持同样规则(用行动代替计划、验证、并行独立调用、事实用工具、不伪造、平台媒体投递、PowerShell 语法)的前提下提升信噪比。 | 建议上游(通用提示词质量 / token 效率改进) | +| **P-057** | `tools/delegate_tool.py`、`tools/session_search_tool.py`、`tools/code_execution_tool.py`、`tools/terminal_tool.py`、`tools/skill_manager_tool.py`、`tools/memory_tool.py`、`tools/todo_tool.py`、`tools/clarify_tool.py`、`tools/browser_tool.py`、`tools/file_tools.py`、`tools/process_registry.py`、`tools/vision_tools.py`、`tools/tts_tool.py`、`tools/skills_tool.py`、`tools/agent_swarm.py`、`tools/web_tools.py`、`agent/coding_context.py`、`agent/prompt_builder.py` | **第二轮:压缩工具 schema 描述与剩余 system-prompt 块以节省 token。** 工具描述是每次 API 调用最大的固定成本(每个工具都随每次调用发送)——默认 28 个核心工具的总 schema 从 47,535 字符降至 ~29,964(降 37%;描述 25,237→13,330,参数 22,298→16,634)。重点:delegate_task 4002→1458、session_search 3547→1246、execute_code 2454→1559、terminal 2398→1364、skill_manage 1800→1001、memory 1476→884、todo 1299→760、clarify 1185→745;browser/file/vision/process/tts/skills/web 各 −40%。保留所有影响行为的关键事实:调用形态、动态限额(delegate 并发/嵌套深度从配置渲染)、必填参数、何时不用及替代工具、PowerShell/bash 命令映射。system-prompt 块:`CODING_AGENT_GUIDANCE` 2626→1387、`KANBAN_GUIDANCE` 5481→2798、`computer_use_guidance` 815→581、`PLATFORM_HINTS["yuanbao"]` 1072→667。未改任何 schema 结构、handler、函数名或测试文件;动态 schema 机制(delegate_task/execute_code/terminal)保持完好。验证:定向套件全绿(856 passed),完整 `tests/tools` 3,672 passed(4 个 MSYS pathconv 环境类失败在纯净基线同样失败),ruff 干净。 | 延续 P-056:指引常量之后,工具描述是下一个 token 杠杆——每次 API 调用都会重发全部模型工具 schema,减 37% 字符直接降成本并提升信噪比。 | 建议上游(通用 token 效率改进,无 CN 专属行为) | -| **P-052** | `tools/environments/local.py`、`tests/tools/test_local_git_bash_port.py`(新增) | **kimi `bash_tool` win32 对等移植。** (1) `_wrap_command` 显式按 `sys.platform == "win32"` 门控 `fix_bash_command`(POSIX 主机逐字节空操作),且仅当修复器真正改动命令时才记录 `_bash_fix_warnings`。(2) 移植 git.exe 发现链(`_where_git_executables` / `_git_bash_candidate_from_git_path` / `_git_exec_path` / `_git_install_root_from_exec_path` / `_git_bash_candidates_from_exec_path`),接入 `_find_bash` 作为最后一个候选源(env 覆盖 → 托管便携 Git → 已知位置 → PATH 之后)。(3) 移植 `_is_git_bash_install`(盘符锚定的 `/cmd/git.exe` 标记)与 `_with_msystem_neutralized`(`export MSYSTEM=; `),在 `_wrap_command` 的 win32 分支逐命令应用,让子进程(xmake/meson)看到空的 MSYSTEM 而不是 `MINGW64`;真实 MSYS2 安装不受影响。(4) 移植 `_encode_startup_script`(base64+gzip 单行自解码)——为将来交互式 `bash -i` 引导准备。(5) 移植 macOS 候选(`_bash_candidates_macos` / `_bash_candidates_system` / `_git_bash_for_macos`),`_find_bash_posix` 增加 darwin 偏好分支(Linux 顺序不变)。(6) `_run_bash` 的 bash 分支改为使用 `self._shell_path`(解析出的 Git Bash),不再重新 `_find_bash_posix()`,使初始化发现与实际执行一致。 | P-050 重新开放了 Windows 上的 `HERMES_SHELL_TYPE=bash`;在该路径下,一次性 `bash -c` 的子进程会误判平台(`MSYSTEM=MINGW64`)、Git Bash 发现会漏掉 per-user/choco/scoop/并排安装、bash-fix 在 POSIX 主机上无条件运行、且 `_run_bash` 可能启动一个与 `_resolve_shell` 所选不同的 bash。 | MSYSTEM 中和 + git.exe 发现 + 标记检查 + macOS 候选是 kimi 的设计且通用——建议上游;win32 门控与 shell 路径一致性是正确性修复。 | +### P-050 / P-052 / P-054:Git Bash 重新开放为显式 Windows opt-in,并逐步加固 + +**现象 / 需求。** P-019 让 Windows PowerShell 5.1 成为唯一受支持的 Windows shell,并禁止 `HERMES_SHELL_TYPE=bash`。但本 fork 用户可能仍为 VCS 操作安装 Git for Windows,且部分工作流合法需要 POSIX shell 语法。以显式 opt-in(仅预装、不自动下载)重新允许 bash 可恢复灵活性,而不重引自动安装复杂度或 PortableGit 下载。然而 P-019 时代 Git Bash 缺失(或无法启动——Mandatory-ASLR 失败类)时的硬 `RuntimeError`,意味着过期的 `terminal.shell: bash` 配置、未安装 Git for Windows、或被策略禁用的 Git Bash 会直接卡死整个 terminal 工具——尽管 PowerShell 5.1 随每个 Windows 系统自带,且 `auto` 模式本就使用 pwsh → powershell.exe 链路。另外,在重新开放的 bash 路径上,一次性 `bash -c` 的子进程会误判平台(`MSYSTEM=MINGW64`)、Git Bash 发现会漏掉 per-user/choco/scoop/并排安装、`fix_bash_command` 在 POSIX 主机上无条件运行、且 `_run_bash` 可能启动一个与 `_resolve_shell` 所选不同的 bash。 + +**P-050 做了什么**(重新开放 `HERMES_SHELL_TYPE=bash` 为显式 opt-in): + +1. **`tools/environments/local.py`** — `_resolve_shell()` 用 `_find_bash_posix()` 查找预装 bash,不再抛 `RuntimeError`。 +2. **`agent/prompt_builder.py`** — 保留 `_WINDOWS_BASH_SHELL_HINT` 与 `bash` 分发分支;`_WINDOWS_POWERSHELL_SHELL_HINT` 更新为提及 `pwsh_transform` 的自动降级;两个提示从 5 条扩到 14 条(Verb-Noun cmdlet、.NET 管道、比较/逻辑运算符、字符串引号、splatting、`$LASTEXITCODE`、反引号规避)。 +3. **`apps/desktop/electron/main.ts`** — `findGitBash()` 简化(移除 PortableGit 自动下载候选);预检按配置条件检查 bash(`shell:bash`)或 PowerShell(默认)。 +4. **测试** — `test_windows_bash_found_returns_bash` 与 `test_windows_bash_not_found_raises_helpful_error` 替代旧的 `test_windows_bash_raises_runtime_error`。 +5. **文档** — 所有 README 与网站文档把 PowerShell 描述为默认 shell、Git Bash 为可选 opt-in。跨平台测试修复:`TestCwdHandling` 先查原始 `docker_cwd_source`、`TestExtractImageRefs` 正则扩展支持 Windows 盘符路径 + `os.path.normpath`、3 个测试文件的平台无关路径断言、易闪 Windows 测试标 `xfail(strict=False)`。 + +**P-052 做了什么**(kimi `bash_tool` win32 对等移植,全部在 `tools/environments/local.py`): + +1. **调用处 win32 门控** — `_wrap_command` 在 `fix_bash_command` 前检查 `sys.platform == "win32"`(POSIX 主机逐字节空操作);仅当修复器真正改动命令时才记录 `_bash_fix_warnings`。 +2. **git.exe 发现链** — `_where_git_executables`(`where.exe git`)、`_git_bash_candidate_from_git_path`(`/../bin/bash.exe`)、`_git_exec_path`(带超时 `git --exec-path`)、`_git_install_root_from_exec_path`(从 `mingw*/libexec/git-core` 上溯)、`_git_bash_candidates_from_exec_path`;接入 `_find_bash` 作为**最后**候选源(env 覆盖 → 托管便携 Git → 已知位置 → PATH 之后);`where.exe`/`git` 子进程用 `windows_hide_flags()`。 +3. **MSYSTEM 中和** — `_is_git_bash_install`(盘符锚定 `/cmd/git.exe` 标记;`bin/bash.exe` 与 `usr/bin/bash.exe` 两种布局;真实 MSYS2 安装永不匹配)+ `_MSYSTEM_NEUTRALIZE_PREFIX = "export MSYSTEM=; "` + `_with_msystem_neutralized`,在 `_wrap_command` 的 win32 分支、bash-fix 之后逐命令应用,让子进程即使快照导出 `MINGW64` 也看到空 `MSYSTEM`。 +4. **`_encode_startup_script`** — base64+gzip 自解码单行封装(stdlib `base64`/`gzip`;与 kimi 的 `pybase64` 输出一致)。暂无生产调用者——它是交互式 `bash -i` 引导集的三分之一,为将来的交互式 Git Bash 模式备好。 +5. **macOS bash 候选** — `_bash_candidates_macos`(`/opt/homebrew`、`/usr/local`、`/opt/local`)、`_bash_candidates_system`、`_git_bash_for_macos`;`_find_bash_posix` 增加 darwin 分支,优先新 Homebrew/MacPorts bash 而非系统 bash 3.2。Linux 顺序不变。 +6. **一致性修复** — `_run_bash` 的 bash 分支启动 `self._shell_path`(环境初始化时解析出的 Git Bash,已含 git.exe 链),不再重新 `_find_bash_posix()`,保证 `init_session` 与实际执行用同一个二进制。 + +**P-054 做了什么**(Git Bash 缺失/损坏 → 优雅回退 PowerShell): + +1. **`tools/environments/local.py` — `_resolve_shell()`。** `bash` 分支用 `try/except RuntimeError` 包住 `_find_bash()`(Git Bash 未找到、或 ASLR 修复提示类),并通过 `_bash_starts()` 缓存复查把"候选存在但探针全失败"也判为不可用。bash 不可用时记录携带原因的 warning,返回与 `auto` 相同的 `pwsh` → `powershell.exe` 链路。`_find_bash()` 本身不变(对需要硬错误的调用者仍抛错,例如 `_git_bash_bin_dirs` 的 try/except 路径)。 +2. **`tools/terminal_tool.py` — `_detect_shell_for_description()`。** bash 模式仅在 `_IS_WINDOWS` 为真 且 `_find_bash()` 返回的路径通过 `_bash_starts()` 时才报告 `"bash"`;否则 `"powershell"`(terminal 实际回退到的 shell)。探测以真实 OS 标志门控,mock `platform.system()` 不会触发真实 bash 搜索。旧的 P-019 过期注释已删除。 +3. **`apps/desktop/electron/main.ts` — 预检。** `HERMES_SHELL_TYPE=bash` 且 `findGitBash()` 失败时,`ensureRuntime` 改为 `console.warn` 并继续走 PowerShell 可用性校验(与核心回退一致),不再抛错。 +4. **测试。** `test_shell_resolution.py`:`test_windows_bash_found_returns_bash` 增加 `_bash_starts=True` mock;旧的 `test_windows_bash_not_found_raises_helpful_error` 被 `test_windows_bash_missing_falls_back_to_pwsh`、`test_windows_bash_missing_falls_back_to_powershell`、`test_windows_bash_broken_falls_back_to_powershell` 取代。`test_terminal_dynamic_description.py`:新增 `test_detect_windows_explicit_bash_returns_bash_when_found`,原测试改名为 `test_detect_windows_explicit_bash_returns_powershell_when_bash_unavailable`。 + +**有意未改。** `_find_bash()` 对其直接调用者仍保留 `RuntimeError`("Git Bash is not found" 与 ASLR 修复提示契约在那里仍有价值)。Windows 上 `auto` 默认不变(本就 PowerShell 优先)。POSIX 行为不变。不新增 Git 自动下载——回退只切换到始终存在的 PowerShell 链路。 + +**测试情况。** `tests/tools/test_local_git_bash_port.py`(新增,44 个用例:编码往返 + 载荷安全性;`_is_git_bash_install` 布局/标记/盘符锚定;`_with_msystem_neutralized` win32+Git Bash / 非 Git Bash / 非 win32 / None;发现链纯函数 + 子进程 mock;`_find_bash` 接线;macOS darwin 偏好;`_wrap_command` MSYSTEM 接线;`_run_bash` 启动解析出的 shell 路径)与 `tests/tools/test_shell_resolution.py` + `tests/tools/test_terminal_dynamic_description.py`(47 通过),以及相关套件 `test_local_env_windows_msys.py`、`test_find_shell.py`、`test_bash_fix.py`、`test_local_shell_init.py`、`test_process_registry.py`。完整 `tests/tools` 套件:8687 通过、330 跳过(均为平台专属跳过)。编辑的 Python 文件 `ruff check` 干净。 + +**是否上游?** 建议——显式选中的 shell 缺失时优雅降级是通用能力;PowerShell 随每个 Windows 系统自带,回退始终安全。MSYSTEM 中和、git.exe 发现链、标记检查与 macOS 候选是 kimi 的设计且通用;win32 门控与 `_run_bash` shell 路径一致性是正确性修复。POSIX 或 Git Bash 健康时无行为变化。有意不移植:`_bash_runs`(已被 Hermes 更强的 `_bash_starts` 取代)与 Windows shell 默认策略(刻意分叉)。 + + +### P-053:测试套件卫生清理——移除 ad-hoc / magic-mock / 防 LLM 幻觉测试 + +**现象 / 需求。** 测试套件积累了一类测试,其唯一目的是冻结当前值或代码形状,防止编辑代码库的 LLM 静默改动它们("防幻觉"护栏):精确的模型/命令/工具列表(`assert models == ["gpt-4.1-mini", ...]`)、枚举计数(`len(tools) == 42`)、schema 形状钉死(`"query" in schema["parameters"]["properties"]`)、版本/常量字面量冻结、`inspect.getsource`/AST 源码形状钉死、mock 自指同义反复(`assert x == MagicMock()` 一类)、内联重写生产逻辑却从不调用真实代码的测试、以及批量凑覆盖率的冒烟测试。AGENTS.md 明确禁止这类测试("Don't write change-detector tests" / "Behavior contracts over snapshots——不要冻结当前值(模型列表、配置版本字面量、枚举计数)")。它们在任何有意改动时都会失败、从不验证行为、还给每次全量回归增加噪音。 + +**改动内容**(全部在 `tests/` 内): + +1. **信号扫描**——按 change-detector 信号(schema 形状钉死、字面量列表钉死、版本钉死、`len()==N` 钉死、`assert_called_once_with` 载荷钉死、仅 isinstance/hasattr/is-not-None 检查)对全部 2,257 个测试文件逐文件打分;1,203 个有信号的文件拆成 102 个分块(63 强信号 + 39 中等信号)。 +2. **子代理三分类**——每个分块按书面规则(`TEST_TRIAGE_RUBRIC.md`)审查,偏保守("拿不准就保留")。保留 1,033 个文件(行为 / 回归 / 不变量 / E2E / 性能测试,含 wire-contract 测试与带 issue 编号的回归复现)。 +3. **删除**——**8 个整文件**(复制生产逻辑的自我指涉测试如 `test_step_callback_compat.py`、只测第三方库的测试如 `test_xxhash_migration.py` / `test_process_loop_event_loop_warning.py`、纯 schema/enum 字面量钉死文件如 `test_hindsight_config_schema.py`)以及 **162 个文件中的 546 个测试**(约 10,467 行)。 +4. **类感知 AST 删除**——`delete_tests2.py` 仅在类的全部方法都被命中(且类内方法数 > 0)时才整类删除,支持整类目标,并在 verdict 中类名前缀拼写错误时回退到非限定名匹配;删除后折叠多余空行,保存前逐个重新解析校验。 + +**回归防护。** 全套仍可收集(44,538 个测试);所有被编辑文件均已重跑:**8,809 通过、398 跳过、0 失败**(覆盖全部 162 个编辑文件);跨目录 32 文件抽样亦通过(862 通过、0 失败)。被删除模块无跨文件引用,被删除测试名无悬空调用。 + +**是否可上游?** 删除*标准*与 AGENTS.md 自身指引一致,可为上游测试卫生提供参考;但本次清理本身是 fork 维护动作,不是行为补丁。`deletion_plan.json`(每个被删测试及其原因的审计清单)与 `TEST_TRIAGE_RUBRIC.md`(规则)保留在仓库根目录备查。 ### P-049:终端输出后处理管线 + rtk (reasoning toolkit) 集成 **现象。** `git log`、`cargo test`、`npm install`、`docker ps` 或 `ls -R` 等终端命令会输出数千行重复内容——重复的错误行、进度条、状态行——而模型为每一行重复内容支付 token,浪费上下文和 API 预算。旧的管线只是一段内联的 ANSI 剥离 + 硬编码的 40%/60% 字符级头尾截断,完全没有去重功能。也没有 `max_lines` 参数,模型只能通过字节上限控制输出大小。 @@ -116,23 +170,6 @@ **是否可上游?** 可以——去重/截断/导出管线是纯 Python 无外部依赖;managed-tools-dir 模式(`get_managed_tools_dir()`)是一个通用的维护改进,整合了 Hermes 发现自身下载的二进制文件的方式。rtk 集成(命令重写 + 二进制检测)依赖第三方 CLI(`rtk-ai/rtk`),可作为可选增强提交上游。 -### P-052:kimi bash_tool win32 对等移植——bash-fix 门控、MSYSTEM 中和、git.exe 发现 - -**现象 / 需求。** `HERMES_SHELL_TYPE=bash`(P-050)恢复了 Git Bash 作为可选的 Windows shell,但与 kimi-agent 的 bash-tool 对比分析(kimi vs Hermes win32 分析第 B 节)发现三处缺口:(1) `fix_bash_command` 在 `_wrap_command` 调用处没有显式的 win32 门控——POSIX 主机为逐字节空操作白白付出扫描开销;(2) Git Bash 的 `bin/bash.exe` 启动器无条件注入 `MSYSTEM=MINGW64`,MSYS2 运行时在变量缺失时还会重新注入给子进程,于是从一次性 `bash -c` 拉起的 xmake/meson/交叉工具链把平台误判为 `MINGW64` 而非 `windows`;(3) Git Bash 发现逻辑从不查询 `where.exe git` / `git --exec-path`,会漏掉合法但不常见的安装(per-user、chocolatey/scoop、并排多版本)。 - -**改动内容**(全部在 `tools/environments/local.py`): - -1. **调用处 win32 门控** —— `_wrap_command` 在调用 `fix_bash_command` 前检查 `sys.platform == "win32"`(与修复器内部的门控一致),把 win32-only 契约显式化,POSIX 主机完全跳过修复器;仅当修复器真正改动命令时才记录 `_bash_fix_warnings`(由 `BaseEnvironment.execute()` 与 `pwsh_warnings` 一起消费一次)。 -2. **git.exe 发现链** —— `_where_git_executables`(`where.exe git`)、`_git_bash_candidate_from_git_path`(`/../bin/bash.exe`)、`_git_exec_path`(带超时的 `git --exec-path`)、`_git_install_root_from_exec_path`(从 `mingw*/libexec/git-core` 向上找安装根)、`_git_bash_candidates_from_exec_path`;接入 `_find_bash` 作为**最后**一个候选源,托管便携 Git 与标准安装位置保持优先,PATH 上的普通 `bash` 也优先于 shell 出 `where.exe`/`git`。`where.exe`/`git` 子进程使用 `windows_hide_flags()`(不闪控制台窗口)。 -3. **MSYSTEM 中和** —— `_is_git_bash_install`(盘符锚定的 `/cmd/git.exe` 标记;`bin/bash.exe` 与 `usr/bin/bash.exe` 两种布局;真实 MSYS2 安装永不匹配)+ `_MSYSTEM_NEUTRALIZE_PREFIX = "export MSYSTEM=; "` + `_with_msystem_neutralized`,在 `_wrap_command` 的 win32 分支、bash-fix 之后逐命令应用。前缀在 eval 内、快照 source 之后执行,因此即使快照导出 `MINGW64`,用户命令的子进程看到的仍是空的 `MSYSTEM`。 -4. **`_encode_startup_script`** —— base64+gzip 自解码单行封装(stdlib `base64`/`gzip`;与 kimi 的 `pybase64` 输出一致)。暂无生产调用者——它是交互式 `bash -i` 引导三元组(与 `bash_compatibility_prelude`)的三分之一,为将来的交互式 Git Bash 模式备好。 -5. **macOS bash 候选** —— `_bash_candidates_macos`(`/opt/homebrew`、`/usr/local`、`/opt/local`)、`_bash_candidates_system`、`_git_bash_for_macos`(官方 Git 安装器自带 bash);`_find_bash_posix` 增加 darwin 分支,优先新 Homebrew/MacPorts bash 而非老旧的系统 bash 3.2。Linux 顺序不变。 -6. **一致性修复** —— `_run_bash` 的 bash 分支现在启动 `self._shell_path`(环境初始化时解析出的 Git Bash,已含 git.exe 链),不再重新走 `_find_bash_posix()`,保证 `init_session` 与实际执行用同一个二进制。 - -**测试。** `tests/tools/test_local_git_bash_port.py`(新增,44 个用例):编码往返 + 载荷安全性;`_is_git_bash_install` 两种布局 / 缺标记 / 盘符锚定(绝不产生 `C:...` 盘符相对路径);`_with_msystem_neutralized` win32+Git Bash 加前缀 / 非 Git Bash 不变 / 非 win32 不变 / None 路径;发现链纯函数 + 子进程 mock(成功 / 非零 / OSError / 超时);`_find_bash` 经 where.exe 分支与 exec-path 分支的接线 + 无候选报错;macOS darwin 偏好 + Git 安装器回退 + Linux 顺序;`_wrap_command` MSYSTEM 接线(win32 内嵌前缀、非 win32 跳过、pwsh 分发跳过、真实标记集成双向);`_run_bash` 启动解析出的 shell 路径(login + 非 login)。完整 `tests/tools` 套件:8687 通过、330 跳过(均为平台专属跳过)。 - -**是否可上游?** 可以——MSYSTEM 中和、git.exe 发现链、标记检查与 macOS 候选都是 kimi 的设计且通用;win32 门控与 `_run_bash` shell 路径一致性是正确性修复。有意不移植:`_bash_runs`(已被 Hermes 更强的 `_bash_starts` 取代)与 Windows shell 默认策略(刻意分叉,见 P-016/P-019/P-050)。 - ## 发布和维护支撑 这些不是运行时行为补丁,但属于 fork 维护能力: @@ -448,73 +485,57 @@ --- -### P-016:PowerShell 原生执行 + 运行时自适应终端工具描述 - -> **由 P-019 更新**:P-019 完成了迁移,移除了所有剩余 Git Bash 发现逻辑,专注于仅使用 **Windows PowerShell 5.1**(`powershell.exe`)。详见下方 P-019。 - -**现象**:Windows 上 agent 硬编码使用 Git Bash。PowerShell 启动更快(`-NoProfile`),原生处理 Windows 路径(无需 `/c/foo` 翻译)。此外,terminal 工具描述包含 Linux/bash 命令引用,在原生 PS 中不存在。 - -**原因**:上游 `LocalEnvironment` 只支持 bash。 - -**改动内容**: - -1. **`tools/environments/local.py`** — 新增 `_resolve_shell()`:Windows 上检测 `pwsh.exe`(PS7)优先,回退到 `powershell.exe`(PS5.1)或 Git Bash。新增 `_run_pwsh()`、`_wrap_command_pwsh()`,覆写 `init_session()`、`_run_bash()`、`_wrap_command()`。支持 `HERMES_SHELL_TYPE` 和 `HERMES_PWSH_PATH`。 - -2. **`tools/terminal_tool.py`** — 动态描述:`_detect_shell_for_description()` + `_build_dynamic_terminal_description()`,将 Linux/bash 命令引用替换为 PS cmdlet。 - -3. **`model_tools.py`** — 将 `_shell_fp` 加入 `get_tool_definitions()` 缓存键。 - -4. **`tools/environments/process_pwsh.py`** — `pwsh_transform()` 将 PS7+ 语法(`?:`、`??`、`&&`、`||`、`?.`、`?[`)降级为 PS5.1 兼容的 `if/else`,带警告传递。 - -**风险和约束**:Windows 上 terminal 命令在 PS 中执行。Git Bash 自动安装已移除,但 Python 层 bash 回退(`_find_bash()`)仍保留为 7 策略发现链。 - -**是否上游**:建议上游——被 P-019 取代并完成迁移。 - ---- - -### P-019:完成 Git-Bash→PowerShell 迁移(仅 Windows PowerShell 5.1) +### P-016 / P-019 / P-XXX:PowerShell 原生执行——Git-Bash→PowerShell 迁移 + pwsh 检测 -**现象**:P-016 为代码库增加了 PowerShell 支持,但留下了混合状态:`pwsh.exe`(PS7)被优先探测,`powershell.exe`(PS5.1)作为回退,而 7 策略 `_find_bash()` Git Bash 发现链(环境覆盖 → PortableGit → git.exe 推导 → 注册表 → PATH → 常见路径 → 自动安装)仍然存在。`HERMES_GIT_BASH_PATH`、`HERMES_PWSH_PATH` 和 `_install_git` 导入(不存在的模块)都是死代码。 +> P-019 完成了 P-016 开启的迁移,移除全部剩余 Git Bash 发现逻辑,只针对 **Windows PowerShell 5.1**(`powershell.exe`);P-XXX(落地时未编号)其后又在 P-019 之上恢复 pwsh 优先检测。三者合并描述如下。 -**原因**:P-016 专注于将 PowerShell 添加为主 shell,但未完全移除 Git Bash 机制。`pwsh.exe`(PS7)的要求是不必要的——Windows PowerShell 5.1(`powershell.exe`)随每套 Windows 10/11 系统自带,始终可用。 +**现象 / 需求。** Windows 上 agent 硬编码为始终使用 Git Bash。PowerShell 启动更快(`-NoProfile`)、原生处理 Windows 路径(无需 `/c/foo` 翻译)。此外 terminal 工具的静态 `TERMINAL_TOOL_DESCRIPTION` 引用了原生 PowerShell 中不存在的 Linux/bash 命令。上游 `LocalEnvironment` 只支持 bash,因此完全没有 PowerShell 路径。 -**改动内容**: +**P-016 做了什么**(已被 P-019 取代): -1. **`tools/environments/local.py`** — 核心 shell 解析(约 ~400 行删除):移除 `_find_bash()`,替换为最小化的 `_find_bash_posix()`。移除 `_is_windows_wsl_launcher()`。`_find_pwsh_simple` → `_find_powershell()`。重写 `_resolve_shell()`:Windows 上始终返回 `("powershell", path)`。`HERMES_SHELL_TYPE=bash` 在 Windows 上抛 `RuntimeError`。函数重命名:`_run_pwsh` → `_run_powershell`,`_wrap_command_pwsh` → `_wrap_command_powershell`。`pwsh_transform` 改为始终开启。所有 `"pwsh"` → `"powershell"`。 +1. **`tools/environments/local.py`** — 新增 `_resolve_shell()`:Windows 上先探测 `pwsh.exe`(PS7),回退到 `powershell.exe`(PS5.1)或 Git Bash。新增 `_run_pwsh()`、`_wrap_command_pwsh()`,覆写 `init_session()`、`_run_bash()`、`_wrap_command()`。支持 `HERMES_SHELL_TYPE` 与 `HERMES_PWSH_PATH`。 +2. **`tools/terminal_tool.py`** — 动态描述:`_detect_shell_for_description()` + `_build_dynamic_terminal_description()` 把 Linux/bash 命令引用替换为 PowerShell cmdlet。 +3. **`model_tools.py`** — 把 `_shell_fp` 加入 `get_tool_definitions()` 缓存键。 +4. **`tools/environments/process_pwsh.py`** — `pwsh_transform()` 把 PS7+ 语法(`?:`、`??`、`&&`、`||`、`?.`、`?[`)降级为 PS5.1 兼容的 `if/else`,带警告传递,让 LLM 获知其 PS7 语法被降级。 -2. **`tools/terminal_tool.py`** — 移除 "Windows Git Bash" 描述分支。简化 `_detect_shell_for_description()`。 - -3. **`agent/prompt_builder.py`** — `_WINDOWS_BASH_SHELL_HINT` → `_WINDOWS_POWERSHELL_SHELL_HINT`。 +**P-019 做了什么**(完成迁移;仅 Windows PowerShell 5.1): +1. **`tools/environments/local.py`** — 核心 shell 解析:移除 `_find_bash()`(约 130 行,7 策略 + WSL 启动器过滤 + 带死 `_install_git` import 的自动安装),仅对非 Windows 保留最小化 `_find_bash_posix()`。`_find_pwsh_simple` → `_find_powershell()`(就是 `shutil.which("powershell.exe") or "powershell.exe"`——不再探测 `pwsh.exe`)。`_resolve_shell()` 在 Windows 上始终返回 `("powershell", path)`;`HERMES_SHELL_TYPE=bash` 抛 `RuntimeError`;移除 `HERMES_PWSH_PATH` 支持。重命名 `_run_pwsh` → `_run_powershell`、`_wrap_command_pwsh` → `_wrap_command_powershell`。**`pwsh_transform` 改为始终开启**(移除 `if os.path.basename(...).startswith("powershell")` 门)。`_update_cwd`/`_extract_cwd_from_output` 的 MSYS 归一化以 `self._shell_type == "bash"` 门控。 +2. **`tools/terminal_tool.py`** — 移除 "Windows Git Bash" 描述分支(死代码);`_detect_shell_for_description()` 在 Windows 上始终返回 `"powershell"`。 +3. **`agent/prompt_builder.py`** — `_WINDOWS_BASH_SHELL_HINT` 换成 `_WINDOWS_POWERSHELL_SHELL_HINT`(PS5.1 语法:`;` 而非 `&&`、`$env:VAR`、无 `?:`/`??`/`?.`)。 4. **`cli.py`** — `_normalize_git_bash_path` → `_normalize_msys_path`。 +5. **`apps/desktop/electron/main.cjs`** — `findGitBash()`(约 40 行)换成 `findPowerShell()`(约 15 行);预检校验 `powershell.exe`。 +6. **`scripts/install.ps1`** — 移除 `Install-Git` bash 发现 + `Set-GitBashEnvVar`(约 210 行),简化 `Stage-Git`,增加防御性 `powershell.exe` 检查,去掉全部 `HERMES_GIT_BASH_PATH` 引用。 +7. **`hermes_cli/uninstall.py`** — 从环境变量清理中移除 `HERMES_GIT_BASH_PATH`。 +8. **`cron/scheduler.py`** — 更新 `.sh`/`.bash` 错误消息(不再提 Git for Windows)。 +9. **注释清理** — `tools/environments/base.py`、`tools/file_operations.py`、`tools/browser_tool.py`:"Git Bash" → "PowerShell" 或通用 "shell"。 +10. **测试** — `test_shell_resolution.py`(重写)、`test_terminal_dynamic_description.py`(移除 bash-on-Windows 测试)、`test_windows_native_support.py`、`test_local_env_windows_msys.py` 更新。 +11. **文档** — `windows-native.md`、`environment-variables.md`、`contributing.md` 改为 PowerShell 5.1 描述。 +12. **PowerShell UTF-8 编码加固** — `tools/environments/windows_env.py` 新增 `ps_with_utf8()`,前置 `[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; $OutputEncoding=[System.Text.Encoding]::UTF8;`;在 `local.py` 的 `pwsh_transform()` 之后调用;仅对 PowerShell 子进程保留 `encoding="utf-8", errors="replace"`(`hermes_cli/claw.py`、`clipboard.py`、`gateway.py`、`managed_uv.py`);`hermes_bootstrap.py` 把 Windows 控制台代码页设为 CP_UTF8(65001)并提供 `HERMES_DISABLE_WINDOWS_UTF8=1` 逃逸开关;撤销所有非 PowerShell 子进程上的 `encoding="utf-8"`;新增测试(`test_clipboard.py::TestClipboardPowershellEncoding`、`test_local_pwsh_warnings.py::TestRunPowershellUtf8Encoding` / `TestPwshTransformAndUtf8Compose`、`test_windows_encoding.py`、`scripts/verify_windows_utf8.py`)。 + +**P-XXX 做了什么**(pwsh 优先后续,未编号): + +1. **`tools/environments/local.py`** — 新增 `_find_pwsh()` 多步检测(PATH、ProgramFiles、注册表、LocalAppData)。`_resolve_shell()` 现在优先 pwsh 而非 powershell.exe。`_wrap_command_powershell()` 在原生 pwsh 时跳过 `pwsh_transform`。所有分发点(`init_session`、`_run_bash`、`_wrap_command`、`_update_cwd`、`_extract_cwd_from_output`)同时接受 `"powershell"` 与 `"pwsh"` 类型。 +2. **`tools/terminal_tool.py`** — `_detect_shell_for_description()` 探测 pwsh,找到即返回 `"pwsh"`;`_build_dynamic_terminal_description()` 增加 pwsh 变体。 +3. **`agent/prompt_builder.py`** — pwsh 可用时使用 `_WINDOWS_PWSH_SHELL_HINT`(无 PS5.1 限制警告)。 + +**为什么需要**: +- `powershell.exe`(5.1)随每套 Windows 10/11 自带——零安装、零下载。 +- 比 Git Bash 启动更快、原生处理 Windows 路径、避免 POSIX 翻译开销。 +- P-019 删除约 400 行死代码(7 策略 bash 发现、WSL 启动器过滤、PortableGit 自动安装、`HERMES_GIT_BASH_PATH`、`HERMES_PWSH_PATH`);agent 在 Windows 上拥有唯一、可预测、始终可用的 shell。P-016 的 `pwsh.exe`(PS7)探测是不必要的复杂度——5.1 全覆盖。 +- P-XXX:装了 pwsh 的用户此前白白承受 PS7→PS5.1 转换开销与 PS5.1 限制警告;pwsh 可用时直接使用并完全跳过降级转换。 -5. **`apps/desktop/electron/main.cjs`** — `findGitBash()` → `findPowerShell()`。更新预检。 - -6. **`scripts/install.ps1`** — 移除 `Install-Git` bash 发现 + `Set-GitBashEnvVar`(约 210 行)。简化 `Stage-Git`。增加 `powershell.exe` 防御性检查。 - -7. **`hermes_cli/uninstall.py`** — 移除 `HERMES_GIT_BASH_PATH`。 - -8. **`cron/scheduler.py`** — 更新 `.sh`/`.bash` 错误消息。 - -9. **注释清理**:`base.py`、`file_operations.py`、`browser_tool.py`。 - -10. **测试**:更新 4 个测试文件。 - -11. **文档**:更新 3 个英文文档页面。 - -12. **PowerShell UTF-8 编码加固** —— 让 Windows 上 PowerShell 子进程输出按 UTF-8 解码: - - 在 `tools/environments/windows_env.py` 中新增 `ps_with_utf8()` 辅助函数,为 PowerShell 命令字符串前置 `[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; $OutputEncoding=[System.Text.Encoding]::UTF8;`。幂等,非 Windows 平台无操作。 - - 在 `tools/environments/local.py` 中于 `pwsh_transform()` 之后调用 `ps_with_utf8()`。 - - 仅对 PowerShell 子进程调用保留 `encoding="utf-8", errors="replace"`:`hermes_cli/claw.py`、`hermes_cli/clipboard.py`、`hermes_cli/gateway.py`、`hermes_cli/managed_uv.py`。 - - `hermes_bootstrap.py` 将 Windows 控制台输入/输出代码页设为 CP_UTF8(65001),并新增 `HERMES_DISABLE_WINDOWS_UTF8=1` 逃逸开关。 - - 撤销所有非 PowerShell 子进程上的 `encoding="utf-8"` 添加(tasklist、ssh、docker、ffmpeg、singularity、ripgrep、termux、comfyui auto-fix、check-windows-footguns 中的 git 辅助,以及若干测试)。 - - 新增测试:`tests/tools/test_clipboard.py::TestClipboardPowershellEncoding`、`tests/tools/test_local_pwsh_warnings.py::TestRunPowershellUtf8Encoding` / `TestPwshTransformAndUtf8Compose`、`tests/tools/test_windows_encoding.py`、`scripts/verify_windows_utf8.py`。 +**风险和约束**: +- `HERMES_SHELL_TYPE=bash` 在 Windows 上抛清晰的 `RuntimeError`(直到 P-050/P-052/P-054 把它重新开放为显式 opt-in——见合并后的 P-050/P-052/P-054 条目)。 +- `HERMES_PWSH_PATH` 与 `HERMES_GIT_BASH_PATH` 环境变量不再被识别。 +- 所有命令无条件经过 `pwsh_transform`(除非 P-XXX 下原生 pwsh 运行)。 +- PowerShell 命令现在能可靠往返非 ASCII 输出(中文、emoji、带重音字符);非 PowerShell 子进程仍用系统 locale,这是 fork 刻意保持的保守范围。 -**为什么需要**:`powershell.exe` (5.1) 随每套 Windows 10/11 系统自带——零安装、零下载。比 Git Bash 启动更快,路径处理原生,避免 POSIX 翻译开销。删除约 400 行死代码。Agent 在 Windows 上拥有唯一、可预测、始终可用的 shell。P-016 的 `pwsh.exe`(PS7)探测是不必要的复杂度——5.1 全覆盖。 +**是否上游?** 建议。P-019 完成 P-016 开启的迁移,使 Hermes 成为零依赖的 Windows 程序;P-XXX 是通用后续。 -**风险和约束**:`HERMES_SHELL_TYPE=bash` 现在在 Windows 上抛清晰的 `RuntimeError`。`HERMES_PWSH_PATH` 和 `HERMES_GIT_BASH_PATH` 环境变量不再被识别。所有命令无条件经过 `pwsh_transform`。PowerShell 命令现在能可靠地往返非 ASCII 输出(中文、emoji、带重音符号字符);非 PowerShell 子进程仍使用系统 locale,这是 fork 有意保持的保守范围。 +**同步说明(2026-06-27,`chore/sync-upstream-20260627`)**:上游会周期性在 `main` 上重新引入 Git Bash 机制。本次同步的 `upstream/main` 恢复了 `tools/environments/local.py` 里的 7 策略 `_find_bash()`、`tools/terminal_tool.py` 里的静态终端描述、以及 `website/docs/developer-guide/contributing.md` 里的 Git-Bash 措辞。同步**重申 P-016/P-019**——保留 fork 的 PowerShell-only 路径,只在上面嫁接上游*独立*的修复:`_find_shell()` 的 POSIX 后台 spawn `$SHELL` 优先(#42203,改调 fork 的 `_find_bash_posix()`);`start_new_session`;install-dir PATH 可达性;以及 `apps/desktop/electron/main.cjs` 的 no-console-python 助手(`getNoConsoleVenvPython`/`toNoConsolePython`/`applyWindowsNoConsoleSpawnHints`/`unwrapWindowsVenvHermesCommand`)与 fork 的 async 后端解析结合(探测是 `async` 的,fork 的 `await` 是必需的)。未来同步应预期同样的 Git-Bash 漂移并以同样方式解决。 -**是否上游**:建议上游。完成 P-016 开始的迁移,使 Hermes 成为零依赖的 Windows 程序。 +**`cli.py` 分解清理(2026-06-27)**:此前一次同步在 `HermesCLI` 内联留了一大块"从上游 CLI 分解恢复"的方法。上游现在通过 `CLICommandsMixin` / `CLIAgentSetupMixin`(`HermesCLI` 继承)提供这些方法,因此本次同步按 MAINTAINING.md "上游已加等价功能就移除本地 fork 实现"删掉了冗余的内联副本。重新应用到 `cli.py` 的唯一真正 fork 改动是 P-019 的重命名;未使用的 `_new_session_id` 助手(0 调用者)一并删除。 --- @@ -624,6 +645,79 @@ --- +### P-027:`save_config_value()` 绝不创建项目级 `cli-config.yaml` + +**现象**:并行测试跑批下,`tests/hermes_cli/test_ignore_user_config_flags.py::test_user_config_skipped_when_flag_set` 失败(一旦 `tests/test_tui_gateway_server.py` 进入同一 CI 切片就稳定复现):`HERMES_IGNORE_USER_CONFIG=1` 时 `load_cli_config()` 返回泄漏的 `model.default`(`anthropic/claude-sonnet-4.6`)而不是内建默认值。 + +**根因**:`save_config_value()` 用 `config_path = user_config_path if user_config_path.exists() else project_config_path`。当(测试隔离的)`HERMES_HOME` 没有 `config.yaml` 时,它写入——并**创建**——`/cli-config.yaml`(已安装包/源码树内的项目配置)且从不清理。`scripts/run_tests.sh` 每个测试文件跑在自己的子进程里,但 8 个并行 worker 共享工作树,泄漏文件污染任何并发跑、其 `load_cli_config()` 回退到 `project_config_path` 的测试——正是 `--ignore-user-config` 读取路径。`tests/test_tui_gateway_server.py` 是写入方。 + +**修复**:只在项目 `cli-config.yaml` **已存在**时才写入它;否则写入(并创建)用户配置。`save_config_value()` 不再在源码树里创建文件。 + +**是否上游?** 是——往已安装包目录写配置是通用 footgun,与 CN 无关。 + +--- + +### P-044:agent 初始化期的 WMI + SSL Windows 开销——无 WMI 平台检测 + 记忆化 CA guard + +**现象。** agent-init 火焰图(`.plans/15-WMI-SSL-Windows-Overhead.md`)标出两个 Windows 特有 C 扩展热点:`_wmi.exec_query`(自耗时 2.91%)与 `_ssl._SSLContext.set_default_verify_paths`(8.19%),外加 import loader 成本(`_io.open_code` 4.39%、`builtins.compile`)。 + +**根因(在 py3.14/Windows 上实测确认,非照搬计划)。** +1. **WMI。** 计划归咎于 `import wmi` / `pywin32` 依赖,但本树**没有** `wmi` 包的使用。真正来源是 stdlib `platform` 模块:Python 3.12+ 的 `platform.uname()` 通过 `_wmi` 内建解析 Windows release + machine 字段(`win32_ver` 与 `_get_machine_win32` 各发一次 `_wmi.exec_query`,冷 ~40-90ms)。`platform.system()` 与 `platform.release()` 都会构造 `uname()`,而 fork 在十来个文件的**模块作用域**跑 `_IS_WINDOWS = platform.system() == "Windows"`——于是 `from run_agent import AIAgent` 导入级联里 WMI 服务被命中**两次**(定位到 `hermes_cli/config.py` + `agent/credential_pool.py`),`agent/prompt_builder.py` 的 `platform.release()` 每次初始化构建 system prompt 时又命中**一次**。 +2. **SSL。** `agent/ssl_guard.verify_ca_bundle()` 每次 `AIAgent` 构造都跑(`agent/agent_init.py`),并构建一次性 `ssl.create_default_context(cafile=certifi.where())` 来证明 CA bundle 可加载——Windows 上 ~225ms,每次初始化都重复支付(实测冷 244ms、暖 225ms),网关/subagent 集群每个 agent 都重载 CA 存储。 + +**修复。** +1. **`platform_utils.py`(新增,无 WMI、纯 stdlib)。** `is_windows()` / `is_macos()` / `is_linux()` 从 `sys.platform` 常量解析(不构造 `uname()` → 不触发 WMI);`windows_release()` 用 CPython 自带的版本→名称表从 `sys.getwindowsversion()` 的 build 号推导出与 `platform.release()` 相同的 release 标签(如 `"11"`),跳过 `win32_ver` 的 WMI 探测;`host_os_label()` 供 prompt/诊断主机行使用。7 处模块级 `_IS_WINDOWS = platform.system() == "Windows"` 标志(`hermes_cli/config.py`、`hermes_cli/dep_ensure.py`、`tools/code_execution_tool.py`、`tools/environments/{powershell_session,local}.py`、`tools/process_registry.py`、内置 `plugins/platforms/whatsapp/adapter.py` 的 inline `sys.platform`)以及 `prompt_builder` 的 Windows 主机行改用它。**结果:导入级联与 `build_environment_hints()` 期间 `_wmi.exec_query` 调用 0 次**(原来 2 + ≥1);主机行逐字节一致(`Host: Windows (11)`)。 +2. **记忆化 CA guard(`agent/ssl_guard.py`)。** `verify_ca_bundle()` 以四个 CA 环境变量 + certifi bundle(path/size/mtime)的廉价指纹为键缓存*成功*结论。CA 配置不变时每个进程只校验一次(重复调用 ~0.05ms vs ~225ms);任何变更(改了环境变量、重装 certifi)都会击碎指纹并重新校验+重新抛出,早破 bundle 的错误契约完整保留。`_reset_ca_bundle_cache()` 是测试钩子;`test_ssl_ca_guard.py` 增加 autouse reset 保证既有 broken-bundle 用例保持隔离。 +3. **`scripts/precompile.py`(新增)。** 基于 `compileall` 的提前 `.pyc` 预热,面向源码运行布局(CN fork 的 Windows 默认),首次 import 不必在热路径上付 `builtins.compile` + `_io.open_code` + Defender 扫描新写字节码。坏源文件绝不致命(报告后返回非零)。`platform_utils` 登记进 `pyproject.toml` `py-modules`。 +4. **Defender 提示。** `tools/environments/windows_env.suggest_defender_exclusion()` 返回可执行的"把 HERMES_HOME 加入 Defender 排除"建议,经 `hermes doctor` 展示(`_check_windows_defender_hint()`),与 P-042 既有的 `FILE_ATTRIBUTE_TEMPORARY` 写路径提示互为补充。仅提示——不动任何 Defender 设置。 + +**结果。** WMI 从导入级联与 prompt 构建路径中彻底移除(0 次查询)。每次 init 重复的 SSL 证书加载消失(每个进程至多付一次,而非每个 `AIAgent` 付一次)。两个火焰热点在主导测量套件的重复 init / 网关路径上均朝计划的 `< 0.1%` 目标收敛。 + +**测试。** `tests/tools/test_wmi_ssl_windows_overhead.py`(新增):`platform_utils` 与 `sys.platform` 一致且从不调用 `platform.uname()`/`system()`;`windows_release()` 在 Windows 上等于 `platform.release()` 且无 `_wmi`(Windows-only 追踪);Windows-only 子进程守卫断言 import `run_agent` + `build_environment_hints()` 发出**零**次 `_wmi.exec_query`;`precompile_all` 产出 `.pyc`、语法错误时返回 `False`(不抛)、缺失路径 no-op;Defender 提示平台门控。`tests/agent/test_ssl_ca_guard.py`(扩展):记忆化在配置不变时跳过重校验、环境变量变更时重校验并抛出、指纹追踪 certifi 身份——所有既有 SSL-guard 用例仍通过。`ruff check` 干净。 + +**是否上游?** 是——`platform_utils`(`sys.platform` 惯用法在每个 OS 上都正确)与 CA-guard 记忆化与 provider/OS 无关;只有 WMI 的*量级*是 py3.12+-on-Windows 特有。相关:P-042(Windows 子进程 spawn 开销)、P-043(首次分发延迟)。 + +--- + +### P-045:Flame-graph import 系统优化 + +**现象 / 目标。** agent-init 火焰图(`.plans/16-Flame-Graph-Import-Optimizations.md`)把冷启动 agent 的 **~71%** 归因于 Python import 系统——主要是 `builtins.compile`(~10.82%)、`_io.open_code`(~4.39%)、模块搜索统计 `nt.stat`(~1.6%)/ `nt._path_exists`(~2.1%)。 + +**计划里被证伪的两条(实现前先验证过)。** 计划五条建议中有两条在本树不成立,故刻意未按原文实现: +- *"在热路径上用 `type(x) is dict` 换掉 `isinstance`。"* 基准:`type(d) is dict`(0.069s/5M)并不比 `isinstance(d, dict)`(0.067s/5M)快——CPython 已有快路径——而保正确性的组合形式(`type() is ... or isinstance(...)`)反而更慢(0.085s)。照搬会拖慢热路径。 +- *"初始化期间的 34.4 万次 isinstance / 15.3 万次 getattr 是冗余计算。"* 这些绝大多数是第三方**import 期**类构造(pydantic、OpenAI SDK),不是 Hermes 代码。真正的对策是不急切 import 它们——已由懒代理 + 懒工具索引(P-043)完成,并经下面的加速器强化——而不是改写我们自己的类型检查。 + +**实现内容。** +1. **`import_accelerator.py`(新增)——一方 meta-path finder。** `sys.meta_path[0]` finder 用 dict 查找解析 Hermes 自身的顶层模块/包,跳过标准机制按 `sys.path` 逐条 `FileFinder` 扫描的开销。关键正确性选择:**精选 allow-list**(镜像 pyproject `py-modules` + `packages.find`——盲目 `os.listdir` 会把仓库本地 `packaging/` 目录注册进去并遮蔽真正的 PyPI `packaging` 依赖);**包优先于模块**(仓库根 `agent.py` harness 绝不赢过 `agent/` 包);`spec_from_file_location` 保留 stdlib `SourceFileLoader` 及其 `.pyc` 缓存;每个非一方名字 O(1) 透传(单次 dict miss);`HERMES_DISABLE_IMPORT_ACCELERATOR` 可关。从 `hermes_bootstrap`(每个入口最早的 import)安装,并从 `run_agent` 幂等安装。映射在构建期校验一次,不做逐 resolve 的 re-stat——早期版本做了,反而比 PathFinder 的缓存目录命中更贵、让暖 import 慢 ~1.7%;去掉后加速器在暖树上中性到更快,冷启动/`sys.path` 把 site-packages 排前时仍绕过扫描。 +2. **`scripts/precompile.py` —— 幂等 + 后台。** 新增 `precompile_if_needed()`(源码+解释器指纹与磁盘 stamp 一致时跳过)、`precompile_in_background()`(daemon 线程)、`[tool.hermes.precompile]` 目标读取、`--if-needed`/`--force` CLI。`hermes_bootstrap.maybe_precompile_on_start()` 仅当 `HERMES_PRECOMPILE_ON_START` 已设且不在 pytest/冻结下运行后台预热(隔离的逐文件测试运行器给每个文件一个全新临时 `HERMES_HOME`,无门控的预热会在每个子进程里起一个 `compileall` 线程)。`pip install` 已对安装包做字节编译;这是给源码运行布局(CN fork 的 Windows 默认)准备的——那里首次 import 否则要付 `builtins.compile`。 +3. **每请求微优化。** `agent/message_utils.get_tool_call_function_and_id()` 把 `sanitize_api_messages` 此前对每个 tool_call 做的两次 `isinstance(tc, dict)` 分发(`get_tool_call_function` + `get_tool_call_id`)合并为一次;sanitizer 在每次 LLM 请求前对整段历史运行,因此对工具调用密集的会话有可测收益。`tools/registry.get_definitions()` 现在只在短暂锁内快照*被请求的*条目,不再每次调用物化整个 ~250 工具注册表的 `{name: entry}` map。 + +**测试。** `tests/test_import_accelerator.py`(新增):精选名构建(agent 是包、`agent.py` 永不是模块、`packaging`/`tests` 永不注册)、保留 `.pyc` 的 `SourceFileLoader` spec、O(1) 透传、子模块透传、幂等安装/卸载、env 关闭、以及 `test_import_hook_bypass`——子进程证明把仓库根从 `sys.path` 剥掉后模块仍经加速器导入(且不咨询 `PathFinder`),而移除加速器的对照组失败。`tests/test_precompile.py`(新增):指纹稳定性/敏感性、`precompile_if_needed` 编译→跳过→变更后重编译→force、后台完成、`precompile_all` 回归。`tests/agent/test_message_utils.py`(扩展):`test_isinstance_caching` 把融合访问器与两个分离访问器在 dict/SDK 对象/畸形输入上逐字节钉死。所有既有 sanitizer/registry/bootstrap/懒 import 不变量测试仍通过(受影响范围 581 个);`ruff check` 干净。 + +**可上游?** 加速器、precompile 幂等与 sanitizer/registry 微优化与 OS 无关;冷启动量级是 Windows/源码运行特有。相关:P-043、P-044。 + +--- + +### P-051:给每一条文本模式子进程管道钉死解码——消灭 GBK `_readerthread` UnicodeDecodeError 这一类 bug + +**现象。** zh-CN Windows(ANSI 代码页 936、py3.14 无 UTF-8 模式)上,CLI 在第一个对话回合结束后打印 `Exception in thread Thread-N (_readerthread): UnicodeDecodeError: 'gbk' codec can't decode byte 0xae ... illegal multibyte sequence`,随后继续运行,但受影响输出被静默丢弃。 + +**根因。** `subprocess.run(..., text=True)` 未传 `encoding=`/`errors=` 时,用 `io.TextIOWrapper` 以 `locale.getpreferredencoding(False)` 包住子进程管道——zh-CN Windows 上即 cp936/GBK。现代跨平台子进程(git、python、node、uv、docker、gh、…)无论 ANSI 代码页如何都写 UTF-8;一个 GBK 非法字节(如实 = `E5 AE 9E` 的中间字节 `0xAE`,中文提交信息常见)在 CPython 的 daemon `_readerthread`(`subprocess.py`: `buffer.append(fh.read())`)内抛错,只能经 `threading.excepthook` 浮出,且 `result.stdout is None`——调用方永远看不到异常。报告的实例是会话开始的 workspace 快照(`agent/system_prompt.py` → `coding_system_blocks()` → `agent/coding_context.py::_git()` 对含中文提交信息的仓库跑 `git status --porcelain=2 --branch` / `git log -3 --pretty=%h %s`):reader 线程死亡后 `_git()` 撞上 `None.strip()`,`system_prompt.py` 的宽 `except` 吞掉——整个 workspace 块从 system prompt 里静默消失。用 `python -X utf8=0`(→ cp936)对输出 UTF-8 CJK 的子进程逐字节复现:同样的 `subprocess.py:1613 _readerthread` 帧、同样的 `0xae` 消息、之后 `stdout is None`。 + +**修复。** 类级修复,不止报告的那个点——全仓库 AST 审计发现 shipped 代码中 **269 处文本模式子进程调用点**缺显式解码(另有 3 处藏在 `**kwargs` dict 展开后面): +1. **按约定输出 UTF-8 的子进程**(git、gh、rg、python/pip/uv、node/npm、docker、ssh/scp、systemctl/loginctl/launchctl/ps/lsof、tmux、s6、cosign、codex/claude CLI、secret CLI、hermes 自身、用户 shell 片段、…):钉 `encoding="utf-8", errors="replace"`——按子进程实际写出的内容解码,坏字节永远杀不死 reader 线程。 +2. **输出 OEM/ANSI 代码页的 Windows 原生工具**(`tasklist`、`taskkill`、`netstat`、`where`):只钉 `errors="replace"`——locale 解码在那里才是忠实选择(本地化消息与中文路径保持正确);唯一的 bug 是严格性。这与 `gateway/status.py::terminate_pid` 既有的文档化加固一致。 +3. 三处 `**kwargs` dict 展开点(`hermes_cli/web_server.py::_fs_git_branch`、`tools/tts_tool.py::_run_command_tts`、`tools/transcription_tools.py::_run_command_stt`)在各自 dict 内补同样钉法。 +4. 顺手修复:`hermes_cli/_subprocess_compat.py` 的(当前未用的)drop-in wrapper 用了 `subprocess.run`/`Popen` 却 `import subprocess`——潜在 `NameError`;补上 import,wrapper docstring 现在记录文本模式解码陷阱。 + +**回归防护。** `tests/test_subprocess_text_pipe_decoding.py`(新增):(a) 对 shipped 代码的静态 AST 不变式——每个文本模式 `subprocess.run/Popen/call/check_call/check_output` 调用(含具名 dict 与 inline `**dict` 展开形式)必须传显式 `errors=`——让这一类不可能经未来代码或上游同步卷土重来;(b) 行为测试把对任何 codec 都非法的字节经 `coding_context._git` 管道,断言线程存活且 UTF-8 CJK 完整落地(解码已钉死,任何 locale 下确定性通过)。沿用 `tests/tui_gateway/test_slash_worker_utf8_decode.py` 的先例(同一 bug 类更早为长生命周期子进程管道修复,commit c210b9621)。 + +**测试。** 新守卫:2 通过。受影响范围扫描(330 个测试文件,覆盖每个被编辑模块——clipboard、docker/ssh/singularity 环境、kanban、gateway、tui_gateway、cli worktree 助手、main 更新流程、tools_config、lazy_deps、computer_use、cron、webhook、transcription/tts/voice、secret 源、插件)在分支与未改动基线上都跑:基线 206 失败 vs 分支 209 失败,其中 205 个是相同的既有批量组合污染(每个抽查失败单独跑都通过;如 `test_update_autostash.py` 的 4 个文件级失败在未改动的基线上也复现)。4 个分支独有失败是 exact-kwargs change-detector 断言(`test_webhook_integration`、`test_whatsapp_connect`、`test_update_autostash`、`test_docker_environment`)——更新为新契约;whatsapp `_terminate_bridge_process` 的 taskkill 点移到 locale 风格(仅 `errors="replace"`)组以匹配其 Windows 原生输出。1 个基线独有失败是 flaky 污染。`scripts/check-windows-footguns.py --all` 干净(793 文件);`ruff check .` 构造上干净(只开 PLW1514/`open()`-encoding,本次未触及;CI lint.yml 是执行门)。 + +**是否上游?** 是——逐点钉法与 AST 不变式测试 OS 无关(崩溃只在非 UTF-8 Windows locale 上*显现*,但把 UTF-8 管道按 locale 解码在哪里都是错的)。相关:c210b9621(slash worker + Copilot ACP 管道钉法)、P-042(Windows 子进程开销)、P-019(Windows shell 处理)。 + +--- + ## Windows 兼容性补丁 以下补丁由 Maxwell Geng 贡献,用于提升 Windows 平台的一等支持体验,均可向上游提交。 diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py index 2e3e8301f9606..481b5a76c1022 100644 --- a/agent/agent_runtime_helpers.py +++ b/agent/agent_runtime_helpers.py @@ -2990,6 +2990,7 @@ def _execute(next_args: dict) -> Any: _todo_tool( todos=next_args.get("todos"), merge=next_args.get("merge", False), + auto_fix=next_args.get("auto_fix", True), store=agent._todo_store, ), next_args, diff --git a/agent/coding_context.py b/agent/coding_context.py index f964c1042728c..08662c7826715 100644 --- a/agent/coding_context.py +++ b/agent/coding_context.py @@ -215,53 +215,26 @@ def _edit_format_line(model: Optional[str]) -> str: # search_files, patch, write_file, terminal, todo) are in the coding toolset and # in _HERMES_CORE_TOOLS, so they're present on every surface this fires on. CODING_AGENT_GUIDANCE = ( - "You are a coding agent pairing with the user inside their codebase. " - "Operate like a careful senior engineer.\n" + "You are a coding agent pairing with the user inside their codebase.\n" "\n" - "Gather context first:\n" - "- Read the relevant files with `read_file` and locate code with " - "`search_files` before changing anything. Trace a symbol to its definition " - "and usages rather than guessing its shape.\n" - "- Batch independent lookups: when several reads/searches don't depend on " - "each other, issue them together in one turn instead of one at a time.\n" - "- Never invent files, symbols, APIs, or imports. If you haven't seen it in " - "the repo, go look. Don't assume a library is available — check the project " - "manifest (pyproject.toml / package.json / Cargo.toml / go.mod) and how " - "neighbouring files import it.\n" + "Context first: read with `read_file`, locate with `search_files` before changing; " + "never invent files, symbols, APIs, or imports; batch independent lookups; " + "check the project manifest (pyproject.toml/package.json/Cargo.toml/go.mod) for deps.\n" "\n" - "Make changes through the tools, not the chat:\n" - "- Edit with `patch`/`write_file`. Do NOT print code blocks to the user as " - "a substitute for editing — apply the change, then summarise it. Only show " - "code when the user explicitly asks to see it.\n" - "- Match the project's existing style and conventions; AGENTS.md / " - "CLAUDE.md / .cursorrules already in context win over your defaults. Touch " - "only what the task needs — no drive-by refactors, renames, or reformatting " - "— and add any imports/dependencies your code requires.\n" - "- If an edit fails to apply, re-read the file to get the current exact " - "contents before retrying — don't repeat a stale patch. If the same region " - "fails twice, rewrite the enclosing function or file with `write_file` " - "instead of attempting a third patch.\n" + "Change via tools, not chat: edit with `patch`/`write_file` — never print code blocks as a " + "substitute; apply, then summarise. Match project style (AGENTS.md/CLAUDE.md/.cursorrules win); " + "no drive-by refactors; add the imports/deps your code requires. If an edit fails, re-read for " + "current exact contents; after two failed patches, rewrite the function/file with `write_file`.\n" "\n" - "Verify, and know when to stop:\n" - "- Use `terminal` for git, builds, tests, and inspection. Run the relevant " - "tests/linter/build and confirm they pass before claiming the work is done.\n" - "- Terminal state persists across calls: current directory and exported " - "environment variables carry forward. Activate a virtualenv or export setup " - "vars once, then reuse that state instead of re-sourcing it before every " - "test command.\n" - "- Fix root causes, not symptoms: when you find a bug, check sibling call " - "paths for the same flaw and fix the class, not just the reported site.\n" - "- When fixing linter/type errors on a file, stop after about three " - "attempts on the same file and ask the user rather than looping.\n" - "- Track multi-step work with `todo`. Reference code as `path:line` instead " - "of pasting whole files.\n" + "Verify and stop: use `terminal` for git/builds/tests; confirm tests/linter/build pass before " + "claiming done. Terminal state persists across calls: cwd and exported env vars carry forward. " + "Activate a virtualenv or export setup vars once, then reuse instead of re-sourcing it before " + "every test command. Fix root-cause classes — check sibling call paths. Stop after ~3 linter " + "attempts on a file, then ask. Track multi-step work with `todo`; reference code as `path:line`.\n" "\n" - "Respect the user's repo: don't commit, push, or rewrite history unless " - "asked, and never read, print, or commit secrets — leave `.env` and " - "credential files alone unless the user explicitly asks. The Workspace " - "block below is a snapshot from session start — re-run `git status`/" - "`git branch` before relying on it. Be concise: lead with the change or " - "answer, not a preamble." + "Respect the repo: no commit/push unless asked; never read, print, or commit secrets — leave " + "`.env`/credential files alone. Workspace snapshot is from session start — re-run `git status`/" + "`git branch` before relying on it. Be concise." ) diff --git a/agent/tool_executor.py b/agent/tool_executor.py index b88a524b71ed7..12a1b454b012c 100644 --- a/agent/tool_executor.py +++ b/agent/tool_executor.py @@ -1609,6 +1609,7 @@ def _execute(next_args: dict) -> Any: return _todo_tool( todos=next_args.get("todos"), merge=next_args.get("merge", False), + auto_fix=next_args.get("auto_fix", True), store=agent._todo_store, ) function_result, function_args, middleware_trace, _execution_blocked, _execution_dispatched = _managed_values(_run_agent_tool_execution_middleware( diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index 8bd098886a0f6..5a12f28793f4e 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -4196,23 +4196,25 @@ async function ensureRuntime(backend) { } // On Windows, preflight the configured shell. When the user has set - // shell:bash, verify Git Bash is installed (no auto-download). Otherwise + // shell:bash, verify Git Bash is installed (no auto-download); a missing + // Git Bash is NOT fatal — the core terminal falls back to PowerShell + // (pwsh → powershell.exe), so warn and continue. Otherwise // (auto/pwsh/powershell), check that PowerShell is available — it ships // with every Windows 10/11 system so the check is confirmatory. if (IS_WINDOWS) { const configShell = (process.env.HERMES_SHELL_TYPE || 'auto').toLowerCase().trim() const useBash = configShell === 'bash' - if (useBash) { - if (!findGitBash()) { - throw new Error( - 'You have configured shell:bash, but Git for Windows (Git Bash) was not found. ' + - 'Install it from https://git-scm.com/download/win and ensure it is on your PATH, ' + - 'then relaunch Hermes. Alternatively, set shell to "auto", "pwsh", or "powershell" ' + - 'to use PowerShell (the default shell on Windows).' + if (useBash && findGitBash()) { + // Git Bash found — it will be used, nothing more to verify. + } else { + if (useBash) { + console.warn( + '[hermes] You have configured shell:bash, but Git for Windows (Git Bash) was not found. ' + + 'Hermes will fall back to PowerShell. Install it from https://git-scm.com/download/win ' + + 'and ensure it is on your PATH to use Git Bash, or keep the default shell "auto".' ) } - } else { // Verify at least one PowerShell is available (ships with every Windows system). const hasPowerShell = findOnPath('pwsh.exe') || diff --git a/cron/scheduler.py b/cron/scheduler.py index 4432e06926e2d..34ffa219403c1 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -2319,6 +2319,118 @@ def _windows_cron_python_invocation(python_exe: str) -> tuple[str, dict[str, str return str(interpreter), env_overlay +def _is_frozen_runtime() -> bool: + """True when running inside the PyInstaller-frozen CN portable runtime. + + The desktop runtime ships as a frozen ``hermes-agent-cn-runtime-*.exe`` + with no standalone ``python.exe`` — inside it ``sys.executable`` IS the + Hermes CLI binary itself. Spawning it with a script path would run + ``hermes