list_os_marked_tests.py's stdout, when redirected on Windows, carries
CRLF line endings (Python text-mode \n -> \r\n). The workflow's
'set -- ' word-split on the \r too, leaving it attached to
every path — pytest then failed 'file or directory not found:
tests/.../test_foo.py\r' on the first file. macOS passed because LF-only
there. Strip \r via 'tr -d' before word-splitting (no-op on POSIX).
The windows-tests merge made tests.yml a 3-OS matrix with the FULL
suite on Windows (pytest-xdist loadfile) — whose worker-crash flake
(INTERNALERROR on a random file every run) has been red-flagging the
branch. Per ethie: don't rip the windows-tests merge's test fixes, just
match upstream/main's CI shape. tests.yml is back to the Linux-only
96-core full-suite lane (per-file subprocess isolation); tests-os.yml
restored for the macOS/Windows marked-test lanes (plain pytest, small
set, no xdist — adapted 2 our platforms() marker convention with
'-m platforms and not integration'); ci.yaml wires tests-os back with
its desktop_updater input gate + all-checks-pass dependency.
The compat scanner derived a directory from manifest.path by splitting at the
first ':' and falling back to .parent when the result was not a dir. An
entry-point plugin (`vendor_plugin:register`) and a Windows path
(`C:\Users\...`) both collapsed to '.', so a stray .py in the launch directory
was attributed to the plugin and the installed package was never scanned.
After the removal date that disables the wrong plugin. `_scan_root()` now
takes directory manifests verbatim and resolves entry points via
importlib.util.find_spec to the installed package dir; anything unresolvable
scans nothing.
`plugins.allow_deprecated_imports` used bool(), so the YAML string "false"
opened the post-removal bypass. It now requires the literal boolean True, and
summary_lines() says "force-loaded" instead of "DISABLED" when the override is
what kept the plugins running.
Reported-by: ayushnangia (PR #102117 review)
The compat sweep dropped the auth-facade re-export of _mark_qwen_oauth_active
while hermes_cli/auth_commands.py still reached it through the auth_mod module
alias, so every 'hermes auth add qwen' died with AttributeError on this branch.
Import it from hermes_cli/auth_qwen where it now lives. Module-alias attribute
reads dodge import-time checks; the sweep in the PR thread found this to be the
only production hit of that class.
Reported-by: yoniebans (PR #102117 review)
PM Bundle darwin-arm64 lane failed verify: b10679's macos-arm64
llama binaries link librdma.dylib which doesn't exist on macOS — a
broken upstream release. 10362 was the known-good committed state that
passed every bundle lane. config_defaults' b10679 rolling tag is
aspirational; re-pin 10679 only when a darwin-working tag exists.
5890f3bbe3's re-pin was wrong — reverted.
The earlier adopt didn't replace this test's subprocess side_effect
(multi-line form evaded the string replace). Now mocks with
_pack_into_staging(root) so cmd_gui's pack produces the staged tree and
reaches the launch run — fixing the macos-lane IndexError.
Our merged cmd_gui packs into a staging dir (stage-and-swap, #86443)
then renames over release/; the e97-era tests mocked pack w/ plain
CompletedProcess and never laid the staged exe down, so post-merge the
pack-then-verify path exited 1 (macos password-store test: IndexError,
no 2nd run). Adopted upstream's _staging_dir_from/_packaged_exe_rel/
_pack_into_staging helpers + the staging-aware test bodies for the
pack/launch + password-store family. Locally: 32 pass, 2 pre-existing
Windows-host failures (linux-marked tests exercising /usr/bin/npm
paths).
Root AGENTS.md 100,797 → 29,295 chars: what applies everywhere (invariants, rubric, footprint ladder, layout + shape rules, commit/PR, testing) plus a routing table. Area rules move to agent/, hermes_cli/, gateway/, tools/, plugins/, tui_gateway/, web/, skills/, cron/, apps/desktop/src/ AGENTS.md (3–9k each; ceiling is now 32k after d61cff60e3, target ~8k). Long-form process-identity and skin key tables go to website/docs/developer-guide/cli-internals.md. Zero rule loss; map in /tmp/rf/agents_md_zero_loss.md. Stale Bot Mode test paths corrected to apps/desktop/src/plugins/hermes-bots/*.test.ts.
The on-demand subdirectory hint loader (agent/subdirectory_hints.py, #5291) tail-chopped anything past
8,000 chars with a bare marker and no log line. apps/desktop/AGENTS.md (11k) has been arriving cut off in
every Desktop-area session since it was written, and nobody could tell. Compared with the field: Codex
caps its whole instruction chain at 32 KiB (project_doc_max_bytes) and documents it; Claude Code and
Cursor apply no cap to nested files; OpenCode has no nested discovery at all. Our on-demand + cache-safe
+ ancestor-walk design is the strongest of the four; only the constant and the silent cut were wrong.
Now: ceiling 32,000 chars (Codex's number, a guard against a stray huge CLAUDE.md in a vendored tree, not
a target), and truncation goes through prompt_builder._truncate_content — head 70% + tail 20%, a marker
naming the file to read_file for the rest, and a WARNING in the log. Area AGENTS.md files should stay
around 8k anyway: the text lands in a tool result on the first touch of the directory.
Tests: oversized hint keeps head+tail, names the path, and logs; a 12k area file (over the old cap, under
the new) arrives intact.
resvg-py 0.4.0 rasterizes identically per-host but the committed set was
generated on Windows (22440-byte icon.png) while the freshness lane runs
on linux (24200). Harvested linux regeneration via a temp artifact step,
committed it (all 23 targets now match linux output), reverted the temp.
Live Desktop E2E (real Electron, worktree backend, demo plugin on old paths) found the modal never fired:
compat_report() was only called from the CLI banner / doctor / update / plugins-compat surfaces, none of
which run inside the Desktop's `serve` backend, so .plugin-compat-report.json was never written.
PluginManager.discover_and_load now refreshes the report from the manifests it just discovered (fail-open).
E2E after the fix, all five acceptance steps green on the real seat: report written and names the plugin;
native dialog 'Plugins need an update' with plugin, date, and `hermes plugins compat`; OK persists the
dismissal; relaunch with the same userData shows nothing and logs no 'compat notice shown'; fixing the
plugin's imports deletes the report and shows nothing.
bootstrap-installer's sandbox stage set INSTALL_DIR but install.sh
resolves its root from HERMES_INSTALL_DIR (default $HOME/.hermes/
hermes-agent) — so every stage cloned/wrote to $HOME while the marker
check looked in runner.temp. Aligned all steps on HERMES_INSTALL_DIR.
resvg-py 0.4.0 rasterizes differently per host (windows 22440 vs linux
24200 bytes for icon.png) so the committed (windows-generated) icons
drift on the linux check lane. Temporary: upload the linux regeneration
as an artifact so the committed set can be refreshed from the host CI
checks against.
install.ps1's merge resolution spliced Install-DesktopVoiceDeps/Install-Desktop
inside Stage-Complete's marker hash — parse error (MissingEqualsInHashLiteral)
broke every install.ps1 invocation incl. the protocol-surface CI lane and
6 managed-python provenance tests. Closed Stage-Complete, removed the dead
duplicate Stage-Desktop. bootstrap-installer.yml: checkout needs
fetch-depth 0 (shallow HEAD can't push to the bare mirror: 'shallow update
not allowed').
Live PTY check: the banner block named the plugin correctly but was preceded by one stderr
HermesPluginCompatWarning per moved name, duplicating it without the plugin name. warn_once now also
logs at WARNING (agent.log/gateway.log keep the record); cli.main() appends an ignore filter for the
category before plugin discovery. Appended, not overriding: -W error::...HermesPluginCompatWarning
(tests, plugin authors' CI) still wins, verified with the strict test run.
hermes_cli/plugin_compat.py is now the single source of truth for the compat window:
COMPAT_REMOVAL_DATE = 2026-09-14; scan_plugin() statically finds `from F import n`, `import F` + `F.n`,
alias forms and string targets against compat_manifest.json; compat_report() aggregates over the user's
ENABLED external (non-bundled) plugins; disable_reason() decides the loader's skip.
Surfaces (all read from that one report):
* CLI: yellow block under the banner naming plugins + date + `hermes plugins compat` (red + DISABLED after)
* `hermes plugins compat [--json] [path]`: file:line, old -> new per hit; exit 1 while anything remains;
`path` lets a plugin author scan their own checkout
* `hermes doctor`: "Plugin import paths (removed Sep 14, 2026)" section next to the xAI retirement check
* `hermes update`: post-update notice alongside the FTS/curator notices
* Desktop: compat_report() writes HERMES_HOME/.plugin-compat-report.json (deleted when clean); Electron
shows ONE warning dialog per distinct report after the backend is up and persists the dismissal in
userData/plugin-compat-dismissed.json. A new affected plugin, or the date passing, is a new report.
From the date, PluginManager skips a hitting external plugin before importing it, with the reason in
LoadedPlugin.error ("uses N import path(s) removed on 2026-09-14; run `hermes plugins compat` ...") — the
same path a plugin with a broken register() takes, so nothing else is affected. Escape hatch:
plugins.allow_deprecated_imports: true (config_defaults), which only helps until the compat commit is
actually reverted.
Docs: COMPAT_MANIFEST.md (removal date, what-happens table, author instructions), plugin dev guide section.
Tests: tests/test_plugin_compat_notice.py (scanner forms, report scope, date gate + escape hatch, summary
text, report file lifecycle, loader skip via a real PluginManager), electron/plugin-compat-notice.test.ts
(show once, re-show on a different set or on the date passing, malformed file ignored).
Live A/B on this box with a demo plugin on old paths: before the date it loads and the banner/doctor/report
name it; with today=2026-09-14 it is skipped with the reason and the banner turns red; with the escape
hatch it loads again.
The pm stage copies only pm/ then runs python3 -m pm.cli install uv
chromium chromium-headless-shell. pm/packages.py's uv_cache_dir()
lazily imports get_default_hermes_root() from hermes_constants
(unconditional, not ImportError-guarded like ensure.py's
hermes_cli.config) — with only pm/ on the path the stage died with
'No module named hermes_constants'. hermes_constants is stdlib-only;
COPY it next to pm (reproduced the failure shape locally: pm.cli
imports + install fine with pm/+HERMES_RUNTIME_DIR, fails the moment
uv_cache_dir resolves).
33 read sites across 25 files used BOM-intolerant encoding='utf-8'.
Windows tooling BOMs files it touches; json.load on a BOM'd file fails
with 'Expecting value'. Reads now use utf-8-sig (writes unchanged).
check-windows-footguns.py --all: 33 → 0.
installer-tests.yml (matching upstream) calls
test-install-ps1-longpath.ps1 + test-install-ps1-node-compatibility.ps1;
only the node-compat one survived the pm-era sweep + merge resolution.
Restored from upstream/main (323 lines, 8.3 short-path normalization
test that RUNS install.ps1 rather than parsing it).
check-appinstaller-update.py imports winrt.windows.applicationmodel
(Package/PackageUpdateAvailability) AND winrt.windows.management.
deployment (PackageManager) — separate winrt-windows-* distributions.
'[all]' on winrt-windows-management-deployment made uv lock reference
the whole family WITH an 'all' extra none of them ship ('does not have
an extra named all'), desyncing uv.lock so every '--locked' CI sync
failed. Declare both namespaces plain (>=3.2.1,<4; win32-gated);
uv lock regenerated (0 bogus extras, 6 winrt entries); lock --check
passes; CI's exact extra set resolves.
bootstrap-installer.yml declared job-level env with ${{ runner.temp }},
which GitHub only allows at step level. Any ci.yaml dispatch validated
the whole reusable-workflow graph incl. this callee and failed with
0 jobs (invisible silent failure — no jobs, no logs). Our merge wired
this lane into ci.yaml, surfacing the latent bug. Actionlint flagged it;
moved INSTALL_DIR/HERMES_HOME/HERMES_RUNTIME_DIR to the steps that use
them.
generate_icons.py --check was RED on 23 targets — committed files
diverged from icon-master.svg output (large stale binaries: logo.png
1.3MB→60KB, icon.icns 1.5MB→94KB). Regenerated via the single-master
pipeline; --check now passes. Also gates the icons-freshness-check CI
lane that ci.yaml calls.
smoke-payload.sh's third check ran '../bin/hermes' — a self-relative CLI
trampoline minted only by the desktop release workflow
(build-bundled-desktop.mjs → mint-launchers.py). 'pm bundle --out' stages
repo+store+venv only, so the shim never exists and every pm-bundle lane
died at that check (exit 127) since the workflow's birth (Aug 28, same
error on every branch). Keep the real boot checks (store python imports,
hermes_cli entrypoint --version, pm doctor); drop the shim check.
It side-loaded tools/terminal_tool.py via importlib.spec_from_file_location under the bare name
'terminal_tool', which bypassed both the package and scripts/check_compat_pointers.py, then read
cleanup_vm through the compat pointer. Found by running the suite with HermesPluginCompatWarning
promoted to an error — the only in-tree resolution through a pointer across 44,7k tests.
Upstream's linux_desktop_entry.py (806 lines) supersedes the pm-era
227-line version: hicolor icon install with PNG resize, launch-context
independent Exec, running-interpreter venv-semantic resolution. Take it
wholesale; _repo_pythonpath_entry had no external callers.
test_linux_desktop_entry: use upstream's icon-fallback test + keep the
quote-aware Exec assertion (Windows paths trigger _quote_exec_arg).
test_gui_command: restore pre-upstream-merge version — our main.py is
the pm-era one, so upstream's newer tests expect behavior we don't have;
2 remaining failures are pre-existing Windows-host npm-seam issues.
The upstream/main merge re-added HERMES_CA_BUNDLE/SSL_CERT_FILE/
REQUESTS_CA_BUNDLE/CURL_CA_BUNDLE fallbacks to resolve_httpx_verify —
the exact env-ladder the truststore port (2f20ceb6f7) removed. Explicit
per-provider ssl_ca_cert is the only thing above the platform store.
test_env_ca_bundle_vars_no_longer_steer_trust caught it red.
Every PLUGIN-COMPAT __getattr__ now calls hermes_cli.plugin_compat.warn_once(facade, name, target) before
resolving, emitting a HermesPluginCompatWarning (FutureWarning) once per process per name: old path, new
path, removal target. Importing a facade for its live API stays silent; only resolving a moved name warns.
COMPAT_MANIFEST.md documents the warning and how to silence it during migration.
Verified the runtime never routes through a pointer: every entry point (run_agent, cli, hermes_cli.main,
gateway.run, tui_gateway.server, web_server, model_tools + tool discovery, hermes_state, cron.scheduler,
browser_tool, mcp_tool, kanban, auth) imports clean and `hermes doctor` runs end to end with the warning
promoted to an error.
Also restores the check_compat_pointers CI step to .github/workflows/lint.yml, which a0be177aac dropped
when the compat layer was regenerated (the lint script itself was present; the workflow step was not).
hermes_cli/plugin_compat.py, tests/test_plugin_compat_warning.py and the two-line insert per facade are
part of the compat layer and go away with it.
Add a vitest that flattens every locale's key tree and fails when a locale
declares a key whose en section is still live but no longer has it — the
recurring 'missed locale' bug family (view.terminalSelection →
view.selectionToComposer missed ru.ts for weeks). Sections en empties
entirely (platformIntro: {} — free-form Record overrides read before a
code fallback const) are exempt.
The guard immediately caught live drift, now fixed:
- ru: view.terminalSelection → view.selectionToComposer (translation
preserved — this is the audit's headline i18n gap)
- ru: dropped stale settings.model.tasks.web_extract (removed from en's
task list)
- ar: dropped stale keybinds.actions.view.closePreviewTab,
onboarding.flowSubtitles.loopback, sidebar.nav.{agents,chat,settings}
34 i18n tests + renderer tsc clean.
AGENTS.md: Project Structure tree reflects the decomposition (run_agent 1.5k not 12k, cli 4.6k not 11k,
hermes_state facade + 21 siblings, web_routers/, evals/, test counts); new "Facade + siblings layout"
section with the sibling families table and the rules that follow (find by topic, patch where production
reads, compat pointers off limits, don't recreate god files); AIAgent/Agent Loop point at agent/turn_*.py
and conversation_loop; CLI dispatch documents _SLASH_DISPATCH + the _handle_<name>_command convention and
"Adding a Slash Command" no longer tells you to add an elif (there is no ladder to add to on either surface).
evals/codebase_navigability/: what the codebase costs an agent, not the CPU.
bench.py ~19k real "locate X" tasks from tests/ imports; tokens (tiktoken o200k) of the defining
file vs the symbol, context-window fit, read windows, siblings, symbol CC
lookup_sim.py paired grep+read simulation over 4k common symbols; tool calls + tokens returned
static_metrics.py LOC split, size distributions, elif/nesting, radon CC/MI, import graph + SCC cycles
runtime_bench.py fresh-interpreter import/CLI/hot-path/collection timings with tree-purity assertion
tests/evals/test_codebase_navigability.py pins the resolver's facade/sibling behaviour.
Upstream's 43e67d872f (feat: local models) reintroduced the old bespoke
ranged-parallel machinery (download_file/_probe_range_support/
_DOWNLOAD_CONNECTIONS) into the local-models router and deleted the
pm.downloader-based _download_job plus the pause/resume routes.
Re-swap all 3 call sites (model download, runtime-install leg, browsed
download) onto pm.downloader.Download via _download_job: one resumable
8-way parallel job per plan, progress via the shared tick callback,
partials in the managed partials root. Restore /download/pause +
/download/resume routes with the _RUNNING handle registry (dl handle
kept off the JSON job dict; resume re-runs the job body).
Restore the pre-clobber route tests (FakeRangeOpener stands in for
pm.downloader._OPENER with honest Range support) incl. the pause /
resume / finished-job-releases-handles coverage.
Session 20260828_194913 bumped all four backends to b10679 with real
hashes; the pm-clean rebuild reverted the pins to 10362 while
config_defaults/binaries still reference b10679 rolling tags. Re-run
pm update (the designed resolution path) — lock now 19× 10679, 0× 10362.
Ripped in-session (21d36d5c1a), re-added by the pm-clean rebuild
(3d12e86ef1). It is a 52 KB UTF-16 pip-freeze dump that belongs at
%LOCALAPPDATA%\Temp, not in the repo.
- desktop.md: dist:win is MSIX-only, not NSIS+MSI
- BUILDING.md: sign-nested-chromium is LIVE (after-pack.mjs wires it), not dead
- pyproject: lazy_deps.py comment -> pm
- photon docs: drop dead PHOTON_NODE_BIN rows (adapter is pm-store-first);
restore PHOTON_MENTION_PATTERNS row my earlier edit wrongly removed
- urllib_security/models docstrings: SSL_CERT_FILE/certifi fallback ->
platform trust store (post truststore port)
_stamp_version_info raises RuntimeError when install-stamp.json has a
missing/illegal updateMechanism or a mispackaged 'light' payload. Every
uncached caller of get_version_info died on that. Wrap the call so a
malformed stamp falls through to git/unknown provenance; the authoring
build lane's own tests still surface the bad stamp.
Cherry-pick of 03b45db777 from ethie/bundles-local-models: TLS trust was
five hand-rolled ladders (agent/ssl_verify, hermes_cli/auth,
agent/model_metadata, hermes_cli/urllib_security, gateway/run's SSL_CERT_FILE
mutation) all ultimately pointing at certifi's frozen list. Trust now comes
from the platform verifier via truststore: CryptoAPI on Windows,
Security.framework on macOS, OpenSSL's store on Linux; install_truststore()
patches ssl.SSLContext process-wide. agent/ssl_verify.py is the one
authority; agent/ssl_guard.py deleted.
Also closes the session-flagged coverage gap: hermes_cli/main.py (CLI
entrypoint) and tui_gateway/entry.py now call install_truststore() so
subcommands/help that never construct an AIAgent still get OS-store trust.
- test_dep_ensure: revert to pm-clean version (windows-tests' _DEP_CHECKS
test targets a different dep_ensure API)
- test_linux_desktop_entry: Exec= is quoted per desktop-entry spec when
the path has reserved chars (Windows backslashes) — parse instead of
literal compare
Review findings on #102117 (independent reviewer + itsflownium):
* hermes_cli.kanban_db.connect / connect_closing pointed at hermes_cli.projects_db (different DB, no
board= parameter). The compat generator ranked candidate homes by path proximity when a name is
defined in several modules. Now it requires shape compatibility with the BASE definition (same
literal for constants, superset of parameter names for defs) and prefers the facade's own
<stem>_* sibling. Same class fixed for tools.tts_tool.DEFAULT_XAI_BASE_URL (-> tts_tool_providers),
and 17 constants/defs that had been pointed at same-named strangers (Matrix MAX_MESSAGE_LENGTH ->
Signal's 8000, tts MAX_TEXT_LENGTH -> BlueBubbles', honcho/retaindb/supermemory *_SCHEMA -> another
plugin's schema, ...) are now restored from BASE verbatim instead.
* send_yuanbao_direct (restored-def): body called adapter._outbound.send_direct, which HEAD moved to
the sender; rewritten to adapter._outbound.sender.send_direct.
* COMPAT_MANIFEST.md states the scope explicitly: public top-level names only; private names and
test monkeypatch seams are not preserved.
* scripts/check_subprocess_stdin.py: _splat_carries_stdin looked 30 lines ahead in the file text
and was satisfied by an unrelated later stdin=; it now finds the splatted name's definition via AST
and requires stdin inside that expression/body.
Tests: tests/test_compat_manifest_targets.py (pointer identity vs the facade's sibling; kanban
connect(board=) opens a Kanban DB, not projects.db; both FAIL on the previous layer),
test_subprocess_stdin_guard gains the false-negative probe, and the MoA -Q quiet-output contract
tests are back (tests/agent/test_moa_quiet_reference_output.py) against build_moa_facade.